Compare commits
5 Commits
c85200ed64
...
8205d4203b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8205d4203b | ||
|
|
1a5e70890c | ||
| 2a9b751053 | |||
|
|
97b91013dd | ||
|
|
1cf8cc3aa5 |
@ -38,7 +38,18 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ label: 'Clasificaciones Comerciales', icon: 'pi pi-tags', to: '/catalog/classifications-comercial' },
|
{ 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: 'Documentos del Modelo', icon: 'pi pi-file', to: '/catalog/model-documents' },
|
||||||
{
|
{
|
||||||
label: 'Empresas',
|
label: 'Empresas',
|
||||||
@ -57,7 +68,14 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
{
|
{
|
||||||
label: 'Productos',
|
label: 'Productos',
|
||||||
icon: 'pi pi-shopping-cart',
|
icon: 'pi pi-shopping-cart',
|
||||||
to: '/products'
|
to: '/products',
|
||||||
|
permission: [
|
||||||
|
'products.index',
|
||||||
|
'products.show',
|
||||||
|
'products.store',
|
||||||
|
'products.update',
|
||||||
|
'products.destroy',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Requisiciones',
|
label: 'Requisiciones',
|
||||||
|
|||||||
@ -166,6 +166,8 @@ class AuthService {
|
|||||||
async getUserPermissions(): Promise<string[]> {
|
async getUserPermissions(): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/api/user/permissions');
|
const response = await api.get('/api/user/permissions');
|
||||||
|
console.log('User permissions response:', response);
|
||||||
|
|
||||||
return this.normalizePermissions(response.data);
|
return this.normalizePermissions(response.data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.resolveAuthError(error, 'Error al obtener permisos del usuario');
|
this.resolveAuthError(error, 'Error al obtener permisos del usuario');
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { onMounted } from 'vue';
|
|
||||||
|
|
||||||
import Card from 'primevue/card';
|
import Card from 'primevue/card';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
@ -16,9 +15,10 @@ import { useToast } from 'primevue/usetoast';
|
|||||||
|
|
||||||
|
|
||||||
import { supplierServices } from '../../services/supplier.services';
|
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 { SupplierType } from '../../types/suppliers.interfaces';
|
||||||
import SupplierModal from './SupplierModal.vue';
|
import SupplierModal from './SupplierModal.vue';
|
||||||
|
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||||
|
|
||||||
|
|
||||||
const suppliers = ref<Supplier[]>([]);
|
const suppliers = ref<Supplier[]>([]);
|
||||||
@ -40,7 +40,26 @@ const formErrors = ref<SupplierFormErrors>({});
|
|||||||
|
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const toast = useToast();
|
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) => {
|
const handleDelete = (supplierId: number) => {
|
||||||
|
if (!canDeleteSupplier.value) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Sin permisos', detail: 'No puedes eliminar proveedores.', life: 4000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
confirm.require({
|
confirm.require({
|
||||||
message: '¿Seguro que deseas eliminar este proveedor?',
|
message: '¿Seguro que deseas eliminar este proveedor?',
|
||||||
header: 'Confirmar eliminación',
|
header: 'Confirmar eliminación',
|
||||||
@ -71,11 +90,21 @@ const mapTypeToApi = (typeLabel: string | null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchSuppliers = async (page = 1) => {
|
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;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const name = searchName.value ? searchName.value : undefined;
|
const name = searchName.value ? searchName.value : undefined;
|
||||||
const type = selectedType.value ? mapTypeToApi(selectedType.value)?.toString() : 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 response = await supplierServices.getSuppliers(true, name, type);
|
||||||
const paginated = response as SupplierPaginatedResponse;
|
const paginated = response as SupplierPaginatedResponse;
|
||||||
suppliers.value = paginated.data;
|
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.first = (paginated.current_page - 1) * paginated.per_page;
|
||||||
pagination.value.rows = paginated.per_page;
|
pagination.value.rows = paginated.per_page;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Manejo de error opcional
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar los proveedores.', life: 3000 });
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
watch(
|
||||||
fetchSuppliers();
|
() => canViewSuppliers.value,
|
||||||
});
|
(allowed) => {
|
||||||
|
if (allowed) {
|
||||||
|
fetchSuppliers();
|
||||||
|
} else {
|
||||||
|
suppliers.value = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
const supplierTypes = [
|
const supplierTypes = [
|
||||||
{ label: 'Todos', value: null },
|
{ label: 'Todos', value: null },
|
||||||
@ -124,6 +161,10 @@ const onPageChange = (event: any) => {
|
|||||||
|
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
|
if (!canCreateSupplier.value) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Sin permisos', detail: 'No puedes crear proveedores.', life: 4000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
isEditMode.value = false;
|
isEditMode.value = false;
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
currentSupplier.value = null;
|
currentSupplier.value = null;
|
||||||
@ -131,6 +172,10 @@ const openCreateModal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openEditModal = async (supplier: Supplier) => {
|
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;
|
isEditMode.value = true;
|
||||||
formErrors.value = {};
|
formErrors.value = {};
|
||||||
loadingSupplier.value = true;
|
loadingSupplier.value = true;
|
||||||
@ -164,9 +209,15 @@ const handleModalSubmit = async (form: any) => {
|
|||||||
formErrors.value = {};
|
formErrors.value = {};
|
||||||
try {
|
try {
|
||||||
if (isEditMode.value && currentSupplier.value) {
|
if (isEditMode.value && currentSupplier.value) {
|
||||||
|
if (!canUpdateSupplier.value) {
|
||||||
|
throw new Error('without-update-permission');
|
||||||
|
}
|
||||||
await supplierServices.updateSupplier(currentSupplier.value.id, form, 'patch');
|
await supplierServices.updateSupplier(currentSupplier.value.id, form, 'patch');
|
||||||
toast.add({ severity: 'success', summary: 'Proveedor actualizado', detail: 'Proveedor actualizado correctamente', life: 3000 });
|
toast.add({ severity: 'success', summary: 'Proveedor actualizado', detail: 'Proveedor actualizado correctamente', life: 3000 });
|
||||||
} else {
|
} else {
|
||||||
|
if (!canCreateSupplier.value) {
|
||||||
|
throw new Error('without-create-permission');
|
||||||
|
}
|
||||||
await supplierServices.createSupplier(form);
|
await supplierServices.createSupplier(form);
|
||||||
toast.add({ severity: 'success', summary: 'Proveedor creado', detail: 'Proveedor registrado correctamente', life: 3000 });
|
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) {
|
if (e?.response?.data?.errors) {
|
||||||
formErrors.value = 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
|
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">Administra la base de datos de proveedores y
|
||||||
sus contactos.</p>
|
sus contactos.</p>
|
||||||
</div>
|
</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"
|
<SupplierModal :visible="showModal" :isEditMode="isEditMode" :supplier="currentSupplier"
|
||||||
:formErrors="formErrors" :loading="loadingSupplier" @update:visible="val => { if (!val) closeModal(); }"
|
:formErrors="formErrors" :loading="loadingSupplier" @update:visible="val => { if (!val) closeModal(); }"
|
||||||
@submit="handleModalSubmit" @cancel="closeModal" />
|
@submit="handleModalSubmit" @cancel="closeModal" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters Toolbar -->
|
<!-- Filters Toolbar -->
|
||||||
<Card>
|
<Card v-if="canViewSuppliers">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex flex-wrap gap-4 items-end">
|
<div class="flex flex-wrap gap-4 items-end">
|
||||||
<div class="flex-1 min-w-60">
|
<div class="flex-1 min-w-60">
|
||||||
@ -239,10 +306,10 @@ const typeSeverity = (type: number) => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Table Content -->
|
<!-- Table Content -->
|
||||||
<Card>
|
<Card v-if="canViewSuppliers">
|
||||||
<template #content>
|
<template #content>
|
||||||
<DataTable :value="suppliers" :loading="loading" stripedRows responsiveLayout="scroll"
|
<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">
|
<Column field="name" header="Razón Social" style="min-width: 180px">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div>
|
<div>
|
||||||
@ -285,9 +352,23 @@ const typeSeverity = (type: number) => {
|
|||||||
style="min-width: 120px">
|
style="min-width: 120px">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div class="flex items-center justify-end gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<Button icon="pi pi-pencil" text rounded size="small" @click="openEditModal(data)" />
|
<Button
|
||||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger"
|
v-if="canUpdateSupplier"
|
||||||
@click="handleDelete(data.id)" />
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
@ -298,8 +379,16 @@ const typeSeverity = (type: number) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
<Toast />
|
<Toast />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -14,10 +14,12 @@ import ConfirmDialog from 'primevue/confirmdialog';
|
|||||||
import ProgressSpinner from 'primevue/progressspinner';
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import Select from 'primevue/select';
|
import Select from 'primevue/select';
|
||||||
|
import UnitsEquivalenceModal from './UnitsEquivalenceModal.vue';
|
||||||
import { useUnitOfMeasureStore } from '../../stores/unitOfMeasureStore';
|
import { useUnitOfMeasureStore } from '../../stores/unitOfMeasureStore';
|
||||||
import UnitsForm from './UnitsForm.vue';
|
import UnitsForm from './UnitsForm.vue';
|
||||||
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../../types/unit-measure.interfaces';
|
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../../types/unit-measure.interfaces';
|
||||||
import { useAuth } from '@/modules/auth/composables/useAuth';
|
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||||
|
import { useUnitEquivalenceStore } from '../../stores/unitEquivalenceStore';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@ -41,6 +43,8 @@ const showDialog = ref(false);
|
|||||||
const isEditing = ref(false);
|
const isEditing = ref(false);
|
||||||
const selectedUnit = ref<UnitOfMeasure | null>(null);
|
const selectedUnit = ref<UnitOfMeasure | null>(null);
|
||||||
const unitsFormRef = ref<InstanceType<typeof UnitsForm> | 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 search = ref('');
|
||||||
const statusFilter = ref<'all' | 'active' | 'inactive'>('all');
|
const statusFilter = ref<'all' | 'active' | 'inactive'>('all');
|
||||||
|
|
||||||
@ -71,6 +75,16 @@ const canViewUnits = computed(() =>
|
|||||||
const canCreateUnit = computed(() => hasPermission('units-of-measure.store'));
|
const canCreateUnit = computed(() => hasPermission('units-of-measure.store'));
|
||||||
const canUpdateUnit = computed(() => hasPermission('units-of-measure.update'));
|
const canUpdateUnit = computed(() => hasPermission('units-of-measure.update'));
|
||||||
const canDeleteUnit = computed(() => hasPermission('units-of-measure.destroy'));
|
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
|
// Methods
|
||||||
const buildFilters = () => ({
|
const buildFilters = () => ({
|
||||||
@ -122,6 +136,14 @@ const openEditDialog = (unit: UnitOfMeasure) => {
|
|||||||
showDialog.value = true;
|
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) => {
|
const handleCreateUnit = async (data: CreateUnitOfMeasureData) => {
|
||||||
await unitStore.createUnit(data);
|
await unitStore.createUnit(data);
|
||||||
toast.add({
|
toast.add({
|
||||||
@ -435,6 +457,16 @@ watch(
|
|||||||
@click="confirmDelete(slotProps.data)"
|
@click="confirmDelete(slotProps.data)"
|
||||||
v-tooltip.top="'Eliminar'"
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
@ -465,5 +497,8 @@ watch(
|
|||||||
:is-editing="isEditing"
|
:is-editing="isEditing"
|
||||||
@save="handleSaveUnit"
|
@save="handleSaveUnit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Equivalences Modal (maquetado, no funcional) -->
|
||||||
|
<UnitsEquivalenceModal v-model="showEquivalenceModal" :unit="equivalenceUnit" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
||||||
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,11 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Card from 'primevue/card';
|
import Card from 'primevue/card';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Select from 'primevue/select';
|
||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import type { FixedAssetAssignmentFormData } from '../../types/fixedAssetAssignment';
|
import type { FixedAssetAssignmentFormData } from '../../types/fixedAssetAssignment';
|
||||||
|
import type { UserOption } from '../../services/fixedAssetsService';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
form: FixedAssetAssignmentFormData;
|
form: FixedAssetAssignmentFormData;
|
||||||
|
users: UserOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
@ -29,6 +32,26 @@ defineProps<Props>();
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Folio de Resguardo (Opcional)</label>
|
||||||
|
<InputText
|
||||||
|
v-model="form.receiptFolio"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Ej. GOLSRMVR-0170"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 md:col-span-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Autoriza (Opcional)</label>
|
||||||
|
<Select
|
||||||
|
v-model="form.authorizedById"
|
||||||
|
:options="users"
|
||||||
|
optionLabel="full_name"
|
||||||
|
optionValue="id"
|
||||||
|
placeholder="Seleccione quien autoriza..."
|
||||||
|
class="w-full"
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="space-y-2 md:col-span-2">
|
<div class="space-y-2 md:col-span-2">
|
||||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Notas o Comentarios (Opcional)</label>
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Notas o Comentarios (Opcional)</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import type {
|
|||||||
AssignmentEmployeeOption,
|
AssignmentEmployeeOption,
|
||||||
FixedAssetAssignmentFormData
|
FixedAssetAssignmentFormData
|
||||||
} from '../../types/fixedAssetAssignment';
|
} from '../../types/fixedAssetAssignment';
|
||||||
import { fixedAssetsService } from '../../services/fixedAssetsService';
|
import { fixedAssetsService, type UserOption } from '../../services/fixedAssetsService';
|
||||||
import { employeesService } from '@/modules/rh/components/employees/employees.services';
|
import { employeesService } from '@/modules/rh/components/employees/employees.services';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -24,20 +24,24 @@ const employeeSearch = ref('');
|
|||||||
|
|
||||||
const assets = ref<AssignmentAssetOption[]>([]);
|
const assets = ref<AssignmentAssetOption[]>([]);
|
||||||
const employees = ref<AssignmentEmployeeOption[]>([]);
|
const employees = ref<AssignmentEmployeeOption[]>([]);
|
||||||
|
const users = ref<UserOption[]>([]);
|
||||||
|
|
||||||
const form = ref<FixedAssetAssignmentFormData>({
|
const form = ref<FixedAssetAssignmentFormData>({
|
||||||
assetId: null,
|
assetId: null,
|
||||||
employeeId: null,
|
employeeId: null,
|
||||||
|
authorizedById: null,
|
||||||
assignedAt: new Date().toISOString().slice(0, 10),
|
assignedAt: new Date().toISOString().slice(0, 10),
|
||||||
|
receiptFolio: '',
|
||||||
notes: ''
|
notes: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadingData.value = true;
|
loadingData.value = true;
|
||||||
try {
|
try {
|
||||||
const [assetsRes, employeesRes] = await Promise.all([
|
const [assetsRes, employeesRes, usersRes] = await Promise.all([
|
||||||
fixedAssetsService.getAssets({ paginate: false, status: 1 }),
|
fixedAssetsService.getAssets({ paginate: false, status: 1 }),
|
||||||
employeesService.getEmployees({ paginate: false }),
|
employeesService.getEmployees({ paginate: false }),
|
||||||
|
fixedAssetsService.getUsers(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const allAssets = (assetsRes as any).data?.data ?? [];
|
const allAssets = (assetsRes as any).data?.data ?? [];
|
||||||
@ -59,6 +63,8 @@ onMounted(async () => {
|
|||||||
role: e.job_position?.name ?? '—',
|
role: e.job_position?.name ?? '—',
|
||||||
department: e.department?.name ?? '—',
|
department: e.department?.name ?? '—',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
users.value = usersRes;
|
||||||
} catch {
|
} catch {
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
@ -89,6 +95,8 @@ const save = async () => {
|
|||||||
await fixedAssetsService.assignAsset(form.value.assetId, {
|
await fixedAssetsService.assignAsset(form.value.assetId, {
|
||||||
employee_id: form.value.employeeId,
|
employee_id: form.value.employeeId,
|
||||||
assigned_at: form.value.assignedAt,
|
assigned_at: form.value.assignedAt,
|
||||||
|
receipt_folio: form.value.receiptFolio || undefined,
|
||||||
|
authorized_by: form.value.authorizedById ?? undefined,
|
||||||
notes: form.value.notes || undefined,
|
notes: form.value.notes || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,7 +147,7 @@ const save = async () => {
|
|||||||
@update:selected-employee-id="form.employeeId = $event"
|
@update:selected-employee-id="form.employeeId = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AssignmentDetailsCard :form="form" />
|
<AssignmentDetailsCard :form="form" :users="users" />
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-end gap-3">
|
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||||
<Button label="Cancelar" text severity="secondary" @click="cancel" />
|
<Button label="Cancelar" text severity="secondary" @click="cancel" />
|
||||||
|
|||||||
@ -77,6 +77,11 @@ const goToOffboarding = (assignment: AssetAssignment) => {
|
|||||||
router.push(`/fixed-assets/assignments/${assignment.asset_id}/${assignment.id}/offboarding`);
|
router.push(`/fixed-assets/assignments/${assignment.asset_id}/${assignment.id}/offboarding`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadResguardo = (assignment: AssetAssignment) => {
|
||||||
|
const url = fixedAssetsService.getResguardoUrl(assignment.asset_id, assignment.id);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(loadAssignments);
|
onMounted(loadAssignments);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -175,6 +180,15 @@ onMounted(loadAssignments);
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-4 py-3 text-right">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-file-pdf"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
v-tooltip.top="'Descargar resguardo'"
|
||||||
|
@click="downloadResguardo(assignment)"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="assignment.status.id === 1"
|
v-if="assignment.status.id === 1"
|
||||||
icon="pi pi-times-circle"
|
icon="pi pi-times-circle"
|
||||||
|
|||||||
@ -115,10 +115,22 @@ export interface AssetAssignmentsPaginatedResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserOption {
|
||||||
|
id: number;
|
||||||
|
full_name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsersResponse {
|
||||||
|
status: string;
|
||||||
|
data: { data: UserOption[] };
|
||||||
|
}
|
||||||
|
|
||||||
interface AssignAssetData {
|
interface AssignAssetData {
|
||||||
employee_id: number;
|
employee_id: number;
|
||||||
assigned_at?: string;
|
assigned_at?: string;
|
||||||
receipt_folio?: string;
|
receipt_folio?: string;
|
||||||
|
authorized_by?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,6 +196,16 @@ class FixedAssetsService {
|
|||||||
const response = await api.put<AssetAssignmentResponse>(`/api/assets/${assetId}/assignments/${assignmentId}/return`, data);
|
const response = await api.put<AssetAssignmentResponse>(`/api/assets/${assetId}/assignments/${assignmentId}/return`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUsers(): Promise<UserOption[]> {
|
||||||
|
const response = await api.get<UsersResponse>('/api/admin/users', { params: { paginate: false } });
|
||||||
|
return response.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getResguardoUrl(assetId: number, assignmentId: number): string {
|
||||||
|
const base = import.meta.env.VITE_API_URL || '';
|
||||||
|
return `${base}/api/asset-assignments-public/${assetId}/assignments/${assignmentId}/resguardo`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fixedAssetsService = new FixedAssetsService();
|
export const fixedAssetsService = new FixedAssetsService();
|
||||||
|
|||||||
@ -17,6 +17,8 @@ export interface AssignmentEmployeeOption {
|
|||||||
export interface FixedAssetAssignmentFormData {
|
export interface FixedAssetAssignmentFormData {
|
||||||
assetId: number | null;
|
assetId: number | null;
|
||||||
employeeId: number | null;
|
employeeId: number | null;
|
||||||
|
authorizedById: number | null;
|
||||||
assignedAt: string;
|
assignedAt: string;
|
||||||
|
receiptFolio: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,10 @@ import { useComercialClassificationStore } from '../../catalog/stores/comercialC
|
|||||||
import { useUnitOfMeasureStore } from '../../catalog/stores/unitOfMeasureStore';
|
import { useUnitOfMeasureStore } from '../../catalog/stores/unitOfMeasureStore';
|
||||||
import { satCodeProductsService, type SatCodeProduct } from '../../catalog/services/sat-code-products.services';
|
import { satCodeProductsService, type SatCodeProduct } from '../../catalog/services/sat-code-products.services';
|
||||||
import type { Product, CreateProductData } from '../types/product';
|
import type { Product, CreateProductData } from '../types/product';
|
||||||
|
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||||
|
|
||||||
|
// Auth/Permissions
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -32,6 +36,11 @@ const emit = defineEmits<{
|
|||||||
const clasificationsStore = useComercialClassificationStore();
|
const clasificationsStore = useComercialClassificationStore();
|
||||||
const unitOfMeasureStore = useUnitOfMeasureStore();
|
const unitOfMeasureStore = useUnitOfMeasureStore();
|
||||||
|
|
||||||
|
// Computed permission to store or update
|
||||||
|
const canSaveProduct = computed(() => {
|
||||||
|
return props.isEditing ? hasPermission('products.update') : hasPermission('products.store');
|
||||||
|
});
|
||||||
|
|
||||||
// Form data
|
// Form data
|
||||||
const formData = ref<CreateProductData>({
|
const formData = ref<CreateProductData>({
|
||||||
code: '',
|
code: '',
|
||||||
@ -700,11 +709,12 @@ const onUpload = (event: any) => {
|
|||||||
outlined
|
outlined
|
||||||
@click="handleCancel"
|
@click="handleCancel"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
:label="isEditing ? 'Actualizar Producto' : 'Guardar Producto'"
|
v-if="canSaveProduct"
|
||||||
:disabled="!isFormValid"
|
:label="isEditing ? 'Actualizar Producto' : 'Guardar Producto'"
|
||||||
@click="handleSubmit"
|
:disabled="!isFormValid"
|
||||||
/>
|
@click="handleSubmit"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
import Breadcrumb from 'primevue/breadcrumb';
|
import Breadcrumb from 'primevue/breadcrumb';
|
||||||
@ -38,6 +39,13 @@ const isEditing = ref(false);
|
|||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
const selectedProduct = ref<Product | null>(null);
|
const selectedProduct = ref<Product | null>(null);
|
||||||
|
|
||||||
|
// Auth/Permissions
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
const canCreateProduct = computed(() => hasPermission('products.store'));
|
||||||
|
const canUpdateProduct = computed(() => hasPermission('products.update'));
|
||||||
|
const canDeleteProduct = computed(() => hasPermission('products.destroy'));
|
||||||
|
const canViewProduct = computed(() => hasPermission('products.index'));
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const products = computed(() => productStore.products);
|
const products = computed(() => productStore.products);
|
||||||
const loading = computed(() => productStore.loading);
|
const loading = computed(() => productStore.loading);
|
||||||
@ -167,7 +175,12 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div>
|
||||||
|
<div v-if="!canViewProduct" class="flex flex-col items-center py-16">
|
||||||
|
<i class="pi pi-ban text-red-400 text-5xl mb-4"></i>
|
||||||
|
<p class="text-lg text-red-600 font-semibold">No tienes permisos para visualizar este módulo.</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-6">
|
||||||
<!-- Toast Notifications -->
|
<!-- Toast Notifications -->
|
||||||
<Toast position="bottom-right" />
|
<Toast position="bottom-right" />
|
||||||
|
|
||||||
@ -182,11 +195,12 @@ onMounted(() => {
|
|||||||
<h1 class="text-2xl font-bold leading-tight tracking-tight text-surface-900 dark:text-white">
|
<h1 class="text-2xl font-bold leading-tight tracking-tight text-surface-900 dark:text-white">
|
||||||
Catálogo de Productos
|
Catálogo de Productos
|
||||||
</h1>
|
</h1>
|
||||||
<Button
|
<Button
|
||||||
label="Nuevo Producto"
|
v-if="canCreateProduct"
|
||||||
icon="pi pi-plus"
|
label="Nuevo Producto"
|
||||||
@click="openCreateDialog"
|
icon="pi pi-plus"
|
||||||
/>
|
@click="openCreateDialog"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters & Search -->
|
<!-- Filters & Search -->
|
||||||
@ -296,23 +310,25 @@ onMounted(() => {
|
|||||||
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right" style="min-width: 120px">
|
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right" style="min-width: 120px">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<div class="flex items-center justify-end gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
icon="pi pi-pencil"
|
v-if="canUpdateProduct"
|
||||||
text
|
icon="pi pi-pencil"
|
||||||
rounded
|
text
|
||||||
size="small"
|
rounded
|
||||||
@click="editProduct(slotProps.data)"
|
size="small"
|
||||||
v-tooltip.top="'Editar'"
|
@click="editProduct(slotProps.data)"
|
||||||
/>
|
v-tooltip.top="'Editar'"
|
||||||
<Button
|
/>
|
||||||
icon="pi pi-trash"
|
<Button
|
||||||
text
|
v-if="canDeleteProduct"
|
||||||
rounded
|
icon="pi pi-trash"
|
||||||
size="small"
|
text
|
||||||
severity="danger"
|
rounded
|
||||||
@click="deleteProductConfirm(slotProps.data)"
|
size="small"
|
||||||
v-tooltip.top="'Eliminar'"
|
severity="danger"
|
||||||
/>
|
@click="deleteProductConfirm(slotProps.data)"
|
||||||
|
v-tooltip.top="'Eliminar'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
@ -363,6 +379,7 @@ onMounted(() => {
|
|||||||
@save="handleSaveProduct"
|
@save="handleSaveProduct"
|
||||||
@cancel="handleCancelForm"
|
@cancel="handleCancelForm"
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user