miinventario-v2/orchestration/analisis/ANALISIS-DETALLADO-UX-MOBILE-2026-01-12.md
rckrdmrd 1a53b5c4d3 [MIINVENTARIO] feat: Initial commit - Sistema de inventario con análisis de video IA
- Backend NestJS con módulos de autenticación, inventario, créditos
- Frontend React con dashboard y componentes UI
- Base de datos PostgreSQL con migraciones
- Tests E2E configurados
- Configuración de Docker y deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 02:25:48 -06:00

534 lines
16 KiB
Markdown

# ANÁLISIS DETALLADO - UX MOBILE: Animaciones + Modo Offline
---
id: ANALISIS-DET-UX-MOBILE-001
type: DetailedAnalysis
status: Approved
created_date: 2026-01-12
updated_date: 2026-01-12
phase: 2-detallado
simco_version: "4.0.0"
---
## FASE 2: ANÁLISIS DETALLADO DE CAMBIOS
### 2.1 Archivos Nuevos - Análisis Completo
---
#### 2.1.1 `src/hooks/useAnimations.ts`
**Propósito:** Colección de hooks reutilizables para animaciones con react-native-reanimated
**Líneas de Código:** 187
**Dependencias Externas:** react-native-reanimated
**Exports:**
| Export | Tipo | Parámetros | Retorno | Uso |
|--------|------|------------|---------|-----|
| `useFadeIn` | Hook | `delay?: number` | `{ animatedStyle, opacity }` | Fade in de 0 a 1 |
| `useSlideIn` | Hook | `delay?: number, distance?: number` | `{ animatedStyle, opacity, translateY }` | Slide desde abajo |
| `useSlideFromRight` | Hook | `delay?: number, distance?: number` | `{ animatedStyle, opacity, translateX }` | Slide desde derecha |
| `usePressScale` | Hook | `pressedScale?: number` | `{ animatedStyle, onPressIn, onPressOut, scale }` | Efecto press en botones |
| `useListItemAnimation` | Hook | `index: number, baseDelay?: number` | Return de useSlideIn | Animación stagger en listas |
| `useShimmer` | Hook | - | `{ animatedStyle, shimmerValue }` | Efecto shimmer para skeletons |
| `usePulse` | Hook | `minScale?: number, maxScale?: number` | `{ animatedStyle, scale }` | Animación de pulso |
| `useToggleAnimation` | Hook | `isVisible: boolean` | `{ animatedStyle }` | Entrada/salida de elementos |
| `Animated` | Re-export | - | - | Re-export de reanimated |
**Configuración por Defecto:**
```typescript
const DEFAULT_TIMING: WithTimingConfig = {
duration: 300,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
};
const DEFAULT_SPRING: WithSpringConfig = {
damping: 15,
stiffness: 150,
};
```
**Consumidores:**
- `src/app/(tabs)/index.tsx` (HomeScreen)
- `src/app/(tabs)/inventory.tsx` (InventoryScreen)
- `src/components/ui/Skeleton.tsx`
---
#### 2.1.2 `src/hooks/useNetworkStatus.ts`
**Propósito:** Detectar y monitorear estado de conexión de red
**Líneas de Código:** 73
**Dependencias Externas:** @react-native-community/netinfo
**Exports:**
| Export | Tipo | Parámetros | Retorno | Uso |
|--------|------|------------|---------|-----|
| `useNetworkStatus` | Hook | - | `NetworkStatus` | Estado completo de red |
| `useIsOnline` | Hook | - | `boolean` | ¿Está online? |
| `useIsOffline` | Hook | - | `boolean` | ¿Está offline? |
**Interface NetworkStatus:**
```typescript
interface NetworkStatus {
isConnected: boolean;
isInternetReachable: boolean | null;
type: NetInfoStateType;
isWifi: boolean;
isCellular: boolean;
}
```
**Comportamiento:**
- Suscripción a eventos de NetInfo en useEffect
- Fetch inicial del estado de red
- Cleanup automático en unmount
- Manejo de `isInternetReachable` null (estado indeterminado)
**Consumidores:**
- `src/components/ui/OfflineBanner.tsx`
---
#### 2.1.3 `src/theme/ThemeContext.tsx`
**Propósito:** Sistema de temas con soporte light/dark mode
**Líneas de Código:** 77
**Dependencias Externas:** react (useColorScheme)
**Exports:**
| Export | Tipo | Descripción |
|--------|------|-------------|
| `ThemeColors` | Interface | Definición de colores del tema |
| `Theme` | Interface | `{ colors: ThemeColors, isDark: boolean }` |
| `ThemeProvider` | Component | Provider del contexto |
| `useTheme` | Hook | Acceso al tema completo |
| `useColors` | Hook | Acceso solo a colores |
**Paleta Light:**
```typescript
const lightColors: ThemeColors = {
primary: '#2563eb', // Azul principal
primaryLight: '#f0f9ff', // Azul claro
background: '#f5f5f5', // Gris fondo
card: '#ffffff', // Blanco tarjetas
text: '#1a1a1a', // Negro texto
textSecondary: '#666666',// Gris texto
border: '#e5e5e5', // Gris bordes
error: '#ef4444', // Rojo error
success: '#22c55e', // Verde éxito
warning: '#f59e0b', // Naranja warning
};
```
**Paleta Dark:**
```typescript
const darkColors: ThemeColors = {
primary: '#3b82f6',
primaryLight: '#1e3a5f',
background: '#0f0f0f',
card: '#1a1a1a',
text: '#ffffff',
textSecondary: '#a3a3a3',
border: '#2d2d2d',
error: '#f87171',
success: '#4ade80',
warning: '#fbbf24',
};
```
**Consumidores:**
- `src/app/_layout.tsx` (Provider)
- `src/components/ui/Skeleton.tsx`
- `src/components/ui/AnimatedList.tsx`
- `src/components/skeletons/*.tsx` (4 archivos)
---
#### 2.1.4 `src/components/ui/Skeleton.tsx`
**Propósito:** Componentes skeleton base con animación shimmer
**Líneas de Código:** 216
**Dependencias:**
- react-native-reanimated
- `../../theme/ThemeContext`
**Exports:**
| Export | Tipo | Props | Descripción |
|--------|------|-------|-------------|
| `Skeleton` | Component | `width?, height?, borderRadius?, style?` | Skeleton base |
| `SkeletonText` | Component | `width?, height?, style?` | Línea de texto |
| `SkeletonCircle` | Component | `size?, style?` | Avatar circular |
| `SkeletonImage` | Component | `width?, height?, borderRadius?, style?` | Imagen cuadrada |
| `SkeletonCard` | Component | `style?` | Tarjeta completa |
| `SkeletonListItem` | Component | `style?` | Item de lista |
| `SkeletonStat` | Component | `style?` | Estadística |
| `SkeletonList` | Component | `count?, style?` | Lista de skeletons |
**Animación Shimmer:**
```typescript
shimmerValue.value = withRepeat(
withTiming(1, { duration: 1200 }),
-1, // Infinito
false // Sin reverse
);
// Interpolación de opacidad
opacity: interpolate(shimmerValue.value, [0, 0.5, 1], [0.3, 0.6, 0.3])
```
**Consumidores:**
- `src/app/(tabs)/index.tsx` (HomeSkeleton)
- `src/components/skeletons/InventoryItemSkeleton.tsx`
- `src/components/skeletons/StoreCardSkeleton.tsx`
- `src/components/skeletons/CreditCardSkeleton.tsx`
- `src/components/skeletons/NotificationSkeleton.tsx`
---
#### 2.1.5 `src/components/ui/OfflineBanner.tsx`
**Propósito:** Banner visual que indica pérdida de conexión
**Líneas de Código:** 115
**Dependencias:**
- react-native-reanimated
- react-native-safe-area-context
- `../../hooks/useNetworkStatus`
- @expo/vector-icons
**Exports:**
| Export | Tipo | Props | Descripción |
|--------|------|-------|-------------|
| `OfflineBanner` | Component | `message?, showIcon?` | Banner offline |
| `WithOfflineBanner` | Component | `children` | Wrapper con banner |
**Comportamiento:**
- Posición absoluta top con z-index 9999
- Animación slide desde arriba con spring
- Respeta safe area insets
- Solo renderiza cuando `isOffline === true`
- Color rojo (#EF4444) para indicar problema
**Consumidores:**
- `src/app/_layout.tsx`
---
#### 2.1.6 `src/components/ui/AnimatedList.tsx`
**Propósito:** FlatList con animaciones de entrada staggered
**Líneas de Código:** 155
**Dependencias:**
- react-native-reanimated
- `../../theme/ThemeContext`
**Exports:**
| Export | Tipo | Descripción |
|--------|------|-------------|
| `AnimatedList<T>` | Generic Component | FlatList con animaciones |
| `useListItemEntering` | Hook | Crear entering animation |
| `AnimatedListItem` | Component | Item individual animado |
**Props AnimatedList:**
```typescript
interface AnimatedListProps<T> {
renderItem: (info: { item: T; index: number }) => ReactElement;
staggerDelay?: number; // Default: 50ms
animationType?: 'fade' | 'slide' | 'spring'; // Default: 'fade'
animateOnRefresh?: boolean; // Default: true
onRefresh?: () => Promise<void> | void;
isRefreshing?: boolean;
// ...FlatListProps
}
```
**Tipos de Animación:**
| Tipo | Animación |
|------|-----------|
| `fade` | FadeIn con delay |
| `slide` | SlideInRight con delay |
| `spring` | FadeIn springify |
**Consumidores:**
- Disponible para uso en cualquier pantalla con listas
---
#### 2.1.7-10 Skeletons Específicos
**Archivos:**
- `src/components/skeletons/InventoryItemSkeleton.tsx` (72 líneas)
- `src/components/skeletons/StoreCardSkeleton.tsx` (68 líneas)
- `src/components/skeletons/CreditCardSkeleton.tsx` (115 líneas)
- `src/components/skeletons/NotificationSkeleton.tsx` (62 líneas)
**Exports por Archivo:**
| Archivo | Exports |
|---------|---------|
| InventoryItemSkeleton | `InventoryItemSkeleton`, `InventoryListSkeleton`, `InventoryStatsSkeleton` |
| StoreCardSkeleton | `StoreCardSkeleton`, `StoreListSkeleton` |
| CreditCardSkeleton | `CreditBalanceSkeleton`, `TransactionSkeleton`, `TransactionListSkeleton`, `CreditPackageSkeleton`, `CreditPackageListSkeleton` |
| NotificationSkeleton | `NotificationItemSkeleton`, `NotificationListSkeleton`, `NotificationHeaderSkeleton` |
---
### 2.2 Archivos Modificados - Análisis Completo
---
#### 2.2.1 Stores (4 archivos)
**Patrón de Modificación Común:**
```typescript
// ANTES
export const useStore = create<State>()((set, get) => ({
// ...state
}));
// DESPUÉS
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const useStore = create<State>()(
persist(
(set, get) => ({
// ...state
lastFetched: null, // NUEVO campo
}),
{
name: 'miinventario-{store-name}',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({
// Solo datos, no funciones
}),
}
)
);
```
**Detalle por Store:**
| Store | Nombre Persistencia | Datos Persistidos | Límite |
|-------|---------------------|-------------------|--------|
| `stores.store.ts` | `miinventario-stores` | stores, currentStore, total, lastFetched | Sin límite |
| `inventory.store.ts` | `miinventario-inventory` | items, total, selectedStoreId, lastFetched | 100 items |
| `credits.store.ts` | `miinventario-credits` | balance, transactions, transactionsTotal, lastFetched | 50 tx |
| `notifications.store.ts` | `miinventario-notifications` | notifications, unreadCount, total, lastFetched | 50 notif |
**Campos Nuevos:**
- `lastFetched: number | null` - Timestamp de última carga (para futuro stale-while-revalidate)
---
#### 2.2.2 `src/app/_layout.tsx`
**Cambios Realizados:**
| # | Cambio | Tipo | Impacto |
|---|--------|------|---------|
| 1 | Import `OfflineBanner` | Nuevo import | - |
| 2 | Import `ThemeProvider` | Nuevo import | - |
| 3 | Wrap con `ThemeProvider` | Nuevo wrapper | Tema disponible globalmente |
| 4 | Agregar `<OfflineBanner />` | Nuevo componente | Banner visible globalmente |
| 5 | `animation: 'slide_from_right'` | Nueva opción | Transiciones mejoradas |
| 6 | `animationDuration: 250` | Nueva opción | Duración consistente |
**Jerarquía de Providers:**
```
GestureHandlerRootView
└── ThemeProvider ← NUEVO
└── SafeAreaProvider
└── QueryClientProvider
└── View
├── OfflineBanner ← NUEVO
├── StatusBar
└── Stack (con animación) ← MODIFICADO
```
---
#### 2.2.3 `src/app/(tabs)/index.tsx`
**Cambios Realizados:**
| # | Cambio | Descripción |
|---|--------|-------------|
| 1 | Imports reanimated | `FadeIn, FadeInDown, FadeInRight, Layout` |
| 2 | Import hooks | `useFadeIn, usePressScale` |
| 3 | Import skeletons | `Skeleton, SkeletonText, SkeletonStat` |
| 4 | `AnimatedTouchable` | `Animated.createAnimatedComponent(TouchableOpacity)` |
| 5 | `ActionCard` component | Nuevo componente con animación |
| 6 | `StatCard` component | Nuevo componente con animación |
| 7 | `HomeSkeleton` component | Skeleton para carga inicial |
| 8 | Estado `initialLoad` | Control de skeleton vs contenido |
| 9 | Animaciones en header | `FadeIn.duration(400)` |
| 10 | Animaciones en cards | `FadeInDown`, `FadeInRight` con delays |
**Componentes Nuevos Internos:**
```typescript
function ActionCard({ icon, title, description, onPress, index }) {
const { animatedStyle, onPressIn, onPressOut } = usePressScale(0.98);
return (
<Animated.View entering={FadeInRight.delay(200 + index * 100)}>
<AnimatedTouchable style={[styles.actionCard, animatedStyle]} ...>
...
</AnimatedTouchable>
</Animated.View>
);
}
function StatCard({ value, label, index }) {
return (
<Animated.View entering={FadeInDown.delay(400 + index * 100)}>
...
</Animated.View>
);
}
function HomeSkeleton() {
return (
<View>
<SkeletonText /><Skeleton /><SkeletonStat />...
</View>
);
}
```
---
#### 2.2.4 `src/app/(tabs)/inventory.tsx`
**Cambios Realizados:**
| # | Cambio | Descripción |
|---|--------|-------------|
| 1 | Imports reanimated | `FadeIn, FadeInDown, FadeInRight, FadeOut, Layout` |
| 2 | Import hooks | `usePressScale` |
| 3 | Import skeletons | `InventoryListSkeleton` |
| 4 | `AnimatedTouchable` | Para items de lista |
| 5 | `InventoryItemCard` component | Nuevo componente con animación |
| 6 | Estado `initialLoad` | Control de skeleton vs lista |
| 7 | Animaciones en header | `FadeIn`, `FadeInDown` |
| 8 | Animaciones en items | `FadeInRight` con delay |
| 9 | Animación de salida | `FadeOut` al eliminar |
| 10 | `Layout.springify()` | Animación de reordenamiento |
**Componente InventoryItemCard:**
```typescript
function InventoryItemCard({ item, index }) {
const { animatedStyle, onPressIn, onPressOut } = usePressScale(0.98);
return (
<Animated.View
entering={FadeInRight.delay(Math.min(index * 50, 300)).duration(300)}
exiting={FadeOut.duration(200)}
layout={Layout.springify()}
>
<AnimatedTouchable style={[styles.itemCard, animatedStyle]} ...>
{/* Item content */}
</AnimatedTouchable>
</Animated.View>
);
}
```
---
### 2.3 Resumen de Líneas de Código
| Categoría | Archivos | Líneas Nuevas | Líneas Modificadas |
|-----------|----------|---------------|-------------------|
| Hooks | 2 | 260 | 0 |
| Theme | 1 | 77 | 0 |
| Components/UI | 3 | 486 | 0 |
| Components/Skeletons | 4 | 317 | 0 |
| Stores | 4 | 0 | ~80 (20 por archivo) |
| Screens | 3 | 0 | ~200 |
**Total:** ~1,140 líneas nuevas + ~280 líneas modificadas
---
### 2.4 Validación de Exports/Imports
#### Grafo de Dependencias Internas:
```
ThemeContext.tsx
┌───┴────────────────────────────────────────┐
│ │
Skeleton.tsx ← AnimatedList.tsx │
↓ │
┌───┴───────────────────────┐ │
│ │ │ │ │ │
│ Inventory Store Credit Notification │
│ Skeleton Skeleton Skeleton Skeleton │
└───────────────────────────────────────────┘
useNetworkStatus.ts
OfflineBanner.tsx
_layout.tsx
useAnimations.ts
┌───┴───────────────┐
│ │
(tabs)/index.tsx (tabs)/inventory.tsx
```
#### Validación de Imports:
| Archivo | Imports | ¿Válido? |
|---------|---------|----------|
| `useAnimations.ts` | react, react-native-reanimated | ✅ |
| `useNetworkStatus.ts` | react, @react-native-community/netinfo | ✅ |
| `ThemeContext.tsx` | react, react-native | ✅ |
| `Skeleton.tsx` | react, react-native, react-native-reanimated, ThemeContext | ✅ |
| `OfflineBanner.tsx` | react, react-native, reanimated, safe-area, useNetworkStatus, icons | ✅ |
| `AnimatedList.tsx` | react, react-native, reanimated, ThemeContext | ✅ |
| `_layout.tsx` | expo-router, react-query, etc, OfflineBanner, ThemeContext | ✅ |
---
### 2.5 Conclusión Fase 2
**Estado:** ✅ Análisis Detallado Completado
**Hallazgos:**
1. Todos los archivos nuevos siguen patrones consistentes
2. No hay dependencias circulares
3. Todos los imports son válidos
4. Los stores mantienen compatibilidad hacia atrás
5. Las pantallas mantienen funcionalidad existente + mejoras
**Siguiente Fase:** Planeación de documentación basada en este análisis
---
**Última Actualización:** 2026-01-12
**Responsable:** Claude Opus 4.5