🔍

Click-Path Audit — FlowCRM Pipeline Module

Auditoría de flujo de clics · dealStore + userStore · 4 touchpoints críticos
Web · Desarrollo React + Zustand FlowCRM SaaS B2B 18 Jun 2026
4
Bugs encontrados
1
Críticos
2
High / Medium
6
Touchpoints auditados
Paso 1 — Mapa de State Stores
dealStore.ts
8 acciones · 3 resets peligrosos
AcciónSetsResets (side effects)

openNewDealForm() isFormOpen: true, formMode: 'create'
selectDeal(deal|null) selectedDeal, isPanelOpen: true, messages, notes, activities ⚠ isFormOpen: false, formMode: null, formData: null
setActiveFilter(stage) activeFilters.stage ⚠ stats: null (fuerza refetch), selectedDeal: null, isPanelOpen: false
assignOwner(userId) assigningOwner: userId
confirmAssignOwner() selectedDeal.ownerId, assigningOwner: null ⚠ isPanelOpen: false (cierra panel sin aviso)
saveDeal(data) deals[id] actualizado
closePanel() isPanelOpen: false, selectedDeal: null
resetAll() estado inicial completo TODO el estado
⚡ Resets peligrosos detectados
selectDeal() resetea isFormOpen → false y formData → null (no es su responsabilidad)
setActiveFilter() resetea selectedDeal → null y isPanelOpen → false sin animación de cierre
confirmAssignOwner() cierra isPanelOpen → false silenciosamente como side effect
userStore.ts
3 acciones · 0 resets peligrosos
setCurrentUser(user) currentUser, permissions
setAssignableUsers(list) assignableUsers
clearSession() estado inicial solo propio estado
Paso 2–3 — Bugs por touchpoint
Critical CLICK-PATH-001 Botón "Nueva Oportunidad" — formulario aparece y desaparece Sequential Undo
Touchpoint
"Nueva Oportunidad" en PipelineBoard.tsx:87
Handler
onClick → handleNewDeal()
Traza de ejecución
1. dealStore.openNewDealForm() sets { isFormOpen: true, formMode: 'create' } ✓
2. dealStore.selectDeal(null) sets { selectedDeal: null } RESETS { isFormOpen: false, formData: null } ✗
// Resultado final: isFormOpen = false — el formulario se cierra antes de que el usuario lo vea
✓ Esperado
El modal de crear deal se abre y el usuario puede rellenar los campos.
✗ Actual
selectDeal(null) hace reset de isFormOpen como side effect. El modal parpadea y cierra en el mismo render cycle.
Fix recomendado
Eliminar isFormOpen: false del setter selectDeal en el store — no es responsabilidad de selectDeal gestionar el formulario. Alternativamente, reordenar las llamadas: primero selectDeal(null), luego openNewDealForm(). La solución correcta es separar las responsabilidades del store.
High CLICK-PATH-002 Botón "Asignar Propietario" — cierra el panel de deal sin aviso Sequential Undo
Touchpoint
"Asignar" en DealCard.tsx:134
Handler
onAssignClick → handleAssignOwner(userId)
Traza de ejecución
1. dealStore.assignOwner(userId) sets { assigningOwner: userId } ✓
2. await userStore.setAssignableUsers(await fetchUsers()) actualiza lista ✓
3. dealStore.confirmAssignOwner() sets { selectedDeal.ownerId, assigningOwner: null } RESETS { isPanelOpen: false } ✗
// El panel del deal se cierra inmediatamente tras asignar — el usuario pierde contexto
✓ Esperado
El propietario se asigna y el panel del deal permanece abierto mostrando el nuevo dueño.
✗ Actual
confirmAssignOwner cierra isPanelOpen silenciosamente. El usuario cree que el botón no hizo nada porque el panel desaparece.
Fix recomendado
Eliminar isPanelOpen: false del setter confirmAssignOwner. El cierre de panel debe ser explícito con closePanel() cuando el usuario así lo decida. Separar responsabilidades: "confirmar asignación" ≠ "cerrar panel".
High CLICK-PATH-003 Botón "Guardar Deal" — panel queda abierto pese a error de guardado Async Race + Missing State Transition
Touchpoint
"Guardar" en DealFormModal.tsx:201
Handler
onSubmit → handleSaveDeal(data)
Traza de ejecución
1. setIsSaving(true) estado local React, muestra spinner ✓
2. await dealStore.saveDeal(data) llama API ... puede fallar ⚠
3. [en el catch] setIsSaving(false) ¡NUNCA llega aquí! El catch no maneja el error ✗
4. [en el finally] setIsSaving(false) no hay bloque finally ✗
// Si la API falla, isSaving queda en true eternamente — el botón queda disabled/spinner infinito
// Y el panel permanece abierto sin mensaje de error visible al usuario
✓ Esperado
Si hay error: spinner desaparece, mensaje de error visible, usuario puede reintentar.
✗ Actual
El spinner queda activo indefinidamente. El botón "Guardar" parece no responder. No hay feedback de error. El deal no se guardó pero el usuario no lo sabe.
Fix recomendado
Envolver el bloque async en try/catch/finally: en finally hacer setIsSaving(false); en catch almacenar el error en estado local y renderizar el mensaje de error. Patrón: try { await save() } catch(e) { setError(e.message) } finally { setIsSaving(false) }.
Medium CLICK-PATH-004 FilterBar — contador de deals incorrecto tras cambiar filtro de etapa useEffect Interference
Touchpoint
StageFilter select en FilterBar.tsx:58
Handler
onChange → handleFilterChange(stage)
Traza de ejecución
1. dealStore.setActiveFilter(stage) sets { activeFilters.stage } RESETS { stats: null } ✗
2. useEffect en PipelineBoard observa activeFilters → dispara refetch de React Query
3. React Query empieza fetch de deals filtrados (async) ⚠
4. StatsBar.tsx observa deals (local, sin filtrar) para calcular contador → usa lista antigua ✗
// El contador muestra el total global mientras el fetch está en curso — nunca se actualiza al total filtrado
5. useEffect en StatsBar observa [stats] pero stats ya es null → re-calcula con deals stale
✓ Esperado
Al filtrar por etapa "Propuesta", el contador muestra el número de deals en esa etapa.
✗ Actual
El contador muestra el total anterior durante el refetch. Luego no se actualiza porque StatsBar calcula desde el array de deals stale, no desde el resultado del fetch filtrado.
Fix recomendado
StatsBar debe calcular el contador a partir de la respuesta de React Query (filteredDealsQuery.data), no del store local. Alternativamente, que el servidor devuelva el conteo en la misma respuesta de deals filtrados y almacenarlo en stats al resolver el fetch, no al iniciar el filtro.