Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
21 KiB
21 KiB
SIMCO: OPERACIONES MOBILE (React Native/TypeScript)
Versión: 1.0.0 Fecha: 2025-12-08 Aplica a: Todo agente que trabaje con código móvil React Native Prioridad: OBLIGATORIA para operaciones mobile
RESUMEN EJECUTIVO
Types alineados + Store configurado + Componentes optimizados + Navegación funcional = Mobile completo.
PRINCIPIO FUNDAMENTAL
╔══════════════════════════════════════════════════════════════════════╗
║ MOBILE-FIRST PERFORMANCE ║
║ ║
║ • Componentes optimizados (memoización) ║
║ • Listas virtualizadas (FlatList) ║
║ • Imágenes optimizadas ║
║ • Offline-first cuando aplique ║
║ • Navegación fluida (<16ms por frame) ║
╚══════════════════════════════════════════════════════════════════════╝
ESTRUCTURA DE PROYECTO
mobile/
├── src/
│ ├── app/ # Entry point y configuración
│ │ ├── App.tsx # Root component
│ │ ├── providers/
│ │ │ ├── index.tsx # Provider composer
│ │ │ ├── QueryProvider.tsx # TanStack Query
│ │ │ └── ThemeProvider.tsx
│ │ └── linking.ts # Deep linking config
│ ├── screens/ # Pantallas
│ │ ├── auth/
│ │ │ ├── LoginScreen.tsx
│ │ │ └── RegisterScreen.tsx
│ │ ├── home/
│ │ │ └── HomeScreen.tsx
│ │ └── {module}/
│ │ ├── {Module}Screen.tsx
│ │ └── {Module}DetailScreen.tsx
│ ├── components/ # Componentes
│ │ ├── common/ # Compartidos
│ │ │ ├── Button.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── Card.tsx
│ │ │ └── Loading.tsx
│ │ ├── forms/
│ │ │ └── FormField.tsx
│ │ └── {module}/
│ │ └── {Component}.tsx
│ ├── navigation/ # Navegación
│ │ ├── RootNavigator.tsx # Navigator principal
│ │ ├── AuthNavigator.tsx
│ │ ├── MainNavigator.tsx
│ │ └── types.ts # Navigation types
│ ├── hooks/ # Hooks personalizados
│ │ ├── useAuth.ts
│ │ ├── useApi.ts
│ │ └── use{Module}.ts
│ ├── services/ # API services
│ │ ├── api.ts # Axios instance
│ │ ├── auth.service.ts
│ │ └── {module}.service.ts
│ ├── stores/ # State management
│ │ ├── auth.store.ts
│ │ └── {module}.store.ts
│ ├── types/ # TypeScript types
│ │ ├── index.ts
│ │ ├── api.types.ts
│ │ └── {module}.types.ts
│ ├── utils/ # Utilidades
│ │ ├── storage.ts # AsyncStorage helpers
│ │ ├── validation.ts
│ │ └── formatting.ts
│ ├── constants/ # Constantes
│ │ ├── colors.ts
│ │ ├── spacing.ts
│ │ └── config.ts
│ └── theme/ # Tema global
│ ├── index.ts
│ ├── colors.ts
│ └── typography.ts
├── android/ # Proyecto Android nativo
├── ios/ # Proyecto iOS nativo
├── __tests__/
├── app.json
├── metro.config.js
├── babel.config.js
├── tsconfig.json
└── package.json
CONVENCIONES DE NOMENCLATURA
Archivos
// Screens: PascalCase + Screen suffix
HomeScreen.tsx
ProductDetailScreen.tsx
UserProfileScreen.tsx
// Components: PascalCase
Button.tsx
ProductCard.tsx
UserAvatar.tsx
// Hooks: camelCase con prefijo use
useAuth.ts
useProducts.ts
useNavigation.ts
// Services: camelCase + .service suffix
auth.service.ts
products.service.ts
// Stores: camelCase + .store suffix
auth.store.ts
cart.store.ts
// Types: camelCase + .types suffix
user.types.ts
product.types.ts
Clases y Tipos
// Interfaces: PascalCase con prefijo I (opcional)
interface User {}
interface IAuthState {}
// Types: PascalCase
type NavigationProps = {}
type ThemeColors = {}
// Enums: PascalCase
enum OrderStatus {
PENDING = 'pending',
COMPLETED = 'completed',
}
Funciones y Variables
// Componentes: PascalCase
const ProductCard: React.FC<Props> = () => {}
// Funciones: camelCase con verbo
const fetchProducts = async () => {}
const handleSubmit = () => {}
const formatPrice = (price: number) => {}
// Variables: camelCase
const isLoading = true
const productList = []
const currentUser = null
// Constantes: UPPER_SNAKE_CASE
const API_URL = ''
const MAX_ITEMS = 50
TEMPLATES
Screen Component
// screens/{module}/{Module}Screen.tsx
import React, { useCallback, useMemo } from 'react';
import { View, StyleSheet, FlatList, RefreshControl } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '@/navigation/types';
import { useProducts } from '@/hooks/useProducts';
import { ProductCard, Loading, EmptyState } from '@/components';
import { Product } from '@/types';
type Props = NativeStackScreenProps<RootStackParamList, 'Products'>;
/**
* ProductsScreen - Lista de productos
*
* Muestra productos con pull-to-refresh y navegación a detalle.
*/
export const ProductsScreen: React.FC<Props> = ({ navigation }) => {
const { products, isLoading, isRefreshing, refresh } = useProducts();
const handleProductPress = useCallback((product: Product) => {
navigation.navigate('ProductDetail', { productId: product.id });
}, [navigation]);
const renderItem = useCallback(({ item }: { item: Product }) => (
<ProductCard
product={item}
onPress={() => handleProductPress(item)}
/>
), [handleProductPress]);
const keyExtractor = useCallback((item: Product) => item.id, []);
const ListEmptyComponent = useMemo(() => (
<EmptyState
title="No hay productos"
message="Intenta de nuevo más tarde"
/>
), []);
if (isLoading && !products.length) {
return <Loading />;
}
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<FlatList
data={products}
renderItem={renderItem}
keyExtractor={keyExtractor}
contentContainerStyle={styles.list}
ListEmptyComponent={ListEmptyComponent}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={refresh}
/>
}
// Optimizaciones de rendimiento
removeClippedSubviews
maxToRenderPerBatch={10}
windowSize={5}
initialNumToRender={10}
getItemLayout={(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
</SafeAreaView>
);
};
const ITEM_HEIGHT = 120;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
list: {
padding: 16,
gap: 12,
},
});
Component (Reusable)
// components/common/Button.tsx
import React, { memo } from 'react';
import {
TouchableOpacity,
Text,
StyleSheet,
ActivityIndicator,
ViewStyle,
TextStyle,
} from 'react-native';
import { colors, spacing } from '@/theme';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
}
/**
* Button - Botón reutilizable
*
* @example
* <Button
* title="Submit"
* onPress={handleSubmit}
* variant="primary"
* loading={isSubmitting}
* />
*/
export const Button = memo<ButtonProps>(({
title,
onPress,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
style,
textStyle,
}) => {
const isDisabled = disabled || loading;
return (
<TouchableOpacity
onPress={onPress}
disabled={isDisabled}
style={[
styles.base,
styles[variant],
styles[size],
isDisabled && styles.disabled,
style,
]}
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator
color={variant === 'outline' ? colors.primary : colors.white}
/>
) : (
<Text
style={[
styles.text,
styles[`${variant}Text`],
styles[`${size}Text`],
textStyle,
]}
>
{title}
</Text>
)}
</TouchableOpacity>
);
});
Button.displayName = 'Button';
const styles = StyleSheet.create({
base: {
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
primary: {
backgroundColor: colors.primary,
},
secondary: {
backgroundColor: colors.secondary,
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: colors.primary,
},
small: {
paddingVertical: spacing.xs,
paddingHorizontal: spacing.sm,
},
medium: {
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
},
large: {
paddingVertical: spacing.md,
paddingHorizontal: spacing.lg,
},
disabled: {
opacity: 0.5,
},
text: {
fontWeight: '600',
},
primaryText: {
color: colors.white,
},
secondaryText: {
color: colors.white,
},
outlineText: {
color: colors.primary,
},
smallText: {
fontSize: 14,
},
mediumText: {
fontSize: 16,
},
largeText: {
fontSize: 18,
},
});
Navigation Setup
// navigation/RootNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuthStore } from '@/stores/auth.store';
import { AuthNavigator } from './AuthNavigator';
import { MainNavigator } from './MainNavigator';
import { RootStackParamList } from './types';
import { linking } from '@/app/linking';
const Stack = createNativeStackNavigator<RootStackParamList>();
export const RootNavigator: React.FC = () => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
return (
<NavigationContainer linking={linking}>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<Stack.Screen name="Main" component={MainNavigator} />
) : (
<Stack.Screen name="Auth" component={AuthNavigator} />
)}
</Stack.Navigator>
</NavigationContainer>
);
};
// navigation/types.ts
import { NativeStackScreenProps } from '@react-navigation/native-stack';
export type RootStackParamList = {
Auth: undefined;
Main: undefined;
ProductDetail: { productId: string };
};
export type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
Store (Zustand)
// stores/auth.store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { authService } from '@/services/auth.service';
import { User, LoginDto, RegisterDto } from '@/types';
interface AuthState {
// State
token: string | null;
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
// Actions
login: (credentials: LoginDto) => Promise<void>;
register: (data: RegisterDto) => Promise<void>;
logout: () => void;
refreshUser: () => Promise<void>;
clearError: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
// Initial state
token: null,
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
// Actions
login: async (credentials) => {
set({ isLoading: true, error: null });
try {
const response = await authService.login(credentials);
set({
token: response.token,
user: response.user,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Login failed',
isLoading: false,
});
throw error;
}
},
register: async (data) => {
set({ isLoading: true, error: null });
try {
const response = await authService.register(data);
set({
token: response.token,
user: response.user,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Registration failed',
isLoading: false,
});
throw error;
}
},
logout: () => {
set({
token: null,
user: null,
isAuthenticated: false,
error: null,
});
},
refreshUser: async () => {
const { token } = get();
if (!token) return;
try {
const user = await authService.getProfile();
set({ user });
} catch {
// Token might be expired
get().logout();
}
},
clearError: () => set({ error: null }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({
token: state.token,
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
}
)
);
API Service
// services/api.ts
import axios, { AxiosInstance, AxiosError } from 'axios';
import { useAuthStore } from '@/stores/auth.store';
import { API_URL } from '@/constants/config';
const api: AxiosInstance = axios.create({
baseURL: API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor - add auth token
api.interceptors.request.use(
(config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - handle errors
api.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
export default api;
// services/products.service.ts
import api from './api';
import { Product, CreateProductDto } from '@/types';
export const productsService = {
getAll: async (): Promise<Product[]> => {
const { data } = await api.get('/products');
return data;
},
getById: async (id: string): Promise<Product> => {
const { data } = await api.get(`/products/${id}`);
return data;
},
create: async (dto: CreateProductDto): Promise<Product> => {
const { data } = await api.post('/products', dto);
return data;
},
update: async (id: string, dto: Partial<CreateProductDto>): Promise<Product> => {
const { data } = await api.put(`/products/${id}`, dto);
return data;
},
delete: async (id: string): Promise<void> => {
await api.delete(`/products/${id}`);
},
};
Custom Hook
// hooks/useProducts.ts
import { useCallback, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { productsService } from '@/services/products.service';
import { Product, CreateProductDto } from '@/types';
export const useProducts = () => {
const queryClient = useQueryClient();
const {
data: products = [],
isLoading,
isRefetching,
refetch,
} = useQuery({
queryKey: ['products'],
queryFn: productsService.getAll,
});
const createMutation = useMutation({
mutationFn: productsService.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<CreateProductDto> }) =>
productsService.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
const deleteMutation = useMutation({
mutationFn: productsService.delete,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
const refresh = useCallback(() => {
refetch();
}, [refetch]);
return {
products,
isLoading,
isRefreshing: isRefetching,
refresh,
createProduct: createMutation.mutateAsync,
updateProduct: updateMutation.mutateAsync,
deleteProduct: deleteMutation.mutateAsync,
isCreating: createMutation.isPending,
isUpdating: updateMutation.isPending,
isDeleting: deleteMutation.isPending,
};
};
export const useProduct = (id: string) => {
return useQuery({
queryKey: ['products', id],
queryFn: () => productsService.getById(id),
enabled: !!id,
});
};
VALIDACIONES OBLIGATORIAS
# 1. TypeScript (OBLIGATORIO)
npx tsc --noEmit
# ✅ Sin errores de tipos
# 2. Lint (OBLIGATORIO)
npm run lint
# ✅ Sin errores
# 3. Tests
npm run test
# ✅ Deben pasar
# 4. iOS (Mac required)
cd ios && pod install && cd ..
npx react-native run-ios
# ✅ Debe compilar e iniciar
# 5. Android
npx react-native run-android
# ✅ Debe compilar e iniciar
# 6. Verificar en dispositivo/emulador
# ✅ Sin errores en consola
# ✅ Navegación funcional
# ✅ API responde
# ✅ Sin memory leaks
OPTIMIZACIONES DE RENDIMIENTO
FlatList Optimizada
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={keyExtractor}
// Optimizaciones críticas
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={5}
initialNumToRender={10}
getItemLayout={(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
Memoización
// Componentes con memo
export const ProductCard = memo<ProductCardProps>(({ product, onPress }) => {
// ...
});
// useCallback para handlers
const handlePress = useCallback(() => {
onPress(item);
}, [onPress, item]);
// useMemo para cálculos costosos
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => a.price - b.price);
}, [products]);
Imágenes Optimizadas
import FastImage from 'react-native-fast-image';
<FastImage
source={{ uri: imageUrl, priority: FastImage.priority.normal }}
style={styles.image}
resizeMode={FastImage.resizeMode.cover}
/>
CHECKLIST MOBILE
SCREEN
├── [ ] SafeAreaView para notches
├── [ ] Manejo de loading state
├── [ ] Manejo de error state
├── [ ] Manejo de empty state
├── [ ] Pull-to-refresh si aplica
├── [ ] Keyboard avoiding si tiene inputs
└── [ ] TypeScript types correctos
COMPONENTE
├── [ ] memo() si recibe props estables
├── [ ] useCallback para handlers
├── [ ] StyleSheet fuera del componente
├── [ ] Props tipadas con interface
└── [ ] displayName para debugging
NAVEGACIÓN
├── [ ] Types para params definidos
├── [ ] Deep linking configurado si aplica
├── [ ] Back button handling
└── [ ] Transiciones fluidas
STORE
├── [ ] Persist configurado si necesario
├── [ ] Actions tipadas
├── [ ] Selectors optimizados
└── [ ] Reset on logout
API
├── [ ] Interceptors de auth
├── [ ] Manejo de 401
├── [ ] Timeout configurado
└── [ ] Error handling
VALIDACIÓN
├── [ ] TypeScript sin errores
├── [ ] Lint sin errores
├── [ ] iOS compila
├── [ ] Android compila
└── [ ] Tests pasan
ERRORES COMUNES
| Error | Causa | Solución |
|---|---|---|
| Re-renders excesivos | Callbacks inline | useCallback, memo |
| Lista lenta | FlatList sin optimizar | getItemLayout, windowSize |
| Memory leak | Subscriptions no limpias | Cleanup en useEffect |
| Keyboard cubre input | No KeyboardAvoidingView | Agregar wrapper |
| Flash en navigation | Estado inicial incorrecto | Hydration correcta |
REFERENCIAS
- Crear archivos: @CREAR (SIMCO-CREAR.md)
- Validar: @VALIDAR (SIMCO-VALIDAR.md)
- Backend: @BACKEND (SIMCO-BACKEND.md)
- Perfil Mobile: @PERFILES/PERFIL-MOBILE-AGENT.md
Versión: 1.0.0 | Sistema: SIMCO | Mantenido por: Tech Lead