#49 feat: desplegable en seccion de provenance con leyenda de colores y titulo sin historia
This commit is contained in:
@@ -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:<productId>` (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:<productId>` (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/<id>/` body `{"suppliers": [ids]}`; detalle de producto (con `suppliers`) via `GET /don_confiao/api/products/<id>/`
|
||||
- Los tests mockean `vis-network/standalone` (`vi.mock('vis-network/standalone', ...)`) o el propio `VisChart.vue`, y la API con `global.provide: { api }`
|
||||
|
||||
@@ -18,9 +18,18 @@
|
||||
:key="option.value"
|
||||
v-model="selectedKinds"
|
||||
density="compact"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
>
|
||||
<template #label>
|
||||
<span class="d-flex align-center">
|
||||
<span
|
||||
class="legend-dot"
|
||||
:style="{ backgroundColor: option.color }"
|
||||
/>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
@@ -57,7 +66,7 @@
|
||||
import VisChart from '@/components/graph/VisChart.vue'
|
||||
import ProvenanceDetailModal from './ProvenanceDetailModal.vue'
|
||||
import { buildProvenanceGraph, hasAnySupplier } from './provenance-graph'
|
||||
import { chartOptions, toVisEdges, toVisNodes } from './provenance-vis'
|
||||
import { chartOptions, KIND_COLORS, toVisEdges, toVisNodes } from './provenance-vis'
|
||||
|
||||
const props = defineProps({
|
||||
provenance: {
|
||||
@@ -67,12 +76,12 @@
|
||||
})
|
||||
|
||||
const options = [
|
||||
{ value: 'product', label: 'Productos' },
|
||||
{ value: 'supplier', label: 'Proveedores' },
|
||||
{ value: 'organization', label: 'Organizaciones' },
|
||||
{ value: 'municipality', label: 'Municipios' },
|
||||
{ value: 'department', label: 'Departamentos' },
|
||||
{ value: 'country', label: 'País' },
|
||||
{ value: 'product', label: 'Productos', color: KIND_COLORS.product },
|
||||
{ value: 'supplier', label: 'Proveedores', color: KIND_COLORS.supplier },
|
||||
{ value: 'organization', label: 'Organizaciones', color: KIND_COLORS.organization },
|
||||
{ value: 'municipality', label: 'Municipios', color: KIND_COLORS.municipality },
|
||||
{ value: 'department', label: 'Departamentos', color: KIND_COLORS.department },
|
||||
{ value: 'country', label: 'País', color: KIND_COLORS.country },
|
||||
]
|
||||
|
||||
const selectedKinds = ref(['product', 'supplier'])
|
||||
@@ -93,3 +102,13 @@
|
||||
modalVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.legend-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
<template>
|
||||
<div v-if="provenance && provenance.length > 0">
|
||||
<v-divider class="my-4" />
|
||||
<h3 class="text-h6 font-weight-bold mb-1">Origen e historia de los productos</h3>
|
||||
<div
|
||||
class="d-flex align-center justify-space-between cursor-pointer"
|
||||
data-test="provenance-toggle"
|
||||
role="button"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<h3 class="text-h6 font-weight-bold">
|
||||
Origen de los productos
|
||||
</h3>
|
||||
<v-btn
|
||||
data-test="provenance-toggle-button"
|
||||
density="comfortable"
|
||||
:icon="expanded ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
variant="text"
|
||||
@click.stop="expanded = !expanded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-expand-transition>
|
||||
<div v-if="expanded">
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Conoce quiénes producen los productos que compras y de qué territorios provienen.
|
||||
</p>
|
||||
|
||||
<ProvenanceGraph :provenance="provenance" />
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import ProvenanceGraph from './ProvenanceGraph.vue'
|
||||
|
||||
defineProps({
|
||||
@@ -19,4 +41,6 @@
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const expanded = ref(false)
|
||||
</script>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ vi.mock('@/components/graph/VisChart.vue', () => ({
|
||||
default: {
|
||||
name: 'VisChart',
|
||||
template: '<div class="vis-chart-stub" />',
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user