diff --git a/.eslintrc.js b/.eslintrc.js
index fdc33b3..8dbbc61 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -31,6 +31,10 @@ module.exports = {
'!src/components/PublicOrderSummary.vue',
'!src/components/order/*.vue',
'!src/components/order/**/*.vue',
+ '!src/components/provenance/*.vue',
+ '!src/components/provenance/**/*.vue',
+ '!src/components/graph/*.vue',
+ '!src/services/graph/*.js',
'!src/pages/pedido/*.vue',
'!src/pages/pedido/**/*.vue',
],
diff --git a/src/components/ProductsManagement.vue b/src/components/ProductsManagement.vue
index da45e57..10da31c 100644
--- a/src/components/ProductsManagement.vue
+++ b/src/components/ProductsManagement.vue
@@ -122,6 +122,19 @@
+
+
+
+ Proveedores
+
+
+
@@ -138,6 +151,13 @@
+
+
+
import { ref, watch, inject, onMounted, computed } from "vue";
+import SupplierLinkDialog from "@/components/provenance/admin/SupplierLinkDialog.vue";
// Estado
const api = inject("api");
@@ -164,6 +185,8 @@ const selected = ref([]);
const loading = ref(false);
const snackbar = ref({ show: false, message: "", color: "success" });
const searchQuery = ref("");
+const linkDialog = ref(false);
+const linkProduct = ref(null);
// Headers de la tabla
const headers = [
@@ -171,6 +194,7 @@ const headers = [
{ title: "Nombre", key: "name", sortable: true },
{ title: "Precio", key: "price", sortable: true },
{ title: "Estado", key: "active", sortable: true },
+ { title: "Proveedores", key: "actions", sortable: false },
];
// Computed - Productos filtrados por búsqueda
@@ -236,6 +260,11 @@ function showSnackbar(message, color) {
snackbar.value = { show: true, message, color };
}
+function openSupplierLink(product) {
+ linkProduct.value = product;
+ linkDialog.value = true;
+}
+
// Watchers
watch(activeFilter, () => {
selected.value = [];
diff --git a/src/components/provenance/admin/SupplierLinkDialog.vue b/src/components/provenance/admin/SupplierLinkDialog.vue
new file mode 100644
index 0000000..5a3a969
--- /dev/null
+++ b/src/components/provenance/admin/SupplierLinkDialog.vue
@@ -0,0 +1,126 @@
+
+
+
+ Proveedores del producto
+
+
+ {{ product.name }}
+
+
+
+ Seleccionados ({{ selectedIds.length }})
+
+
+
+
+ Cancelar
+
+ Guardar
+
+
+
+
+
+
+
diff --git a/tests/unit/components/provenance/admin/SupplierLinkDialog.spec.js b/tests/unit/components/provenance/admin/SupplierLinkDialog.spec.js
new file mode 100644
index 0000000..195b2c7
--- /dev/null
+++ b/tests/unit/components/provenance/admin/SupplierLinkDialog.spec.js
@@ -0,0 +1,72 @@
+import { describe, expect, it, vi } from 'vitest'
+import { flushPromises, mount } from '@vue/test-utils'
+import SupplierLinkDialog from '@/components/provenance/admin/SupplierLinkDialog.vue'
+import vuetify from '@/plugins/vuetify'
+import { clickBody } from './helpers'
+
+const suppliers = [
+ { id: 5, name: 'Asociación Agropecuaria La Mesa' },
+ { id: 9, name: 'Finca El Paraíso' },
+ { id: 12, name: 'Cooperativa del Valle' },
+]
+
+function mockApi () {
+ return {
+ getSuppliers: vi.fn().mockResolvedValue(suppliers),
+ getProduct: vi.fn().mockResolvedValue({ id: 1, name: 'Panela regional por Kg', suppliers: [5] }),
+ updateProduct: vi.fn().mockResolvedValue({}),
+ }
+}
+
+function mountDialog (api, props = {}) {
+ return mount(SupplierLinkDialog, {
+ props: { visible: true, product: { id: 1, name: 'Panela regional por Kg' }, ...props },
+ global: { plugins: [vuetify], provide: { api } },
+ })
+}
+
+describe('SupplierLinkDialog', () => {
+ it('carga los proveedores y los suppliers actuales del producto al abrir', async () => {
+ const api = mockApi()
+ mountDialog(api)
+ await flushPromises()
+
+ expect(api.getSuppliers).toHaveBeenCalled()
+ expect(api.getProduct).toHaveBeenCalledWith(1)
+ expect(document.body.textContent).toContain('Panela regional por Kg')
+ })
+
+ it('permite cambiar la selección y guarda los proveedores', async () => {
+ const api = mockApi()
+ const wrapper = mountDialog(api)
+ await flushPromises()
+
+ const autocomplete = wrapper.findAllComponents({ name: 'VAutocomplete' })
+ .find(component => component.props('items').some(item => item.id === 5))
+ await autocomplete.vm.$emit('update:modelValue', [5, 9])
+
+ expect(document.body.textContent).toContain('Seleccionados (2)')
+
+ clickBody('[data-testid="supplier-link-save"]')
+ await flushPromises()
+
+ expect(api.updateProduct).toHaveBeenCalledWith(1, { suppliers: [5, 9] })
+ })
+
+ it('mantiene los proveedores seleccionados aunque el filtro no los incluya', async () => {
+ const api = mockApi()
+ const wrapper = mountDialog(api)
+ await flushPromises()
+
+ const autocomplete = wrapper.findAllComponents({ name: 'VAutocomplete' })
+ .find(component => component.props('items').some(item => item.id === 5))
+ await autocomplete.vm.$emit('update:modelValue', [5, 12])
+ await autocomplete.vm.$emit('update:search', 'Finca')
+
+ const items = autocomplete.props('items')
+ expect(items.some(item => item.id === 5)).toBe(true)
+ expect(items.some(item => item.id === 12)).toBe(true)
+ expect(items.some(item => item.id === 9)).toBe(true)
+ expect(items.some(item => item.id === 7)).toBe(false)
+ })
+})