Mejoras en el grafo de trazabilidad
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s

- Corregir referencia a colorMode (agregar .value)
- Optimizar renderizado de aristas usando paths SVG
- Mejorar posicionamiento de nodos con agrupación por padre
- Evitar solapamiento de nodos hermanos
- Ajustar cálculo dinámico del ancho del SVG
This commit is contained in:
2025-11-22 02:57:12 -06:00
parent 22fe3547b5
commit 8816900edd

View File

@@ -30,7 +30,7 @@
</defs> </defs>
<!-- Grid horizontal y vertical --> <!-- Grid horizontal y vertical -->
<g stroke="#e2e8f0" stroke-width="1" :stroke-opacity="colorMode === 'dark' ? 0.1 : 0.35"> <g stroke="#e2e8f0" stroke-width="1" :stroke-opacity="colorMode.value === 'dark' ? 0.1 : 0.35">
<line <line
v-for="depth in depthLines" v-for="depth in depthLines"
:key="`h-${depth}`" :key="`h-${depth}`"
@@ -52,48 +52,20 @@
</g> </g>
<!-- Aristas con codo (vertical + horizontal) --> <!-- Aristas con codo (vertical + horizontal) -->
<g stroke="url(#edgeGradient)" stroke-width="2.5" stroke-linecap="round"> <g
<template v-for="edge in edges" :key="`${edge.from}-${edge.to}`"> stroke="url(#edgeGradient)"
<!-- tramo vertical --> stroke-width="2.5"
<line stroke-linecap="round"
:x1="edge.fromPos.x" stroke-linejoin="round"
:y1="edge.fromPos.y" fill="none"
:x2="edge.fromPos.x" >
:y2="edge.midPos.y" <path
class="transition-opacity duration-200" v-for="edge in edges"
:opacity="0.9" :key="edge.id"
/> :d="edge.path"
<!-- tramo horizontal --> class="transition-opacity duration-200"
<line :opacity="0.9"
:x1="edge.fromPos.x" />
:y1="edge.midPos.y"
:x2="edge.toPos.x"
:y2="edge.midPos.y"
class="transition-opacity duration-200"
:opacity="0.9"
/>
<!-- tramo final vertical -->
<line
:x1="edge.toPos.x"
:y1="edge.midPos.y"
:x2="edge.toPos.x"
:y2="edge.toPos.y"
class="transition-opacity duration-200"
:opacity="0.9"
/>
<!-- tramo diagonal suave (opcional) -->
<line
v-if="edge.toPos.x !== edge.fromPos.x"
:x1="edge.fromPos.x"
:y1="edge.midPos.y"
:x2="edge.toPos.x"
:y2="edge.toPos.y"
class="transition-opacity duration-200"
:opacity="0.4"
stroke-dasharray="4 6"
/>
</template>
</g> </g>
<!-- Nodos --> <!-- Nodos -->
@@ -170,6 +142,10 @@ const paddingX = 80
const paddingY = 60 const paddingY = 60
const levelGap = 140 const levelGap = 140
const nodeBox = { width: 180, height: 64 } const nodeBox = { width: 180, height: 64 }
const horizontalGap = 40
const siblingSpacing = nodeBox.width + horizontalGap
const edgePadding = 12
const minElbowHeight = 24
const colorMode = useColorMode() const colorMode = useColorMode()
const bgGradient = computed(() => colorMode.value === 'dark' const bgGradient = computed(() => colorMode.value === 'dark'
? 'from-slate-900 to-slate-950' ? 'from-slate-900 to-slate-950'
@@ -203,23 +179,88 @@ const nodes = computed<GraphNode[]>(() => {
const result: GraphNode[] = [] const result: GraphNode[] = []
const levels = groupedByDepth.value const levels = groupedByDepth.value
if (levels.size === 0) return result if (levels.size === 0) return result
const maxCount = Math.max(...Array.from(levels.values()).map((l) => l.length))
for (const [depth, rows] of Array.from(levels.entries()).sort((a, b) => a[0] - b[0])) { const depthKeys = Array.from(levels.keys()).sort((a, b) => a - b)
const count = rows.length const minCenterX = paddingX + nodeBox.width / 2
const width = Math.max(maxCount * nodeBox.width * 1.2, 400) const placedByDepth = new Map<number, GraphNode[]>()
const spacing = width / (count + 1)
rows.forEach((row, idx) => { for (const depth of depthKeys) {
const rows = [...(levels.get(depth) || [])]
// Nivel raíz: ubicar centrado y con separación básica
if (depth === 0) {
rows.forEach((row, idx) => {
result.push({
id: row.lote_id,
label: row.codigo || row.lote_id.slice(0, 8),
tipo: row.tipo,
cantidad_kg: row.cantidad_kg,
depth,
x: minCenterX + idx * siblingSpacing,
y: paddingY + depth * levelGap,
})
})
placedByDepth.set(depth, result.filter((n) => n.depth === depth))
continue
}
const parents = placedByDepth.get(depth - 1) || []
const childrenByParent = new Map<string, TrazabilidadRow[]>()
const placedIds = new Set<string>()
for (const row of rows) {
const key = row.parent_lote_id || '__orphan__'
const list = childrenByParent.get(key) || []
list.push(row)
childrenByParent.set(key, list)
}
let cursor = minCenterX
// Ubicar hijos agrupados bajo su padre directo
for (const parent of parents) {
const children = childrenByParent.get(parent.id)
if (!children?.length) continue
const groupWidth = (children.length - 1) * siblingSpacing
let startX = parent.x - groupWidth / 2
// Evitar solapamiento con grupos previos
if (startX < cursor) startX = cursor
children.forEach((row, idx) => {
const x = startX + idx * siblingSpacing
result.push({
id: row.lote_id,
label: row.codigo || row.lote_id.slice(0, 8),
tipo: row.tipo,
cantidad_kg: row.cantidad_kg,
depth,
x,
y: paddingY + depth * levelGap,
})
placedIds.add(row.lote_id)
cursor = x + siblingSpacing
})
}
// Cualquier nodo huérfano (sin padre en el nivel superior) se coloca al final
const orphanRows = rows.filter((row) => !placedIds.has(row.lote_id))
orphanRows.forEach((row, idx) => {
const x = cursor + idx * siblingSpacing
result.push({ result.push({
id: row.lote_id, id: row.lote_id,
label: row.codigo || row.lote_id.slice(0, 8), label: row.codigo || row.lote_id.slice(0, 8),
tipo: row.tipo, tipo: row.tipo,
cantidad_kg: row.cantidad_kg, cantidad_kg: row.cantidad_kg,
depth, depth,
x: paddingX + spacing * (idx + 1), x,
y: paddingY + depth * levelGap, y: paddingY + depth * levelGap,
}) })
cursor = x + siblingSpacing
}) })
placedByDepth.set(depth, result.filter((n) => n.depth === depth))
} }
return result return result
@@ -235,11 +276,11 @@ const nodeById = computed(() => {
const edges = computed(() => { const edges = computed(() => {
const list: { const list: {
id: string
from: string from: string
to: string to: string
fromPos: { x: number; y: number } path: string
toPos: { x: number; y: number } midY: number
midPos: { x: number; y: number }
}[] = [] }[] = []
for (const row of props.historial) { for (const row of props.historial) {
@@ -247,12 +288,20 @@ const edges = computed(() => {
const fromNode = nodeById.value.get(row.parent_lote_id) const fromNode = nodeById.value.get(row.parent_lote_id)
const toNode = nodeById.value.get(row.lote_id) const toNode = nodeById.value.get(row.lote_id)
if (fromNode && toNode) { if (fromNode && toNode) {
const fromY = fromNode.y + nodeBox.height / 2 + edgePadding
const toY = toNode.y - nodeBox.height / 2 - edgePadding
const rawMidY = (fromY + toY) / 2
const midY = Math.max(
fromY + minElbowHeight,
Math.min(toY - minElbowHeight, rawMidY),
)
list.push({ list.push({
id: `${fromNode.id}-${toNode.id}`,
from: fromNode.id, from: fromNode.id,
to: toNode.id, to: toNode.id,
fromPos: { x: fromNode.x, y: fromNode.y + nodeBox.height / 2 }, midY,
toPos: { x: toNode.x, y: toNode.y - nodeBox.height / 2 }, path: `M ${fromNode.x} ${fromY} L ${fromNode.x} ${midY} L ${toNode.x} ${midY} L ${toNode.x} ${toY}`,
midPos: { x: fromNode.x, y: (fromNode.y + toNode.y) / 2 },
}) })
} }
} }
@@ -262,8 +311,11 @@ const edges = computed(() => {
const svgWidth = computed(() => { const svgWidth = computed(() => {
if (!nodes.value.length) return 600 if (!nodes.value.length) return 600
const maxX = Math.max(...nodes.value.map((n) => n.x + nodeBox.width / 2), 600) const xs = nodes.value.map((n) => n.x)
return maxX + paddingX const minX = Math.min(...xs)
const maxX = Math.max(...xs)
const width = (maxX - minX) + paddingX * 2 + nodeBox.width
return Math.max(width, 600)
}) })
const svgHeight = computed(() => { const svgHeight = computed(() => {