Refactor supplier and unit of measure components and services

- Updated SupplierModal.vue to include new fields for supplier information and improved form validation.
- Enhanced Suppliers.vue to handle loading states and improved supplier data fetching logic.
- Removed old supplierServices and unitOfMeasureService files, replacing them with updated service files that align with new interfaces.
- Created new interfaces for suppliers and unit of measure to standardize data handling across the application.
- Adjusted the store files to reference the new service files and interfaces.
- Improved error handling and logging in service methods for better debugging.
This commit is contained in:
Edgar Méndez Mendoza 2026-02-24 09:08:44 -06:00
parent df0b707064
commit 522235d441
11 changed files with 303 additions and 500 deletions

View File

@ -18,7 +18,7 @@ import ConfirmDialog from 'primevue/confirmdialog';
import ProgressSpinner from 'primevue/progressspinner';
import { useUnitOfMeasureStore } from '../stores/unitOfMeasureStore';
import { unitTypesService } from '../services/unitsTypes';
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../types/unitOfMeasure';
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../types/unit-measure.interfaces';
const router = useRouter();
const toast = useToast();

View File

@ -1,378 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>System Document Types Management</title>
<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;800;900&amp;display=swap"
rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap"
rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap"
rel="stylesheet" />
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"primary": "#195de6",
"background-light": "#f6f6f8",
"background-dark": "#111621",
"neutral-dark": "#1a202c",
"neutral-sidebar": "#0f172a",
},
fontFamily: {
"display": ["Inter", "sans-serif"]
},
borderRadius: {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body {
font-family: 'Inter', sans-serif;
}
</style>
</head>
<body class="bg-background-light dark:bg-background-dark font-display text-slate-900 overflow-hidden">
<div class="flex h-screen w-full">
<!-- Sidebar -->
<aside class="w-64 bg-neutral-sidebar flex flex-col h-full border-r border-slate-800 text-slate-300">
<div class="p-6 flex items-center gap-3 border-b border-slate-800">
<div class="bg-primary p-1.5 rounded-lg">
<span class="material-symbols-outlined text-white">inventory_2</span>
</div>
<h2 class="text-white text-lg font-bold tracking-tight">LogisPro</h2>
</div>
<nav class="flex-1 px-4 py-6 space-y-2 overflow-y-auto">
<div class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 px-2">Navegación</div>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 transition-colors" href="#">
<span class="material-symbols-outlined text-xl">dashboard</span>
<span class="text-sm font-medium">Dashboard</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 transition-colors" href="#">
<span class="material-symbols-outlined text-xl">warehouse</span>
<span class="text-sm font-medium">Almacén</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 transition-colors" href="#">
<span class="material-symbols-outlined text-xl">factory</span>
<span class="text-sm font-medium">Producción</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 transition-colors" href="#">
<span class="material-symbols-outlined text-xl">analytics</span>
<span class="text-sm font-medium">Reportes</span>
</a>
<div class="pt-6 text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 px-2">Sistema</div>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg bg-primary text-white transition-colors"
href="#">
<span class="material-symbols-outlined text-xl">settings</span>
<span class="text-sm font-medium">Configuración</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 transition-colors" href="#">
<span class="material-symbols-outlined text-xl">group</span>
<span class="text-sm font-medium">Usuarios</span>
</a>
</nav>
<div class="p-4 border-t border-slate-800">
<div class="flex items-center gap-3 px-3 py-2">
<div class="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-primary font-bold text-xs"
data-alt="User avatar placeholder">AD</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-white truncate">Admin Warehouse</p>
<p class="text-xs text-slate-500 truncate">admin@logispro.com</p>
</div>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-w-0 bg-background-light overflow-hidden">
<!-- Top Header -->
<header class="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-8 shrink-0">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-slate-400">menu</span>
<h1 class="text-xl font-semibold text-slate-800">Administración de Documentos</h1>
</div>
<div class="flex items-center gap-4">
<button class="p-2 text-slate-500 hover:bg-slate-100 rounded-full relative">
<span class="material-symbols-outlined">notifications</span>
<span
class="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white"></span>
</button>
<button class="p-2 text-slate-500 hover:bg-slate-100 rounded-full">
<span class="material-symbols-outlined">help_outline</span>
</button>
</div>
</header>
<!-- Scrollable Body -->
<div class="flex-1 overflow-y-auto p-8 space-y-6">
<!-- Breadcrumbs & Action -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<nav class="flex text-xs font-semibold uppercase tracking-wider text-slate-500 space-x-2">
<a class="hover:text-primary transition-colors" href="#">Inicio</a>
<span>/</span>
<a class="hover:text-primary transition-colors" href="#">Configuración</a>
<span>/</span>
<span class="text-primary/70">Tipos de Documento</span>
</nav>
<h2 class="text-3xl font-black text-slate-900 mt-1 tracking-tight">Tipos de Documento</h2>
<p class="text-slate-500 text-sm mt-1">Gestione los formatos y folios permitidos para los
módulos de almacén y producción.</p>
</div>
<button
class="bg-primary text-white px-6 py-2.5 rounded-lg font-bold text-sm shadow-lg shadow-primary/20 hover:bg-primary/90 transition-all flex items-center gap-2 shrink-0 self-start md:self-center">
<span class="material-symbols-outlined text-lg">add</span>
Agregar Nuevo Documento
</button>
</div>
<!-- Filters -->
<div class="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex flex-wrap gap-4 items-end">
<div class="flex-1 min-w-[240px]">
<label class="block text-xs font-bold text-slate-700 uppercase mb-1.5 ml-1">Buscar por
nombre</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-2.5 text-slate-400">search</span>
<input
class="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm"
placeholder="Ej. Cotización, Factura..." type="text" />
</div>
</div>
<div class="w-full md:w-64">
<label class="block text-xs font-bold text-slate-700 uppercase mb-1.5 ml-1">Módulo</label>
<select
class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm appearance-none bg-white">
<option>Todos los módulos</option>
<option>Almacén</option>
<option>Producción</option>
<option>Ventas</option>
</select>
</div>
<button
class="px-4 py-2 text-primary font-bold text-sm hover:bg-primary/5 rounded-lg transition-colors flex items-center gap-1.5">
<span class="material-symbols-outlined text-lg">filter_alt</span>
Más Filtros
</button>
<button
class="px-4 py-2 text-slate-500 font-medium text-sm hover:bg-slate-100 rounded-lg transition-colors">
Limpiar
</button>
</div>
<!-- Table Card -->
<div class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="px-6 py-4 text-xs font-bold text-slate-600 uppercase tracking-wider">ID
</th>
<th class="px-6 py-4 text-xs font-bold text-slate-600 uppercase tracking-wider">
Nombre</th>
<th class="px-6 py-4 text-xs font-bold text-slate-600 uppercase tracking-wider">
Módulo ID</th>
<th class="px-6 py-4 text-xs font-bold text-slate-600 uppercase tracking-wider">Tipo
ID</th>
<th class="px-6 py-4 text-xs font-bold text-slate-600 uppercase tracking-wider">
Folio Actual</th>
<th class="px-6 py-4 text-xs font-bold text-slate-600 uppercase tracking-wider">
Fecha Creación</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-600 uppercase tracking-wider text-right">
Acciones</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
<tr class="hover:bg-primary/5 transition-colors group">
<td class="px-6 py-4 text-sm font-semibold text-slate-400">#1024</td>
<td class="px-6 py-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span class="text-sm font-semibold text-slate-700">Cotización</span>
</div>
</td>
<td class="px-6 py-4">
<span
class="px-2.5 py-1 rounded-full bg-slate-100 text-slate-600 text-[10px] font-bold uppercase">ALM-01</span>
</td>
<td class="px-6 py-4 text-sm text-slate-500">CT-DOC</td>
<td class="px-6 py-4 font-mono text-sm text-slate-600">000542</td>
<td class="px-6 py-4 text-sm text-slate-500">12/10/2023</td>
<td class="px-6 py-4 text-right">
<div class="flex justify-end gap-2">
<button
class="p-1.5 text-slate-400 hover:text-primary transition-colors hover:bg-white rounded-md shadow-none group-hover:shadow-sm">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button
class="p-1.5 text-slate-400 hover:text-red-500 transition-colors hover:bg-white rounded-md shadow-none group-hover:shadow-sm">
<span class="material-symbols-outlined text-lg">archive</span>
</button>
</div>
</td>
</tr>
<tr class="hover:bg-primary/5 transition-colors group">
<td class="px-6 py-4 text-sm font-semibold text-slate-400">#1025</td>
<td class="px-6 py-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span class="text-sm font-semibold text-slate-700">Orden de Compra</span>
</div>
</td>
<td class="px-6 py-4">
<span
class="px-2.5 py-1 rounded-full bg-slate-100 text-slate-600 text-[10px] font-bold uppercase">PRO-05</span>
</td>
<td class="px-6 py-4 text-sm text-slate-500">OC-PRO</td>
<td class="px-6 py-4 font-mono text-sm text-slate-600">000128</td>
<td class="px-6 py-4 text-sm text-slate-500">15/10/2023</td>
<td class="px-6 py-4 text-right">
<div class="flex justify-end gap-2">
<button
class="p-1.5 text-slate-400 hover:text-primary transition-colors hover:bg-white rounded-md shadow-none group-hover:shadow-sm">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button
class="p-1.5 text-slate-400 hover:text-red-500 transition-colors hover:bg-white rounded-md shadow-none group-hover:shadow-sm">
<span class="material-symbols-outlined text-lg">archive</span>
</button>
</div>
</td>
</tr>
<tr class="hover:bg-primary/5 transition-colors group">
<td class="px-6 py-4 text-sm font-semibold text-slate-400">#1026</td>
<td class="px-6 py-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span class="text-sm font-semibold text-slate-700">Vale de Salida</span>
</div>
</td>
<td class="px-6 py-4">
<span
class="px-2.5 py-1 rounded-full bg-slate-100 text-slate-600 text-[10px] font-bold uppercase">ALM-01</span>
</td>
<td class="px-6 py-4 text-sm text-slate-500">VS-INV</td>
<td class="px-6 py-4 font-mono text-sm text-slate-600">000891</td>
<td class="px-6 py-4 text-sm text-slate-500">18/10/2023</td>
<td class="px-6 py-4 text-right">
<div class="flex justify-end gap-2">
<button
class="p-1.5 text-slate-400 hover:text-primary transition-colors hover:bg-white rounded-md shadow-none group-hover:shadow-sm">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button
class="p-1.5 text-slate-400 hover:text-red-500 transition-colors hover:bg-white rounded-md shadow-none group-hover:shadow-sm">
<span class="material-symbols-outlined text-lg">archive</span>
</button>
</div>
</td>
</tr>
<tr class="hover:bg-primary/5 transition-colors group">
<td class="px-6 py-4 text-sm font-semibold text-slate-400">#1027</td>
<td class="px-6 py-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-amber-400"></span>
<span class="text-sm font-semibold text-slate-700">Ticket de
Mantenimiento</span>
</div>
</td>
<td class="px-6 py-4">
<span
class="px-2.5 py-1 rounded-full bg-slate-100 text-slate-600 text-[10px] font-bold uppercase">PRO-02</span>
</td>
<td class="px-6 py-4 text-sm text-slate-500">TM-MAI</td>
<td class="px-6 py-4 font-mono text-sm text-slate-600">000042</td>
<td class="px-6 py-4 text-sm text-slate-500">20/10/2023</td>
<td class="px-6 py-4 text-right">
<div class="flex justify-end gap-2">
<button
class="p-1.5 text-slate-400 hover:text-primary transition-colors hover:bg-white rounded-md shadow-none group-hover:shadow-sm">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button
class="p-1.5 text-slate-400 hover:text-red-500 transition-colors hover:bg-white rounded-md shadow-none group-hover:shadow-sm">
<span class="material-symbols-outlined text-lg">archive</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="px-6 py-4 bg-slate-50 border-t border-slate-200 flex items-center justify-between">
<p class="text-sm text-slate-500">Mostrando <span class="font-bold text-slate-700">1-4</span> de
<span class="font-bold text-slate-700">24</span> tipos de documento</p>
<div class="flex items-center gap-1">
<button
class="p-2 rounded hover:bg-slate-200 text-slate-500 transition-colors disabled:opacity-30"
disabled="">
<span class="material-symbols-outlined text-lg">chevron_left</span>
</button>
<button class="w-8 h-8 rounded bg-primary text-white text-sm font-bold shadow-sm">1</button>
<button
class="w-8 h-8 rounded hover:bg-slate-200 text-slate-600 text-sm font-medium transition-colors">2</button>
<button
class="w-8 h-8 rounded hover:bg-slate-200 text-slate-600 text-sm font-medium transition-colors">3</button>
<span class="px-1 text-slate-400">...</span>
<button
class="w-8 h-8 rounded hover:bg-slate-200 text-slate-600 text-sm font-medium transition-colors">6</button>
<button class="p-2 rounded hover:bg-slate-200 text-slate-500 transition-colors">
<span class="material-symbols-outlined text-lg">chevron_right</span>
</button>
</div>
</div>
</div>
<!-- Footer Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white p-6 rounded-xl border border-slate-200 flex items-center gap-4">
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center text-primary">
<span class="material-symbols-outlined">description</span>
</div>
<div>
<p class="text-xs font-bold text-slate-500 uppercase">Total Activos</p>
<p class="text-2xl font-black text-slate-900">24</p>
</div>
</div>
<div class="bg-white p-6 rounded-xl border border-slate-200 flex items-center gap-4">
<div
class="w-12 h-12 bg-amber-100 rounded-full flex items-center justify-center text-amber-600">
<span class="material-symbols-outlined">update</span>
</div>
<div>
<p class="text-xs font-bold text-slate-500 uppercase">Último Folio Emitido</p>
<p class="text-2xl font-black text-slate-900">ALM-542</p>
</div>
</div>
<div class="bg-white p-6 rounded-xl border border-slate-200 flex items-center gap-4">
<div
class="w-12 h-12 bg-indigo-100 rounded-full flex items-center justify-center text-indigo-600">
<span class="material-symbols-outlined">archive</span>
</div>
<div>
<p class="text-xs font-bold text-slate-500 uppercase">Archivados</p>
<p class="text-2xl font-black text-slate-900">12</p>
</div>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

View File

@ -1,17 +1,20 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ref, watch, computed } from 'vue';
import Dialog from 'primevue/dialog';
import InputText from 'primevue/inputtext';
import InputNumber from 'primevue/inputnumber';
import Dropdown from 'primevue/dropdown';
import Textarea from 'primevue/textarea';
import Button from 'primevue/button';
import type { Supplier, SupplierFormErrors } from '../../types/suppliers';
import ProgressSpinner from 'primevue/progressspinner';
import type { Supplier, SupplierFormErrors, SupplierAddress } from '../../types/suppliers.interfaces';
import { SupplierType } from '../../types/suppliers.interfaces';
const props = defineProps<{
visible: boolean;
isEditMode: boolean;
supplier?: Supplier | null;
formErrors: SupplierFormErrors;
loading?: boolean;
}>();
const emit = defineEmits<{
@ -20,36 +23,66 @@ const emit = defineEmits<{
(e: 'cancel'): void;
}>();
const defaultAddress = (): SupplierAddress => ({
country: 'México',
postal_code: '',
state: '',
municipality: '',
city: '',
street: '',
num_ext: '',
num_int: ''
});
const form = ref({
name: '',
email: '',
phone: '',
type: '',
address: ''
comercial_name: '',
rfc: '',
curp: '',
type: SupplierType.PROVIDER as number,
credit_limit: 0,
payment_days: 30,
addresses: [defaultAddress()]
});
const address = computed(() => form.value.addresses[0] || defaultAddress());
watch(
() => props.supplier,
(supplier) => {
if (props.isEditMode && supplier) {
form.value = {
name: supplier.name,
email: supplier.contact_email,
phone: supplier.phone_number,
comercial_name: supplier.comercial_name,
rfc: supplier.rfc,
curp: supplier.curp,
type: supplier.type,
address: supplier.address
credit_limit: parseFloat(supplier.credit_limit),
payment_days: supplier.payment_days,
addresses: supplier.addresses && supplier.addresses.length > 0
? [...supplier.addresses]
: [defaultAddress()]
};
} else {
form.value = { name: '', email: '', phone: '', type: '', address: '' };
form.value = {
name: '',
comercial_name: '',
rfc: '',
curp: '',
type: SupplierType.PROVIDER,
credit_limit: 0,
payment_days: 30,
addresses: [defaultAddress()]
};
}
},
{ immediate: true }
);
const supplierTypeOptions = [
{ label: 'General', value: 'general' },
{ label: 'Compra', value: 'purchases' },
{ label: 'Venta', value: 'sales' }
{ label: 'Cliente', value: SupplierType.CLIENT },
{ label: 'Proveedor', value: SupplierType.PROVIDER },
{ label: 'Cliente/Proveedor', value: SupplierType.BOTH }
];
const handleSubmit = () => {
@ -62,42 +95,125 @@ const handleCancel = () => {
</script>
<template>
<Dialog :visible="visible" modal :style="{ width: '600px' }" :header="isEditMode ? 'Editar Proveedor' : 'Crear Nuevo Proveedor'" :closable="true" @update:visible="val => emit('update:visible', val)">
<form class="flex flex-col gap-8" @submit.prevent="handleSubmit">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<label class="flex flex-col gap-2">
<p class="text-[#111418] dark:text-white text-sm font-semibold leading-normal">
Nombre del Proveedor <span class="text-red-500">*</span></p>
<InputText v-model="form.name" placeholder="Ej: Suministros Industriales S.A." required class="w-full" />
<div v-if="formErrors.name" class="text-red-500 text-xs mt-1" v-for="err in formErrors.name" :key="err">{{ err }}</div>
</label>
<label class="flex flex-col gap-2">
<p class="text-[#111418] dark:text-white text-sm font-semibold leading-normal">
Correo de Contacto <span class="text-red-500">*</span></p>
<InputText v-model="form.email" placeholder="contacto@proveedor.com" required class="w-full" type="email" />
<div v-if="formErrors.contact_email" class="text-red-500 text-xs mt-1" v-for="err in formErrors.contact_email" :key="err">{{ err }}</div>
</label>
<Dialog :visible="visible" modal :style="{ width: '800px' }" :header="isEditMode ? 'Editar Proveedor' : 'Crear Nuevo Proveedor'" :closable="true" @update:visible="val => emit('update:visible', val)">
<!-- Loading Overlay -->
<div v-if="loading" class="flex items-center justify-center py-20">
<ProgressSpinner style="width: 50px; height: 50px" strokeWidth="4" />
</div>
<form v-else class="flex flex-col gap-6" @submit.prevent="handleSubmit">
<!-- Información General -->
<div class="border-b pb-4">
<h3 class="text-lg font-semibold mb-4">Información General</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Razón Social <span class="text-red-500">*</span></p>
<InputText v-model="form.name" placeholder="Ej: Distribuidora Nacional S.A. de C.V." required />
<div v-if="formErrors.name" class="text-red-500 text-xs" v-for="err in formErrors.name" :key="err">{{ err }}</div>
</label>
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Nombre Comercial <span class="text-red-500">*</span></p>
<InputText v-model="form.comercial_name" placeholder="Ej: DistriNacional" required />
<div v-if="formErrors.comercial_name" class="text-red-500 text-xs" v-for="err in formErrors.comercial_name" :key="err">{{ err }}</div>
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">RFC <span class="text-red-500">*</span></p>
<InputText v-model="form.rfc" placeholder="XAXX010101000" required maxlength="13" class="font-mono uppercase" @input="form.rfc = form.rfc.toUpperCase()" />
<div v-if="formErrors.rfc" class="text-red-500 text-xs" v-for="err in formErrors.rfc" :key="err">{{ err }}</div>
</label>
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">CURP <span class="text-red-500">*</span></p>
<InputText v-model="form.curp" placeholder="XAXX010101HDFABC01" required maxlength="18" class="font-mono uppercase" @input="form.curp = form.curp.toUpperCase()" />
<div v-if="formErrors.curp" class="text-red-500 text-xs" v-for="err in formErrors.curp" :key="err">{{ err }}</div>
</label>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<label class="flex flex-col gap-2">
<p class="text-[#111418] dark:text-white text-sm font-semibold leading-normal">Teléfono</p>
<InputText v-model="form.phone" placeholder="+52 ..." class="w-full" type="tel" />
<div v-if="formErrors.phone_number" class="text-red-500 text-xs mt-1" v-for="err in formErrors.phone_number" :key="err">{{ err }}</div>
</label>
<label class="flex flex-col gap-2">
<p class="text-[#111418] dark:text-white text-sm font-semibold leading-normal">Tipo de Proveedor <span class="text-red-500">*</span></p>
<Dropdown v-model="form.type" :options="supplierTypeOptions" optionLabel="label" optionValue="value" placeholder="Seleccionar tipo" class="w-full" required />
<div v-if="formErrors.type" class="text-red-500 text-xs mt-1" v-for="err in formErrors.type" :key="err">{{ err }}</div>
</label>
<!-- Información Comercial -->
<div class="border-b pb-4">
<h3 class="text-lg font-semibold mb-4">Información Comercial</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Tipo <span class="text-red-500">*</span></p>
<Dropdown v-model="form.type" :options="supplierTypeOptions" optionLabel="label" optionValue="value" placeholder="Seleccionar tipo" required />
<div v-if="formErrors.type" class="text-red-500 text-xs" v-for="err in formErrors.type" :key="err">{{ err }}</div>
</label>
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Límite de Crédito <span class="text-red-500">*</span></p>
<InputNumber v-model="form.credit_limit" mode="currency" currency="MXN" locale="es-MX" :minFractionDigits="2" required />
<div v-if="formErrors.credit_limit" class="text-red-500 text-xs" v-for="err in formErrors.credit_limit" :key="err">{{ err }}</div>
</label>
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Días de Pago <span class="text-red-500">*</span></p>
<InputNumber v-model="form.payment_days" :min="0" :max="365" suffix=" días" required />
<div v-if="formErrors.payment_days" class="text-red-500 text-xs" v-for="err in formErrors.payment_days" :key="err">{{ err }}</div>
</label>
</div>
</div>
<label class="flex flex-col gap-2">
<p class="text-[#111418] dark:text-white text-sm font-semibold leading-normal">Dirección</p>
<Textarea v-model="form.address" placeholder="Calle, Número, Colonia, Ciudad, Estado, CP" class="w-full" autoResize />
<div v-if="formErrors.address" class="text-red-500 text-xs mt-1" v-for="err in formErrors.address" :key="err">{{ err }}</div>
</label>
<div class="mt-4 pt-8 border-t border-[#dbe0e6] dark:border-[#2d3a4a] flex flex-col sm:flex-row items-center justify-end gap-4">
<Button label="Cancelar" text class="w-full sm:w-auto" @click="handleCancel" type="button" />
<Button :label="isEditMode ? 'Actualizar Proveedor' : 'Guardar Proveedor'" type="submit" class="w-full sm:w-auto" />
<!-- Dirección -->
<div class="border-b pb-4">
<h3 class="text-lg font-semibold mb-4">Dirección</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">País</p>
<InputText v-model="address.country" placeholder="México" />
</label>
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Código Postal <span class="text-red-500">*</span></p>
<InputText v-model="address.postal_code" placeholder="06000" required maxlength="5" />
</label>
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Estado <span class="text-red-500">*</span></p>
<InputText v-model="address.state" placeholder="CDMX" required />
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Municipio/Alcaldía <span class="text-red-500">*</span></p>
<InputText v-model="address.municipality" placeholder="Cuauhtémoc" required />
</label>
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Ciudad <span class="text-red-500">*</span></p>
<InputText v-model="address.city" placeholder="Ciudad de México" required />
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
<label class="flex flex-col gap-2 md:col-span-1">
<p class="text-sm font-semibold">Calle <span class="text-red-500">*</span></p>
<InputText v-model="address.street" placeholder="Av. Reforma" required />
</label>
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Número Ext. <span class="text-red-500">*</span></p>
<InputText v-model="address.num_ext" placeholder="123" required />
</label>
<label class="flex flex-col gap-2">
<p class="text-sm font-semibold">Número Int.</p>
<InputText v-model="address.num_int" placeholder="4B" />
</label>
</div>
</div>
<!-- Botones -->
<div class="flex flex-col sm:flex-row items-center justify-end gap-3 pt-2">
<Button label="Cancelar" text severity="secondary" @click="handleCancel" type="button" />
<Button :label="isEditMode ? 'Actualizar Proveedor' : 'Crear Proveedor'" type="submit" icon="pi pi-check" />
</div>
</form>
</Dialog>

View File

@ -15,8 +15,9 @@ import { useToast } from 'primevue/usetoast';
import { supplierServices } from '../../services/supplierServices';
import type { Supplier, SupplierPaginatedResponse, SupplierFormErrors } from '../../types/suppliers';
import { supplierServices } from '../../services/supplier.services';
import type { Supplier, SupplierPaginatedResponse, SupplierFormErrors } from '../../types/suppliers.interfaces';
import { SupplierType } from '../../types/suppliers.interfaces';
import SupplierModal from './SupplierModal.vue';
@ -29,6 +30,7 @@ const pagination = ref({
lastPage: 1,
});
const loading = ref(false);
const loadingSupplier = ref(false);
// Modal state and form fields
const showModal = ref(false);
@ -58,11 +60,12 @@ const handleDelete = (supplierId: number) => {
};
const mapTypeToApi = (type: string) => {
switch (type) {
case 'General': return 'general';
case 'Compra': return 'purchases';
case 'Venta': return 'sales';
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;
}
};
@ -71,17 +74,11 @@ const fetchSuppliers = async (page = 1) => {
loading.value = true;
try {
const name = searchName.value ? searchName.value : undefined;
const type = selectedType.value ? mapTypeToApi(selectedType.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.map(s => ({
...s,
email: s.contact_email,
phone: s.phone_number,
typeColor: s.type === 'general' ? 'info' : s.type === 'purchases' ? 'success' : 'warning',
date: s.created_at ? new Date(s.created_at).toLocaleDateString() : ''
}));
suppliers.value = paginated.data;
pagination.value.total = paginated.total;
pagination.value.page = paginated.current_page;
pagination.value.lastPage = paginated.last_page;
@ -100,9 +97,9 @@ onMounted(() => {
const supplierTypes = [
{ label: 'Todos', value: null },
{ label: 'General', value: 'General' },
{ label: 'Compras', value: 'Compra' },
{ label: 'Ventas', value: 'Venta' },
{ label: 'Cliente', value: 'Cliente' },
{ label: 'Proveedor', value: 'Proveedor' },
{ label: 'Ambos', value: 'Ambos' },
];
const selectedType = ref(null);
@ -133,11 +130,27 @@ const openCreateModal = () => {
formErrors.value = {};
};
const openEditModal = (supplier: Supplier) => {
const openEditModal = async (supplier: Supplier) => {
isEditMode.value = true;
showModal.value = true;
currentSupplier.value = supplier;
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 = () => {
@ -151,24 +164,10 @@ const handleModalSubmit = async (form: any) => {
formErrors.value = {};
try {
if (isEditMode.value && currentSupplier.value) {
const payload = {
name: form.name,
contact_email: form.email,
phone_number: form.phone,
address: form.address,
type: form.type,
};
await supplierServices.updateSupplier(currentSupplier.value.id, payload, 'patch');
await supplierServices.updateSupplier(currentSupplier.value.id, form, 'patch');
toast.add({ severity: 'success', summary: 'Proveedor actualizado', detail: 'Proveedor actualizado correctamente', life: 3000 });
} else {
const payload = {
name: form.name,
contact_email: form.email,
phone_number: form.phone,
address: form.address,
type: form.type,
};
await supplierServices.createSupplier(payload);
await supplierServices.createSupplier(form);
toast.add({ severity: 'success', summary: 'Proveedor creado', detail: 'Proveedor registrado correctamente', life: 3000 });
}
closeModal();
@ -182,14 +181,24 @@ const handleModalSubmit = async (form: any) => {
};
// Mapeo para mostrar el tipo de proveedor con label legible
const typeLabel = (type: string) => {
const typeLabel = (type: number) => {
switch (type) {
case 'general': return 'General';
case 'purchases': return 'Compras';
case 'sales': return 'Ventas';
default: return 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>
@ -204,7 +213,7 @@ const typeLabel = (type: string) => {
</div>
<Button label="Nuevo Proveedor" icon="pi pi-plus" @click="openCreateModal" />
<SupplierModal :visible="showModal" :isEditMode="isEditMode" :supplier="currentSupplier"
:formErrors="formErrors" @update:visible="val => { if (!val) closeModal(); }"
:formErrors="formErrors" :loading="loadingSupplier" @update:visible="val => { if (!val) closeModal(); }"
@submit="handleModalSubmit" @cancel="closeModal" />
</div>
@ -233,27 +242,45 @@ const typeLabel = (type: string) => {
<Card>
<template #content>
<DataTable :value="suppliers" :loading="loading" stripedRows responsiveLayout="scroll"
class="p-datatable-sm">
<Column field="id" header="ID" style="min-width: 80px" />
<Column field="name" header="Nombre" style="min-width: 200px">
class="p-datatable-sm"><
<Column field="name" header="Razón Social" style="min-width: 180px">
<template #body="{ data }">
<span class="font-bold text-surface-900 dark:text-white">{{ data.name }}</span>
<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="email" header="Correo de Contacto" style="min-width: 200px">
<Column field="rfc" header="RFC" style="min-width: 130px">
<template #body="{ data }">
<span class="text-primary-600 dark:text-primary-400">{{ data.email }}</span>
<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="phone" header="Teléfono" style="min-width: 140px" />
<Column field="address" header="Dirección" style="min-width: 200px" />
<Column field="type" header="Tipo" style="min-width: 100px">
<template #body="{ data }">
<Tag :value="typeLabel(data.type)" :severity="data.typeColor" />
<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 field="date" header="Fecha de Registro" style="min-width: 120px" />
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right"
style="min-width: 120px">
<template #body="{ data }">

View File

@ -1,5 +1,5 @@
import api from "../../../services/api";
import type { SupplierCreateRequest, SupplierCreateResponse, SupplierDeleteResponse, SupplierListResponse, SupplierPaginatedResponse, SupplierUpdateRequest, SupplierUpdateResponse } from "../types/suppliers";
import type { SupplierCreateRequest, SupplierCreateResponse, SupplierDeleteResponse, SupplierListResponse, SupplierPaginatedResponse, SupplierUpdateRequest, SupplierUpdateResponse } from "../types/suppliers.interfaces";
const supplierServices = {
@ -26,7 +26,18 @@ const supplierServices = {
throw error;
}
},
async getSupplierById(supplierId: number): Promise<SupplierCreateResponse> {
try {
const response = await api.get(`/api/suppliers/${supplierId}`);
console.log(`📦 Supplier with ID ${supplierId} response:`, response);
return response.data;
} catch (error) {
console.error(`❌ Error fetching supplier with ID ${supplierId}:`, error);
throw error;
}
},
async createSupplier(data: SupplierCreateRequest): Promise<SupplierCreateResponse> {
try {
const response = await api.post('/api/suppliers', data);

View File

@ -4,7 +4,7 @@ import type {
CreateUnitOfMeasureData,
UpdateUnitOfMeasureData,
SingleUnitOfMeasureResponse
} from '../types/unitOfMeasure';
} from '../types/unit-measure.interfaces';
export const unitOfMeasureService = {
/**

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { supplierServices } from '../services/supplierServices';
import type { Supplier } from '../types/suppliers';
import { supplierServices } from '../services/supplier.services';
import type { Supplier } from '../types/suppliers.interfaces';
export const useSupplierStore = defineStore('supplier', () => {
const suppliers = ref<Supplier[]>([]);

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { unitOfMeasureService } from '../services/unitOfMeasureService';
import type { UnitOfMeasure, CreateUnitOfMeasureData, UpdateUnitOfMeasureData } from '../types/unitOfMeasure';
import { unitOfMeasureService } from '../services/unit-measure.services';
import type { UnitOfMeasure, CreateUnitOfMeasureData, UpdateUnitOfMeasureData } from '../types/unit-measure.interfaces';
export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
// State

View File

@ -1,11 +1,21 @@
// Enum para tipos de proveedor
export const SupplierType = {
CLIENT: 0,
PROVIDER: 1,
BOTH: 2
} as const;
export type SupplierType = typeof SupplierType[keyof typeof SupplierType];
// Errores de validación del formulario de proveedor
export interface SupplierFormErrors {
name?: string[];
contact_email?: string[];
phone_number?: string[];
address?: string[];
comercial_name?: string[];
rfc?: string[];
curp?: string[];
type?: string[];
credit_limit?: string[];
payment_days?: string[];
}
// Respuesta simple de proveedores (sin paginación)
export interface SupplierListResponse {
@ -15,10 +25,13 @@ export interface SupplierListResponse {
export interface Supplier {
id: number;
name: string;
contact_email: string;
phone_number: string;
address: string;
type: string;
comercial_name: string;
rfc: string;
curp: string;
type: SupplierType;
credit_limit: string;
payment_days: number;
addresses?: SupplierAddress[];
created_at: string;
updated_at: string;
deleted_at: string | null;
@ -51,12 +64,26 @@ export interface SupplierDeleteResponse {
data: null;
}
export interface SupplierAddress {
country: string;
postal_code: string;
state: string;
municipality: string;
city: string;
street: string;
num_ext: string;
num_int: string;
}
export interface SupplierCreateRequest {
name: string;
contact_email: string;
phone_number: string;
address: string;
type: string;
comercial_name: string;
rfc: string;
curp: string;
type: SupplierType;
credit_limit: number;
payment_days: number;
addresses: SupplierAddress[];
}
export interface SupplierCreateResponse {

View File

@ -1,4 +1,4 @@
import type { Supplier } from '../../catalog/types/suppliers';
import type { Supplier } from '../../catalog/types/suppliers.interfaces';
export interface Product {
id: number;