mejoras de UI 2
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
This commit is contained in:
@@ -30,9 +30,9 @@
|
|||||||
<UFormGroup label="Tipo" name="tipo" required>
|
<UFormGroup label="Tipo" name="tipo" required>
|
||||||
<USelect
|
<USelect
|
||||||
v-model="formState.tipo"
|
v-model="formState.tipo"
|
||||||
:options="TIPOS_LOTE"
|
:items="TIPOS_LOTE"
|
||||||
option-attribute="label"
|
label-key="label"
|
||||||
value-attribute="value"
|
value-key="value"
|
||||||
placeholder="Selecciona un tipo"
|
placeholder="Selecciona un tipo"
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<USelect
|
<USelect
|
||||||
v-model="filtroTipo"
|
v-model="filtroTipo"
|
||||||
:options="selectOptions"
|
:items="selectOptions"
|
||||||
option-attribute="label"
|
label-key="label"
|
||||||
value-attribute="value"
|
value-key="value"
|
||||||
searchable
|
searchable
|
||||||
placeholder="Filtrar por tipo"
|
placeholder="Filtrar por tipo"
|
||||||
class="w-64"
|
class="w-64"
|
||||||
|
|||||||
@@ -1,23 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-3">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-lg font-semibold">Grafo de trazabilidad</h4>
|
<h4 class="text-lg font-semibold">Grafo de trazabilidad</h4>
|
||||||
<p class="text-sm text-gray-500">Relaciones insumo → resultado por nivel de profundidad</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">Relaciones insumo → resultado por nivel</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
<UBadge color="blue" variant="subtle" v-if="nodes.length">
|
<UBadge color="blue" variant="subtle" v-if="nodes.length">
|
||||||
{{ nodes.length }} nodos · {{ edges.length }} aristas
|
{{ nodes.length }} nodos · {{ edges.length }} aristas
|
||||||
</UBadge>
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative w-full overflow-x-auto">
|
<div class="relative w-full overflow-x-auto rounded-xl border border-gray-200 dark:border-slate-800 bg-gradient-to-b" :class="bgGradient">
|
||||||
<svg
|
<svg
|
||||||
:width="svgWidth"
|
:width="svgWidth"
|
||||||
:height="svgHeight"
|
:height="svgHeight"
|
||||||
class="min-w-full bg-gradient-to-b from-white to-gray-50 rounded-lg border"
|
class="min-w-full"
|
||||||
|
:style="{ background: 'transparent' }"
|
||||||
>
|
>
|
||||||
|
<defs>
|
||||||
|
<filter id="nodeShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#0f172a" flood-opacity="0.08" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="edgeGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" :stop-color="edgeColor" stop-opacity="0.8" />
|
||||||
|
<stop offset="100%" :stop-color="edgeColor" stop-opacity="0.4" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Grid horizontal por nivel -->
|
||||||
|
<g stroke="#e2e8f0" stroke-width="1" :stroke-opacity="colorMode === 'dark' ? 0.1 : 0.35">
|
||||||
|
<line
|
||||||
|
v-for="depth in depthLines"
|
||||||
|
:key="depth"
|
||||||
|
:x1="0"
|
||||||
|
:x2="svgWidth"
|
||||||
|
:y1="paddingY + depth * levelGap"
|
||||||
|
:y2="paddingY + depth * levelGap"
|
||||||
|
stroke-dasharray="4 4"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
<!-- Aristas -->
|
<!-- Aristas -->
|
||||||
<g stroke="#CBD5E1" stroke-width="2">
|
<g stroke="url(#edgeGradient)" stroke-width="2.5" stroke-linecap="round">
|
||||||
<line
|
<line
|
||||||
v-for="edge in edges"
|
v-for="edge in edges"
|
||||||
:key="`${edge.from}-${edge.to}`"
|
:key="`${edge.from}-${edge.to}`"
|
||||||
@@ -25,8 +51,8 @@
|
|||||||
:y1="edge.fromPos.y"
|
:y1="edge.fromPos.y"
|
||||||
:x2="edge.toPos.x"
|
:x2="edge.toPos.x"
|
||||||
:y2="edge.toPos.y"
|
:y2="edge.toPos.y"
|
||||||
stroke-linecap="round"
|
|
||||||
class="transition-opacity duration-200"
|
class="transition-opacity duration-200"
|
||||||
|
:opacity="0.9"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
@@ -36,23 +62,25 @@
|
|||||||
v-for="node in nodes"
|
v-for="node in nodes"
|
||||||
:key="node.id"
|
:key="node.id"
|
||||||
class="transition-transform duration-200"
|
class="transition-transform duration-200"
|
||||||
|
filter="url(#nodeShadow)"
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
:x="node.x - nodeBox.width / 2"
|
:x="node.x - nodeBox.width / 2"
|
||||||
:y="node.y - nodeBox.height / 2"
|
:y="node.y - nodeBox.height / 2"
|
||||||
:width="nodeBox.width"
|
:width="nodeBox.width"
|
||||||
:height="nodeBox.height"
|
:height="nodeBox.height"
|
||||||
rx="12"
|
rx="14"
|
||||||
:fill="getNodeFill(node.tipo)"
|
:fill="getNodeFill(node.tipo)"
|
||||||
stroke="#0F172A"
|
:stroke="node.depth === 0 ? accentColor : '#0F172A'"
|
||||||
stroke-opacity="0.05"
|
:stroke-opacity="node.depth === 0 ? 0.6 : 0.05"
|
||||||
|
:stroke-width="node.depth === 0 ? 2 : 1"
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
:x="node.x"
|
:x="node.x"
|
||||||
:y="node.y - 10"
|
:y="node.y - 10"
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
class="font-medium"
|
class="font-semibold"
|
||||||
fill="#0F172A"
|
:fill="textColor"
|
||||||
>
|
>
|
||||||
{{ node.label }}
|
{{ node.label }}
|
||||||
</text>
|
</text>
|
||||||
@@ -61,7 +89,7 @@
|
|||||||
:y="node.y + 10"
|
:y="node.y + 10"
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
class="text-[11px]"
|
class="text-[11px]"
|
||||||
fill="#334155"
|
:fill="subTextColor"
|
||||||
>
|
>
|
||||||
{{ getTipoLabel(node.tipo) }}
|
{{ getTipoLabel(node.tipo) }}
|
||||||
</text>
|
</text>
|
||||||
@@ -71,7 +99,7 @@
|
|||||||
:y="node.y + 26"
|
:y="node.y + 26"
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
class="text-[10px]"
|
class="text-[10px]"
|
||||||
fill="#475569"
|
:fill="subTextColor"
|
||||||
>
|
>
|
||||||
{{ node.cantidad_kg.toLocaleString('es-AR') }} kg
|
{{ node.cantidad_kg.toLocaleString('es-AR') }} kg
|
||||||
</text>
|
</text>
|
||||||
@@ -80,27 +108,18 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 text-xs text-gray-600">
|
<div class="flex flex-wrap gap-2 text-xs">
|
||||||
<span class="inline-flex items-center gap-1">
|
<UBadge color="violet" variant="subtle">Uva</UBadge>
|
||||||
<span class="w-3 h-3 rounded-sm" :style="{ background: getNodeFill('uva') }"></span> Uva
|
<UBadge color="green" variant="subtle">Despulpado</UBadge>
|
||||||
</span>
|
<UBadge color="orange" variant="subtle">Oreado/Presecado</UBadge>
|
||||||
<span class="inline-flex items-center gap-1">
|
<UBadge color="blue" variant="subtle">Reposo</UBadge>
|
||||||
<span class="w-3 h-3 rounded-sm" :style="{ background: getNodeFill('despulpado_primera') }"></span> Despulpado
|
<UBadge color="emerald" variant="subtle">Secado/Mezcla</UBadge>
|
||||||
</span>
|
|
||||||
<span class="inline-flex items-center gap-1">
|
|
||||||
<span class="w-3 h-3 rounded-sm" :style="{ background: getNodeFill('oreado') }"></span> Oreado / Presecado
|
|
||||||
</span>
|
|
||||||
<span class="inline-flex items-center gap-1">
|
|
||||||
<span class="w-3 h-3 rounded-sm" :style="{ background: getNodeFill('reposo') }"></span> Reposo
|
|
||||||
</span>
|
|
||||||
<span class="inline-flex items-center gap-1">
|
|
||||||
<span class="w-3 h-3 rounded-sm" :style="{ background: getNodeFill('secado') }"></span> Secado / mezcla
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useColorMode } from '#imports'
|
||||||
import type { TrazabilidadRow } from '~/composables/useLotes'
|
import type { TrazabilidadRow } from '~/composables/useLotes'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -111,6 +130,14 @@ 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 colorMode = useColorMode()
|
||||||
|
const bgGradient = computed(() => colorMode.value === 'dark'
|
||||||
|
? 'from-slate-900 to-slate-950'
|
||||||
|
: 'from-white to-slate-50')
|
||||||
|
const edgeColor = computed(() => colorMode.value === 'dark' ? '#94a3b8' : '#94a3b8')
|
||||||
|
const textColor = computed(() => colorMode.value === 'dark' ? '#e2e8f0' : '#0f172a')
|
||||||
|
const subTextColor = computed(() => colorMode.value === 'dark' ? '#cbd5e1' : '#334155')
|
||||||
|
const accentColor = computed(() => colorMode.value === 'dark' ? '#22d3ee' : '#0ea5e9')
|
||||||
|
|
||||||
type GraphNode = {
|
type GraphNode = {
|
||||||
id: string
|
id: string
|
||||||
@@ -203,16 +230,22 @@ const svgHeight = computed(() => {
|
|||||||
return paddingY * 2 + maxDepth * levelGap + nodeBox.height
|
return paddingY * 2 + maxDepth * levelGap + nodeBox.height
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const depthLines = computed(() => {
|
||||||
|
if (!nodes.value.length) return []
|
||||||
|
const maxDepth = Math.max(...nodes.value.map((n) => n.depth), 0)
|
||||||
|
return Array.from({ length: maxDepth + 1 }, (_, i) => i)
|
||||||
|
})
|
||||||
|
|
||||||
const getNodeFill = (tipo: string): string => {
|
const getNodeFill = (tipo: string): string => {
|
||||||
const palette: Record<string, string> = {
|
const palette: Record<string, string> = {
|
||||||
uva: '#F3E8FF',
|
uva: colorMode.value === 'dark' ? '#4c1d95' : '#F3E8FF',
|
||||||
despulpado_primera: '#DCFCE7',
|
despulpado_primera: colorMode.value === 'dark' ? '#166534' : '#DCFCE7',
|
||||||
despulpado_segunda: '#FEF9C3',
|
despulpado_segunda: colorMode.value === 'dark' ? '#854d0e' : '#FEF9C3',
|
||||||
despulpado_rechazos: '#FEE2E2',
|
despulpado_rechazos: colorMode.value === 'dark' ? '#7f1d1d' : '#FEE2E2',
|
||||||
oreado: '#FFEDD5',
|
oreado: colorMode.value === 'dark' ? '#9a3412' : '#FFEDD5',
|
||||||
presecado: '#FDE68A',
|
presecado: colorMode.value === 'dark' ? '#92400e' : '#FDE68A',
|
||||||
reposo: '#DBEAFE',
|
reposo: colorMode.value === 'dark' ? '#1d4ed8' : '#DBEAFE',
|
||||||
secado: '#D1FAE5',
|
secado: colorMode.value === 'dark' ? '#065f46' : '#D1FAE5',
|
||||||
}
|
}
|
||||||
return palette[tipo] || '#E2E8F0'
|
return palette[tipo] || '#E2E8F0'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,9 +154,9 @@
|
|||||||
<UFormGroup label="Tipo" required>
|
<UFormGroup label="Tipo" required>
|
||||||
<USelect
|
<USelect
|
||||||
v-model="output.tipo"
|
v-model="output.tipo"
|
||||||
:options="TIPOS_LOTE"
|
:items="TIPOS_LOTE"
|
||||||
option-attribute="label"
|
label-key="label"
|
||||||
value-attribute="value"
|
value-key="value"
|
||||||
placeholder="Selecciona tipo"
|
placeholder="Selecciona tipo"
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user