From ad0bbb579d6572b42c2be0baf3a203d690cd4dec Mon Sep 17 00:00:00 2001
From: monomono
Date: Sun, 16 Aug 2026 03:10:57 -0500
Subject: [PATCH] #49 feat: grafico unificado de provenance con filtro por
niveles y re-estabilizacion
---
AGENTS.md | 2 +-
src/components/graph/VisChart.vue | 38 +-
.../ProductSupplierOrganizationChart.vue | 59 ----
.../provenance/ProductTerritoryChart.vue | 59 ----
src/components/provenance/ProvenanceGraph.vue | 95 +++++
.../provenance/ProvenanceSection.vue | 11 +-
src/components/provenance/provenance-graph.js | 113 +++---
src/components/provenance/provenance-vis.js | 9 +-
tests/unit/components/graph/VisChart.spec.js | 40 ++-
.../ProductSupplierOrganizationChart.spec.js | 182 ----------
.../provenance/ProductTerritoryChart.spec.js | 146 --------
.../provenance/ProvenanceGraph.spec.js | 154 +++++++++
.../provenance/ProvenanceSection.spec.js | 22 +-
.../provenance/provenance-graph.spec.js | 325 ++++++------------
14 files changed, 508 insertions(+), 747 deletions(-)
delete mode 100644 src/components/provenance/ProductSupplierOrganizationChart.vue
delete mode 100644 src/components/provenance/ProductTerritoryChart.vue
create mode 100644 src/components/provenance/ProvenanceGraph.vue
delete mode 100644 tests/unit/components/provenance/ProductSupplierOrganizationChart.spec.js
delete mode 100644 tests/unit/components/provenance/ProductTerritoryChart.spec.js
create mode 100644 tests/unit/components/provenance/ProvenanceGraph.spec.js
diff --git a/AGENTS.md b/AGENTS.md
index 14db67a..90d43e2 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/`): builders puros en `provenance-graph.js` (`buildSupplierOrganizationGraph`, `buildTerritoryGraph`, `hasAnySupplier`, `hasAnyTerritory`) 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). El gráfico de territorio va solo hasta departamento (sin país, por ahora Colombia). Las aristas se deduplican por par `(from, to)` y si un mismo par repite con distinta certeza gana la duda. Los charts (que muestran "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) 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`
- **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/graph/VisChart.vue b/src/components/graph/VisChart.vue
index 8957898..35a4853 100644
--- a/src/components/graph/VisChart.vue
+++ b/src/components/graph/VisChart.vue
@@ -27,6 +27,10 @@
type: Object,
default: () => ({}),
},
+ minVerticalSpacing: {
+ type: Number,
+ default: null,
+ },
})
const emit = defineEmits(['select'])
@@ -87,7 +91,28 @@
network.setData({ nodes: nodesDataSet, edges: new DataSet(edges) })
}
- onMounted(() => {
+ function applyMinVerticalSpacing (minSpacing) {
+ if (!minSpacing || minSpacing <= 0) return
+ const columns = new Map()
+ for (const [id, position] of Object.entries(network.getPositions())) {
+ const x = Math.round(position.x)
+ if (!columns.has(x)) columns.set(x, [])
+ columns.get(x).push({ id, x, y: position.y })
+ }
+ for (const column of columns.values()) {
+ column.sort((a, b) => a.y - b.y)
+ let previousY = null
+ for (const item of column) {
+ if (previousY !== null && item.y - previousY < minSpacing) {
+ network.moveNode(item.id, item.x, previousY + minSpacing)
+ }
+ previousY = network.getPositions()[item.id].y
+ }
+ }
+ }
+
+ function setupNetwork () {
+ if (network) network.destroy()
network = new Network(containerRef.value, { nodes: [], edges: [] }, mergeOptions())
render(props.nodes, props.edges)
@@ -100,15 +125,18 @@
network.once('stabilizationIterationsDone', () => {
network.setOptions({ physics: { enabled: false } })
+ applyMinVerticalSpacing(props.minVerticalSpacing)
network.fit()
})
- })
+ }
+
+ onMounted(setupNetwork)
watch(
() => [props.nodes, props.edges],
- ([nodes, edges]) => {
- if (!network) return
- render(nodes, edges)
+ () => {
+ if (!containerRef.value) return
+ setupNetwork()
},
{ deep: true }
)
diff --git a/src/components/provenance/ProductSupplierOrganizationChart.vue b/src/components/provenance/ProductSupplierOrganizationChart.vue
deleted file mode 100644
index e87a7ad..0000000
--- a/src/components/provenance/ProductSupplierOrganizationChart.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
- La información de proveedores y organizaciones de este pedido estará
- disponible próximamente.
-
-
-
-
-
-
-
-
-
diff --git a/src/components/provenance/ProductTerritoryChart.vue b/src/components/provenance/ProductTerritoryChart.vue
deleted file mode 100644
index 12289f5..0000000
--- a/src/components/provenance/ProductTerritoryChart.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
- La información de municipio, departamento y país de este pedido estará
- disponible próximamente.
-
-
-
-
-
-
-
-
-
diff --git a/src/components/provenance/ProvenanceGraph.vue b/src/components/provenance/ProvenanceGraph.vue
new file mode 100644
index 0000000..a6c8682
--- /dev/null
+++ b/src/components/provenance/ProvenanceGraph.vue
@@ -0,0 +1,95 @@
+
+
+
+ La información de origen de los productos de este pedido estará disponible
+ próximamente.
+
+
+
+
+ Elementos a graficar:
+
+
+
+
+ Selecciona al menos un elemento para graficar.
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/provenance/ProvenanceSection.vue b/src/components/provenance/ProvenanceSection.vue
index efae78d..4f6da26 100644
--- a/src/components/provenance/ProvenanceSection.vue
+++ b/src/components/provenance/ProvenanceSection.vue
@@ -6,19 +6,12 @@
Conoce quiénes producen los productos que compras y de qué territorios provienen.
- Proveedores y organizaciones
-
-
-
-
- Departamento y municipio
-
+