Propósito: Gestionar login y validación de credenciales
Responsabilidades:
- Validar usuario y PIN contra base de datos
- Mantener token de sesión activa
- Cerrar sesión de usuario
Métodos Principales:
autenticar(nombre, pin) → Usuario | null
• Consulta tabla "usuarios" en Supabase
• Valida que PIN coincida
• Retorna objeto Usuario si es válido
cerrarSesion() → Future<void>
• Limpia sesión actual
• Elimina token de memoria
obtenerUsuarioActual() → Usuario?
• Obtiene usuario en sesión sin DBDependencias:
- supabase_flutter
- usuario_model.dart
Estado Manejado:
- Usuario autenticado actual
- Token de sesión
Propósito: Core del sistema realtime - Sincroniza datos con PostgreSQL en vivo
Responsabilidades Críticas:
- Gestionar StreamControllers para datos reactivos
- Configurar listeners con Supabase Realtime
- Detectar cambios en inventario y movimientos
- Disparar callbacks para animaciones
Métodos Principales:
obtenerInventarioStream() → Stream<List<Map>>
• Retorna stream actualizado en tiempo real
• Emite cuando hay cambios en BD
• Usado en HomeScreen + StockScreen
obtenerAlmacenes() → Future<List<Map>>
• Lista todos los almacenes disponibles
• Cache local para optimización
obtenerMovimientos() → Future<List<Map>>
• Historial de movimientos de stock
• Filtra por fecha y usuario
initializeRealtimeListeners() → void
⚠️ CRÍTICO: Se llama UNA SOLA VEZ en main.dart
• _subscribeToAlmacenesChanges()
• _subscribeToMovimientosChanges()
• Configura WebSocket listeners
dispose() → void
• Limpia todos los streams
• Cancela suscripciones
• IMPORTANTE: Llamar en onDisposeCallbacks para Animaciones:
onNewStockChange: Function() ?
• Se dispara cuando actualiza inventario
• Anima tarjeta de Stock en HomeScreen
onNewMovimiento: Function() ?
• Se dispara cuando hay nuevo movimiento
• Anima tarjeta de Movimientos en HomeScreenListeners Internos:
_subscribeToAlmacenesChanges()
└─ Escucha: INSERT, UPDATE en tabla "almacenes"
└─ Acción: Actualiza _inventarioStreamController
└─ Efecto: Dispara onNewStockChange callback
_subscribeToMovimientosChanges()
└─ Escucha: INSERT en tabla "movimientos"
└─ Acción: Agrega nuevo movimiento a stream
└─ Efecto: Dispara onNewMovimiento callback
Mapeo de Datos:
Inventario {
id: int,
almacen_id: int,
producto: string,
cantidad: int,
cantidad_minima: int,
cantidad_maxima: int,
precio_unitario: decimal,
fecha_actualizacion: timestamp
}
Movimiento {
id: int,
tipo: 'entrada' | 'salida' | 'transferencia',
cantidad: int,
almacen_origen_id: int,
almacen_destino_id: int,
usuario_id: int,
fecha: timestamp,
motivo: string,
estado: 'pendiente' | 'completado' | 'cancelado'
}Dependencias:
- supabase_flutter
- dart:async (StreamControllers)
Propósito: Persistir y recuperar información de sesión del usuario
Responsabilidades:
- Guardar datos de usuario en dispositivo
- Recuperar sesión guardada al abrir app
- Limpiar sesión al logout
Métodos Principales:
guardarSesion(usuario) → Future<void>
• Almacena Usuario en SharedPreferences
• Encripta si es posible
obtenerSesion() → Future<Usuario?>
• Lee Usuario de SharedPreferences
• Retorna null si no existe
limpiarSesion() → Future<void>
• Elimina datos de SharedPreferences
• Limpia caché local
obtenerUsuarioActual() → Usuario?
• Lee usuario en memoria (sin I/O)
• Rápido para checks frecuentesAlmacenamiento:
- ✅ SharedPreferences (local)
⚠️ NO encriptado actualmente- 🔒 Considerar Keychain/KeyStore en producción
Dependencias:
- shared_preferences
- usuario_model.dart
Propósito: Wrapper para operaciones Supabase
Responsabilidades:
- Gestionar cliente Supabase singleton
- Métodos helpers para CRUD
- Manejo de errores de conexión
Métodos Principales:
getClient() → SupabaseClient
• Singleton del cliente Supabase
query(table) → PostgrestQueryBuilder
• Builder para consultas
consultarInventario() → Future<List<Map>>
• SELECT * FROM inventarios
insertarMovimiento(datos) → Future<void>
• INSERT INTO movimientos
actualizarInventario(id, datos) → Future<void>
• UPDATE inventarios SET...Dependencias:
- supabase_flutter
Propósito: Operaciones específicas de transferencia de inventario
Responsabilidades:
- Validar transferencias
- Actualizar almacenes origen y destino
- Crear registro en movimientos
Métodos Principales:
transferir(origen, destino, cantidad, motivo)
• Valida disponibilidad
• Resta de origen, suma a destino
• Registra en movimientosDependencias:
- supabase_flutter
- dashboard_service.dart
Propósito: Interfaz de ingreso de usuario
Componentes:
- TextField para usuario
- TextField para PIN (con toggle visibilidad)
- Botón Login
- SnackBar para errores
Flujo:
Usuario ingresa credenciales
↓
Presiona botón Login
↓
authService.autenticar(usuario, pin)
↓
¿Validado?
├─ SÍ: SessionService.guardarSesion()
│ Navigator → MainLayout
└─ NO: SnackBar error
Estado:
_usuario: String // Username ingresado
_pin: String // PIN ingresado
_mostrarPin: bool // Toggle visibilidad
_cargando: bool // Loading durante validaciónPropósito: Panel principal con datos en tiempo real
Características: ✅ Dos tarjetas lado a lado (Row layout) ✅ Animaciones con glow effect verde ✅ Lista de actividades expandible ✅ Actualización en tiempo real con StreamBuilder
Layout:
┌─────────────────────────────────┐
│ HomeScreen Container │
├─────────────────┬───────────────┤
│ Stock General │ Movimientos │
│ (izquierda) │ Hoy │
│ │ (derecha) │
├─────────────────────────────────┤
│ Activity List (expandible) │
│ - últimos 10 movimientos │
│ - AnimatedOpacity │
│ - SlideTransition │
└─────────────────────────────────┘
Animaciones:
AnimationController
├─ Duración: 1 segundo
├─ Curve: Curves.easeInOut
└─ Usado para: FadeTransition + glow
onNewStockChange callback
└─ Dispara: animationController.forward()
└─ Efecto: Tarjeta izquierda destella
onNewMovimiento callback
└─ Dispara: animationController.forward()
└─ Efecto: Tarjeta derecha destellaDatos Mostrados:
- Stock General: Suma de cantidades en inventario
- Movimientos Hoy: Conteo de movimientos en fecha actual
- Actividades: Usuario, Tipo, Cantidad, Hora
Estado:
_animationController: AnimationController
_inventarioStream: Stream<List<Map>>
_movimientosStream: Stream<List<Map>>
_expanded: bool // Lista expandida?Propósito: Pantalla principal de gestión de stock
Funcionalidades: ✅ Vista Grid/List de productos ✅ Filtro por almacén ✅ Búsqueda de productos ✅ Transferencia entre almacenes ✅ Edición de cantidades ✅ Indicadores visuales de stock
Componentes:
AppBar
├─ Título: "Stock"
├─ Toggle Grid/List button
└─ Filtro almacén selector
StreamBuilder<List<Map>>
├─ data: Grid/List de productos
├─ loading: Shimmer loader
└─ error: Error widget
Grid/List View
├─ ProductCard/ProductRow
├─ Actions: Edit, Transfer
└─ Indicadores de estadoVista Grid:
Tarjetas de productos en cuadrícula
┌──────────┐ ┌──────────┐
│ Producto │ │ Producto │
│ Cantidad │ │ Cantidad │
│ Estado │ │ Estado │
└──────────┘ └──────────┘
Vista List:
Filas con más detalles
┌─────────────────────────────────────┐
│ Prod | Cant | Almacén | Acciones │
├─────────────────────────────────────┤
│ Item 1 | 125 | Almacén 1| Edit/Xfer│
Indicadores de Stock:
cantidad >= cantidad_maxima * 0.8 → VERDE ✅
cantidad >= cantidad_minima → NARANJA ⚠️
cantidad < cantidad_minima → ROJO ❌Diálogos:
EditarCantidadDialog
├─ Ingresa nueva cantidad
├─ Valida > 0
└─ Actualiza en BD
TransferenciaDialog (stock_transfer_sheet.dart)
├─ Almacén origen
├─ Almacén destino
├─ Cantidad
└─ Motivo
Estado:
_vistaGrid: bool // Grid o List?
_filtroAlmacenId: int? // Almacén seleccionado
_inventarioStream: Stream // Datos en vivo
_ultimoInventario: List<Map>? // Cache
_almacenes: List<Map> // Almacenes disponiblesPropósito: Navegación entre pantallas principales
Estructura:
Scaffold
├─ appBar (opcional)
├─ body: IndexedStack
│ ├─ HomeScreen (index 0)
│ └─ StockScreen (index 1)
└─ bottomNavigationBar
├─ "Panel" icon (índice 0)
└─ "Stock" icon (índice 1)Características: ✅ Navegación persistent (mantiene estado) ✅ IndexedStack para no perder datos ✅ BottomNavigationBar con 2 tabs
Estado:
_selectedIndex: int = 0 // Tab seleccionadoNavegación:
BottomNavigationBar tap (index)
└─ setState(() { _selectedIndex = index; })
└─ IndexedStack cambia a ese widget
Propósito: Componente reutilizable para mostrar KPIs
Props:
titulo: String // "Stock General"
valor: String // "1,245"
color: Color // #1B4332 (verde)
icono: IconData // Icons.inventory_2
animado: bool? // Mostrar animación?Efectos Visuales:
- Sombra verde glow
- FadeTransition suave
- Responsive sizing
Propósito: Bottom Sheet para transferencias de inventario
Campos:
- Selector almacén origen
- Selector almacén destino
- TextField cantidad
- TextField motivo
Validaciones: ✓ Cantidad > 0 ✓ Almacenes diferentes ✓ Stock disponible
Acción:
Usuario completa form
↓
Validar datos
↓
Crear registro en "movimientos"
↓
Actualizar "almacenes" origen/destino
↓
Cerrar dialog + mostrar success
Propósito: Widget reutilizable que anima cuando llegan datos en tiempo real
Props:
channel: String // 'almacenes'
tableName: String // 'almacenes'
child: Widget // Widget a animarFuncionalidad:
- Suscribe a cambios en tabla
- Dispara animación glow
- Refresca datos
Propósito: Animar transición de números
Props:
value: int // Valor final
duration: Duration // Duración animaciónEfecto:
Valor anterior: 100
Valor nuevo: 125
↓
Anima suavemente de 100 → 125
Propósito: Widget de navegación personalizada
Estado:
- Se mantiene para referencia futura
- System actual usa BottomNavigationBar en main_layout.dart
Estructura:
class Usuario {
final int id;
final String nombre;
final String rango; // 'Admin', 'Owner', 'Empleado'
final bool activo;
final DateTime? fechaCreacion;
// Getters útiles
bool esAdmin() → nombre == 'Admin'
bool esOwner() → nombre == 'Owner'
}Responsabilidad: Inicialización y configuración global
Flujo:
main()
├─ Supabase.initialize(url, anonKey)
├─ DashboardService.initializeRealtimeListeners() ⚠️
├─ Verificar sesión guardada
├─ runApp(MyApp)
└─ Cargar LoginScreen o MainLayout
MyApp build()
├─ MaterialApp setup
├─ Tema global
├─ Rutas
└─ Home screen según autenticación
Configuración Importante:
Supabase.initialize(
url: 'https://xxxx.supabase.co', // Tu URL
anonKey: 'eyJ0eXAi...', // Tu key
)
// CRÍTICO: Inicializar listeners aquí
DashboardService dashService = DashboardService();
dashService.initializeRealtimeListeners();┌─────────────────────────────────────────────────────────┐
│ INICIO APP │
│ main.dart → Supabase.initialize() │
│ → DashboardService.initializeRealtimeListeners()│
└────────────────────┬────────────────────────────────────┘
│
┌────▼─────────────────┐
│ ¿Usuario logged in? │
└────┬─────────┬───────┘
│ │
Sí ◄─────┘ └────► No
│ │
▼ ▼
MainLayout LoginScreen
│ │
┌────┼────┐ Ingresa Usuario+PIN
│ │ │ │
Home│ Stock │ AuthService.autenticar()
│ │ │ │
▼ ▼ ▼ ¿Válido?
Widgets│ ├─ SÍ: SessionService.guardarSesion()
Stream │ └─ Navega MainLayout
Builder │ └─ NO: SnackBar error
│
REAL-TIME DATA
PostgreSQL <─── Supabase Realtime (WebSocket)
│
DashboardService
│
StreamControllers
inventarioStream
movimientosStream
│
HomeScreen/StockScreen StreamBuilders
│
Actualiza UI + Animaciones
| Archivo | Tipo | Líneas | Propósito |
|---|---|---|---|
| main.dart | Entrada | ~50 | Inicialización y rutas |
| Servicios | |||
| auth_service.dart | Service | ~100 | Autenticación PIN |
| dashboard_service.dart | Service | ~400 | Realtime core ⭐ |
| session_service.dart | Service | ~80 | Sesión local |
| supabase_service.dart | Service | ~100 | Cliente Supabase |
| stock_transfer_service.dart | Service | ~150 | Transferencias |
| Pantallas | |||
| login_screen.dart | Screen | ~180 | Login UI |
| home_screen.dart | Screen | ~350 | Dashboard principal |
| stock_screen.dart | Screen | ~1155 | Gestor inventario |
| main_layout.dart | Screen | ~80 | Navegación principal |
| Widgets | |||
| dashboard_card.dart | Widget | ~50 | Tarjeta métrica |
| stock_transfer_sheet.dart | Widget | ~200 | Modal transferencia |
| realtime_animated_widget.dart | Widget | ~150 | Wrapper realtime |
| animated_count.dart | Widget | ~100 | Contador animado |
| custom_navbar.dart | Widget | ~30 | NavBar (no usado) |
| Modelos | |||
| usuario_model.dart | Model | ~40 | Estructura usuario |
- ✅ Usar
StreamBuilderpara datos dinámicos - ✅ Llamar
dispose()en widgets stateful - ✅ Usar
async/awaitpara operaciones BD - ✅ Validar datos antes de guardar
- ✅ Mostrar loading states
- ✅ Capturar excepciones con try/catch
- ❌ Modificar estado sin setState()
- ❌ Olvidar dispose() de controllers
- ❌ Hacer queries sin error handling
- ❌ Almacenar datos sensibles en SharedPreferences sin encripción
- ❌ Crear múltiples instancias de Supabase
- ❌ Inicializar listeners más de una vez
Documento: Guía de Ficheros - Los Toldos ERP v1.0 Última actualización: 2024