- 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.
395 lines
15 KiB
Vue
395 lines
15 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue';
|
|
|
|
import Card from 'primevue/card';
|
|
import Button from 'primevue/button';
|
|
import DataTable from 'primevue/datatable';
|
|
import Column from 'primevue/column';
|
|
import InputText from 'primevue/inputtext';
|
|
import Dropdown from 'primevue/dropdown';
|
|
import Tag from 'primevue/tag';
|
|
import Paginator from 'primevue/paginator';
|
|
import { useConfirm } from 'primevue/useconfirm';
|
|
import { useToast } from 'primevue/usetoast';
|
|
|
|
|
|
|
|
import { supplierServices } from '../../services/supplier.services';
|
|
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[]>([]);
|
|
const pagination = ref({
|
|
first: 0,
|
|
rows: 5,
|
|
total: 0,
|
|
page: 1,
|
|
lastPage: 1,
|
|
});
|
|
const loading = ref(false);
|
|
const loadingSupplier = ref(false);
|
|
|
|
// Modal state and form fields
|
|
const showModal = ref(false);
|
|
const isEditMode = ref(false);
|
|
const currentSupplier = ref<Supplier | null>(null);
|
|
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',
|
|
icon: 'pi pi-exclamation-triangle',
|
|
acceptLabel: 'Sí, eliminar',
|
|
rejectLabel: 'Cancelar',
|
|
accept: async () => {
|
|
try {
|
|
await supplierServices.deleteSupplier(supplierId);
|
|
toast.add({ severity: 'success', summary: 'Eliminado', detail: 'Proveedor eliminado correctamente', life: 3000 });
|
|
fetchSuppliers(pagination.value.page);
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo eliminar el proveedor', life: 3000 });
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
const mapTypeToApi = (typeLabel: string | null) => {
|
|
if (!typeLabel) return undefined;
|
|
switch (typeLabel) {
|
|
case 'Cliente': return SupplierType.CLIENT;
|
|
case 'Proveedor': return SupplierType.PROVIDER;
|
|
case 'Ambos': return SupplierType.BOTH;
|
|
default: return undefined;
|
|
}
|
|
};
|
|
|
|
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;
|
|
const response = await supplierServices.getSuppliers(true, name, type);
|
|
const paginated = response as SupplierPaginatedResponse;
|
|
suppliers.value = paginated.data;
|
|
pagination.value.total = paginated.total;
|
|
pagination.value.page = paginated.current_page;
|
|
pagination.value.lastPage = paginated.last_page;
|
|
pagination.value.first = (paginated.current_page - 1) * paginated.per_page;
|
|
pagination.value.rows = paginated.per_page;
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar los proveedores.', life: 3000 });
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => canViewSuppliers.value,
|
|
(allowed) => {
|
|
if (allowed) {
|
|
fetchSuppliers();
|
|
} else {
|
|
suppliers.value = [];
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
const supplierTypes = [
|
|
{ label: 'Todos', value: null },
|
|
{ label: 'Cliente', value: 'Cliente' },
|
|
{ label: 'Proveedor', value: 'Proveedor' },
|
|
{ label: 'Ambos', value: 'Ambos' },
|
|
];
|
|
|
|
const selectedType = ref(null);
|
|
const searchName = ref('');
|
|
|
|
|
|
const clearFilters = () => {
|
|
searchName.value = '';
|
|
selectedType.value = null;
|
|
fetchSuppliers(1);
|
|
};
|
|
|
|
const onFilter = () => {
|
|
fetchSuppliers(1);
|
|
};
|
|
|
|
|
|
const onPageChange = (event: any) => {
|
|
const newPage = Math.floor(event.first / event.rows) + 1;
|
|
fetchSuppliers(newPage);
|
|
};
|
|
|
|
|
|
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;
|
|
formErrors.value = {};
|
|
};
|
|
|
|
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;
|
|
|
|
try {
|
|
// Obtener la información completa del proveedor incluyendo direcciones
|
|
const response = await supplierServices.getSupplierById(supplier.id);
|
|
currentSupplier.value = response.data;
|
|
showModal.value = true;
|
|
} catch (e: any) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: 'No se pudo cargar la información del proveedor',
|
|
life: 3000
|
|
});
|
|
console.error('Error loading supplier details:', e);
|
|
} finally {
|
|
loadingSupplier.value = false;
|
|
}
|
|
};
|
|
|
|
const closeModal = () => {
|
|
showModal.value = false;
|
|
isEditMode.value = false;
|
|
currentSupplier.value = null;
|
|
formErrors.value = {};
|
|
};
|
|
|
|
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 });
|
|
}
|
|
closeModal();
|
|
fetchSuppliers(pagination.value.page);
|
|
} catch (e: any) {
|
|
if (e?.response?.data?.errors) {
|
|
formErrors.value = e.response.data.errors;
|
|
}
|
|
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,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Mapeo para mostrar el tipo de proveedor con label legible
|
|
const typeLabel = (type: number) => {
|
|
switch (type) {
|
|
case SupplierType.CLIENT: return 'Cliente';
|
|
case SupplierType.PROVIDER: return 'Proveedor';
|
|
case SupplierType.BOTH: return 'Ambos';
|
|
default: return 'Desconocido';
|
|
}
|
|
};
|
|
|
|
const typeSeverity = (type: number) => {
|
|
switch (type) {
|
|
case SupplierType.CLIENT: return 'info';
|
|
case SupplierType.PROVIDER: return 'success';
|
|
case SupplierType.BOTH: return 'warning';
|
|
default: return 'secondary';
|
|
}
|
|
};
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-6">
|
|
<!-- Heading -->
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
|
<div>
|
|
<h2 class="text-3xl font-black text-surface-900 dark:text-white tracking-tight">Gestión de Proveedores
|
|
</h2>
|
|
<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
|
|
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 v-if="canViewSuppliers">
|
|
<template #content>
|
|
<div class="flex flex-wrap gap-4 items-end">
|
|
<div class="flex-1 min-w-60">
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">Buscar
|
|
Nombre</label>
|
|
<InputText v-model="searchName" placeholder="Ej. TechLogistics S.A." class="w-full" />
|
|
</div>
|
|
<div class="w-48">
|
|
<label
|
|
class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">Tipo</label>
|
|
<Dropdown v-model="selectedType" :options="supplierTypes" optionLabel="label"
|
|
optionValue="value" placeholder="Todos" class="w-full" @change="onFilter" />
|
|
</div>
|
|
<Button icon="pi pi-search" text rounded @click="onFilter" />
|
|
<Button icon="pi pi-times" text rounded severity="secondary" label="Limpiar" @click="clearFilters" />
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Table Content -->
|
|
<Card v-if="canViewSuppliers">
|
|
<template #content>
|
|
<DataTable :value="suppliers" :loading="loading" stripedRows responsiveLayout="scroll"
|
|
class="p-datatable-sm">
|
|
<Column field="name" header="Razón Social" style="min-width: 180px">
|
|
<template #body="{ data }">
|
|
<div>
|
|
<div class="font-bold text-surface-900 dark:text-white">{{ data.name }}</div>
|
|
<div class="text-xs text-gray-500">{{ data.comercial_name }}</div>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
<Column field="rfc" header="RFC" style="min-width: 130px">
|
|
<template #body="{ data }">
|
|
<span class="font-mono text-sm">{{ data.rfc }}</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="curp" header="CURP" style="min-width: 180px">
|
|
<template #body="{ data }">
|
|
<span class="font-mono text-sm">{{ data.curp }}</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="type" header="Tipo" style="min-width: 100px">
|
|
<template #body="{ data }">
|
|
<Tag :value="typeLabel(data.type)" :severity="typeSeverity(data.type)" />
|
|
</template>
|
|
</Column>
|
|
<Column field="credit_limit" header="Límite de Crédito" style="min-width: 140px">
|
|
<template #body="{ data }">
|
|
<span class="font-semibold text-green-600 dark:text-green-400">${{ parseFloat(data.credit_limit).toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="payment_days" header="Días de Pago" style="min-width: 120px">
|
|
<template #body="{ data }">
|
|
<span class="font-semibold">{{ data.payment_days }} días</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="created_at" header="Fecha de Registro" style="min-width: 120px">
|
|
<template #body="{ data }">
|
|
<span class="text-sm">{{ new Date(data.created_at).toLocaleDateString('es-MX') }}</span>
|
|
</template>
|
|
</Column>
|
|
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right"
|
|
style="min-width: 120px">
|
|
<template #body="{ data }">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<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>
|
|
</DataTable>
|
|
<div class="mt-4">
|
|
<Paginator :first="pagination.first" :rows="pagination.rows" :totalRecords="pagination.total"
|
|
:rowsPerPageOptions="[5, 10, 20, 50]" @page="onPageChange" />
|
|
</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 />
|
|
<Toast />
|
|
</template>
|