diff --git a/AGENTS.md b/AGENTS.md index 90d43e2..0bf2639 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,7 +177,7 @@ No hay un estilo mayoritario. El código histórico está partido: - Payload: `[{ product: {id, name, catalogue_images[]}, suppliers: [{ supplier: {...}, organization|null, municipality|null, department|null, country|null }] }]`. - **Genérico reutilizable:** - `src/components/graph/VisChart.vue`: wrapper de vis-network (props `nodes`, `edges`, `height`, `options`; import `import { DataSet, Network } from 'vis-network/standalone'`; emite `select` con el nodo). **OJO**: nodo con `image: null` → TypeError de vis; omitir la clave `image` si no hay foto -- **Específico público** (`src/components/provenance/`): builder puro en `provenance-graph.js` (`buildProvenanceGraph(provenance, kinds)`, `hasAnySupplier`) y el adaptador vis `provenance-vis.js` (`toVisNodes`, `toVisEdges`, `chartOptions`). **Semántica de certeza**: arista con `certain: true` es continua (inequívoca) y `certain: false` es discontinua (dudosa); con varios proveedores por producto se inserta un nodo `junction:` (disyunción) con arista sólida hasta él y discontinua hacia cada proveedor; la duda se corta donde los proveedores coinciden (misma organización/municipio/departamento). Las aristas se deduplican por par `(from, to)` y si un mismo par repite con distinta certeza gana la duda. `buildProvenanceGraph` acepta los niveles a graficar (`product`, `supplier`, `organization`, `municipality`, `department`, `country`); los niveles omitidos se saltan conectando el nivel previo con el siguiente. `ProvenanceGraph.vue` unifica los charts en uno con checkboxes de filtro (por defecto solo productos y proveedores) y columnas por nivel (x fijo por tipo; la física ordena la y); muestra "próximamente estará disponible" cuando no hay relaciones. `ProvenanceDetailModal.vue` y `ProvenanceSection.vue` +- **Específico público** (`src/components/provenance/`): builder puro en `provenance-graph.js` (`buildProvenanceGraph(provenance, kinds)`, `hasAnySupplier`) y el adaptador vis `provenance-vis.js` (`toVisNodes`, `toVisEdges`, `chartOptions`). **Semántica de certeza**: arista con `certain: true` es continua (inequívoca) y `certain: false` es discontinua (dudosa); con varios proveedores por producto se inserta un nodo `junction:` (disyunción) con arista sólida hasta él y discontinua hacia cada proveedor; la duda se corta donde los proveedores coinciden (misma organización/municipio/departamento). Las aristas se deduplican por par `(from, to)` y si un mismo par repite con distinta certeza gana la duda. `buildProvenanceGraph` acepta los niveles a graficar (`product`, `supplier`, `organization`, `municipality`, `department`, `country`); los niveles omitidos se saltan conectando el nivel previo con el siguiente. `ProvenanceGraph.vue` unifica los charts en uno con checkboxes de filtro (por defecto solo productos y proveedores), leyenda con el color de cada nivel (`KIND_COLORS` en `provenance-vis.js`), columnas por nivel (x fijo por tipo; la física ordena la y) y espaciado vertical mínimo (`minVerticalSpacing` en `VisChart`); muestra "próximamente estará disponible" cuando no hay relaciones. `ProvenanceSection.vue` muestra el título ("Origen de los productos") con un desplegable (clic en el título o botón chevron) que oculta el gráfico por defecto — patrón reutilizable para futuros bloques como el mapa leaflet. `ProvenanceDetailModal.vue` - **Admin CRUD** (`src/components/provenance/admin/`): `OrganizationsManagement.vue`, `SuppliersManagement.vue`, `GeographyManagement.vue` (tabs países/departamentos/municipios), `SupplierLinkDialog.vue` (vincula productos↔proveedores, abierto desde `ProductsManagement.vue`). Páginas en `src/pages/admin/{organizations,suppliers,geography}.vue`; rutas en `ADMIN_ROUTES` (`router/index.js`); ítems en `NavBar.vue` - **Endpoints provenance**: `/don_confiao/api/organizations/`, `/suppliers/`, `/countries/`, `/departments/`, `/municipalities/` (CRUD); vincular productos con `PATCH /don_confiao/api/products//` body `{"suppliers": [ids]}`; detalle de producto (con `suppliers`) via `GET /don_confiao/api/products//` - Los tests mockean `vis-network/standalone` (`vi.mock('vis-network/standalone', ...)`) o el propio `VisChart.vue`, y la API con `global.provide: { api }` diff --git a/src/components/provenance/ProvenanceGraph.vue b/src/components/provenance/ProvenanceGraph.vue index a6c8682..1014ce0 100644 --- a/src/components/provenance/ProvenanceGraph.vue +++ b/src/components/provenance/ProvenanceGraph.vue @@ -18,9 +18,18 @@ :key="option.value" v-model="selectedKinds" density="compact" - :label="option.label" :value="option.value" - /> + > + + + + diff --git a/src/components/provenance/ProvenanceSection.vue b/src/components/provenance/ProvenanceSection.vue index 4f6da26..4f0fcc9 100644 --- a/src/components/provenance/ProvenanceSection.vue +++ b/src/components/provenance/ProvenanceSection.vue @@ -1,16 +1,38 @@ diff --git a/src/components/provenance/provenance-vis.js b/src/components/provenance/provenance-vis.js index e17d99a..c83560d 100644 --- a/src/components/provenance/provenance-vis.js +++ b/src/components/provenance/provenance-vis.js @@ -25,6 +25,8 @@ const COLUMN_X = { country: 560, } +export const KIND_COLORS = COLORS + export function toVisNodes (nodes) { const products = nodes.filter(node => node.kind === 'product') const productY = new Map() diff --git a/tests/unit/components/provenance/ProvenanceGraph.spec.js b/tests/unit/components/provenance/ProvenanceGraph.spec.js index 186f247..b533934 100644 --- a/tests/unit/components/provenance/ProvenanceGraph.spec.js +++ b/tests/unit/components/provenance/ProvenanceGraph.spec.js @@ -44,7 +44,7 @@ function visChart (wrapper) { } function checkbox (wrapper, label) { - return wrapper.findAllComponents({ name: 'VCheckbox' }).find(cb => cb.props('label') === label) + return wrapper.findAllComponents({ name: 'VCheckbox' }).find(cb => cb.text().trim() === label) } async function setKinds (wrapper, kinds) { @@ -75,10 +75,20 @@ describe('ProvenanceGraph', () => { it('muestra un checkbox por cada nivel con productos y proveedores activos', () => { const wrapper = mountGraph() - const labels = wrapper.findAllComponents({ name: 'VCheckbox' }).map(cb => cb.props('label')) + const labels = wrapper.findAllComponents({ name: 'VCheckbox' }).map(cb => cb.text().trim()) expect(labels).toEqual(['Productos', 'Proveedores', 'Organizaciones', 'Municipios', 'Departamentos', 'País']) }) + it('dibuja un círculo de color junto a cada checkbox como leyenda', () => { + const wrapper = mountGraph() + + const dots = wrapper.findAll('.legend-dot') + expect(dots).toHaveLength(6) + expect(dots[0].element.style.backgroundColor).toBe('rgb(38, 166, 154)') + expect(dots[1].element.style.backgroundColor).toBe('rgb(66, 165, 245)') + expect(dots[2].element.style.backgroundColor).toBe('rgb(255, 183, 77)') + }) + it('al activar municipios agrega nodos de municipio y la arista proveedor→municipio', async () => { const wrapper = mountGraph() diff --git a/tests/unit/components/provenance/ProvenanceSection.spec.js b/tests/unit/components/provenance/ProvenanceSection.spec.js index a8f2ef2..497b07a 100644 --- a/tests/unit/components/provenance/ProvenanceSection.spec.js +++ b/tests/unit/components/provenance/ProvenanceSection.spec.js @@ -8,7 +8,7 @@ vi.mock('@/components/graph/VisChart.vue', () => ({ default: { name: 'VisChart', template: '
', - props: ['nodes', 'edges', 'height', 'options'], + props: ['nodes', 'edges', 'height', 'options', 'minVerticalSpacing'], }, })) @@ -47,16 +47,41 @@ describe('ProvenanceSection', () => { expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(false) }) - it('muestra el gráfico unificado con su título cuando hay provenance', () => { + it('muestra el título y oculta el gráfico por defecto', () => { const wrapper = mountSection() - expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(true) - expect(wrapper.text()).toContain('Origen e historia de los productos') + expect(wrapper.text()).toContain('Origen de los productos') + expect(wrapper.text()).not.toContain('historia') + expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(false) }) - it('pasa el provenance al gráfico', () => { + it('muestra el gráfico al hacer clic en el título y lo oculta al volver a hacer clic', async () => { const wrapper = mountSection() + await wrapper.find('[data-test="provenance-toggle"]').trigger('click') + expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(true) + + await wrapper.find('[data-test="provenance-toggle"]').trigger('click') + expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(false) + }) + + it('también despliega y repliega con el botón de chevron', async () => { + const wrapper = mountSection() + + const button = wrapper.find('[data-test="provenance-toggle-button"]') + expect(button.exists()).toBe(true) + await button.trigger('click') + expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(true) + + await button.trigger('click') + expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(false) + }) + + it('pasa el provenance al gráfico al desplegarlo', async () => { + const wrapper = mountSection() + + await wrapper.find('[data-test="provenance-toggle"]').trigger('click') + expect(wrapper.findComponent(ProvenanceGraph).props('provenance')).toStrictEqual(provenance) }) })