diff --git a/.env.example b/.env.example deleted file mode 100644 index fa383c9..0000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# API -EXPO_PUBLIC_API_URL=http://localhost:3142/api/v1 - -# Environment -EXPO_PUBLIC_ENV=development diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 119d533..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,47 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 2021, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, - plugins: ['@typescript-eslint', 'react', 'react-hooks'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - ], - env: { - browser: true, - es2021: true, - node: true, - jest: true, - }, - settings: { - react: { - version: 'detect', - }, - }, - ignorePatterns: [ - '.eslintrc.js', - 'babel.config.js', - 'metro.config.js', - 'node_modules', - '.expo', - ], - rules: { - 'react/react-in-jsx-scope': 'off', - 'react/prop-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': [ - 'error', - { argsIgnorePattern: '^_' }, - ], - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', - 'no-console': ['warn', { allow: ['warn', 'error'] }], - }, -}; diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 0063d2c..0000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "singleQuote": true, - "trailingComma": "all", - "printWidth": 80, - "tabWidth": 2, - "semi": true, - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf", - "jsxSingleQuote": false -} diff --git a/app.json b/app.json deleted file mode 100644 index 5971906..0000000 --- a/app.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "expo": { - "name": "MiInventario", - "slug": "miinventario", - "version": "0.1.0", - "orientation": "portrait", - "icon": "./src/assets/icon.png", - "userInterfaceStyle": "light", - "splash": { - "image": "./src/assets/splash.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, - "assetBundlePatterns": [ - "**/*" - ], - "ios": { - "supportsTablet": true, - "bundleIdentifier": "com.miinventario.app", - "infoPlist": { - "NSCameraUsageDescription": "MiInventario necesita acceso a la camara para grabar videos de tus anaqueles y generar inventario automatico.", - "NSMicrophoneUsageDescription": "MiInventario necesita acceso al microfono para grabar videos." - } - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./src/assets/adaptive-icon.png", - "backgroundColor": "#ffffff" - }, - "package": "com.miinventario.app", - "permissions": [ - "android.permission.CAMERA", - "android.permission.RECORD_AUDIO" - ] - }, - "web": { - "bundler": "metro", - "output": "static", - "favicon": "./src/assets/favicon.png" - }, - "plugins": [ - "expo-router", - [ - "expo-camera", - { - "cameraPermission": "Permite acceso a la camara para escanear inventario." - } - ] - ], - "experiments": { - "typedRoutes": true - }, - "scheme": "miinventario" - } -} diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index eccb337..0000000 --- a/babel.config.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = function (api) { - api.cache(true); - return { - presets: ['babel-preset-expo'], - plugins: [ - 'react-native-reanimated/plugin', - [ - 'module-resolver', - { - root: ['./src'], - alias: { - '@': './src', - '@screens': './src/screens', - '@components': './src/components', - '@hooks': './src/hooks', - '@stores': './src/stores', - '@services': './src/services', - '@utils': './src/utils', - '@types': './src/types', - }, - }, - ], - ], - }; -}; diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 3e223b1..0000000 --- a/jest.config.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = { - preset: 'react-native', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$', - transformIgnorePatterns: [ - 'node_modules/(?!(react-native|@react-native|expo|@expo|expo-.*|@react-native-async-storage|zustand|react-native-.*|@react-navigation)/)', - ], - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - '^@services/(.*)$': '/src/services/$1', - '^@stores/(.*)$': '/src/stores/$1', - '^@components/(.*)$': '/src/components/$1', - '^@hooks/(.*)$': '/src/hooks/$1', - '^@utils/(.*)$': '/src/utils/$1', - '^@theme/(.*)$': '/src/theme/$1', - '^@types/(.*)$': '/src/types/$1', - }, - setupFilesAfterEnv: ['/jest.setup.js'], - testEnvironment: 'node', - collectCoverageFrom: [ - 'src/stores/**/*.{ts,tsx}', - 'src/services/api/**/*.{ts,tsx}', - '!src/**/*.d.ts', - '!src/**/__tests__/**', - '!src/**/__mocks__/**', - ], - coverageThreshold: { - global: { - branches: 70, - functions: 70, - lines: 70, - statements: 70, - }, - }, - coverageReporters: ['text', 'lcov', 'html'], - reporters: ['default', 'jest-junit'], -}; diff --git a/jest.setup.js b/jest.setup.js deleted file mode 100644 index fd10c1d..0000000 --- a/jest.setup.js +++ /dev/null @@ -1,71 +0,0 @@ -// Mock expo-secure-store -jest.mock('expo-secure-store', () => ({ - getItemAsync: jest.fn(() => Promise.resolve(null)), - setItemAsync: jest.fn(() => Promise.resolve()), - deleteItemAsync: jest.fn(() => Promise.resolve()), -})); - -// Mock expo-router -jest.mock('expo-router', () => ({ - useRouter: jest.fn(() => ({ - push: jest.fn(), - replace: jest.fn(), - back: jest.fn(), - })), - useLocalSearchParams: jest.fn(() => ({})), - usePathname: jest.fn(() => '/'), - useSegments: jest.fn(() => []), - Stack: { - Screen: jest.fn(() => null), - }, - Tabs: { - Screen: jest.fn(() => null), - }, - Link: jest.fn(() => null), -})); - -// Mock @react-native-async-storage/async-storage -jest.mock('@react-native-async-storage/async-storage', () => ({ - default: { - getItem: jest.fn(() => Promise.resolve(null)), - setItem: jest.fn(() => Promise.resolve()), - removeItem: jest.fn(() => Promise.resolve()), - clear: jest.fn(() => Promise.resolve()), - getAllKeys: jest.fn(() => Promise.resolve([])), - }, -})); - -// Mock react-native-reanimated -jest.mock('react-native-reanimated', () => { - const Reanimated = require('react-native-reanimated/mock'); - Reanimated.default.call = () => {}; - return Reanimated; -}); - -// Mock @react-native-community/netinfo -jest.mock('@react-native-community/netinfo', () => ({ - addEventListener: jest.fn(() => jest.fn()), - fetch: jest.fn(() => Promise.resolve({ isConnected: true })), -})); - -// Global fetch mock -global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve({}), - ok: true, - status: 200, - }) -); - -// Console error suppression for known issues -const originalError = console.error; -console.error = (...args) => { - if ( - typeof args[0] === 'string' && - (args[0].includes('Warning: ReactDOM.render') || - args[0].includes('Warning: An update to')) - ) { - return; - } - originalError.call(console, ...args); -}; diff --git a/package.json b/package.json deleted file mode 100644 index b3b9e67..0000000 --- a/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@miinventario/mobile", - "version": "0.1.0", - "private": true, - "main": "expo-router/entry", - "scripts": { - "start": "expo start", - "start:dev": "expo start --dev-client", - "android": "expo run:android", - "ios": "expo run:ios", - "web": "expo start --web", - "lint": "eslint . --ext .ts,.tsx", - "format": "prettier --write \"src/**/*.{ts,tsx}\"", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "test:ci": "jest --ci --coverage --reporters=default --reporters=jest-junit" - }, - "dependencies": { - "@hookform/resolvers": "^3.3.0", - "@react-native-async-storage/async-storage": "1.21.0", - "@react-native-community/netinfo": "11.1.0", - "@react-navigation/bottom-tabs": "^6.5.0", - "@react-navigation/native": "^6.1.0", - "@react-navigation/native-stack": "^6.9.0", - "@tanstack/react-query": "^5.0.0", - "axios": "^1.6.0", - "expo": "~50.0.0", - "expo-av": "~13.10.0", - "expo-camera": "~14.0.0", - "expo-clipboard": "^8.0.8", - "expo-file-system": "~16.0.0", - "expo-image-picker": "~14.7.0", - "expo-router": "~3.4.0", - "expo-secure-store": "~12.8.0", - "expo-splash-screen": "~0.26.0", - "expo-status-bar": "~1.11.0", - "react": "18.2.0", - "react-hook-form": "^7.48.0", - "react-native": "0.73.0", - "react-native-gesture-handler": "~2.14.0", - "react-native-reanimated": "~3.6.0", - "react-native-safe-area-context": "4.8.2", - "react-native-screens": "~3.29.0", - "zod": "^3.22.0", - "zustand": "^4.4.0" - }, - "devDependencies": { - "@babel/core": "^7.20.0", - "@testing-library/react-native": "^12.0.0", - "@types/react": "~18.2.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "eslint-plugin-react": "^7.32.0", - "eslint-plugin-react-hooks": "^4.6.0", - "jest": "^29.5.0", - "jest-junit": "^16.0.0", - "prettier": "^3.0.0", - "react-test-renderer": "18.2.0", - "typescript": "^5.1.0" - } -} diff --git a/src/__mocks__/apiClient.mock.ts b/src/__mocks__/apiClient.mock.ts deleted file mode 100644 index 0a8d67d..0000000 --- a/src/__mocks__/apiClient.mock.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { jest } from '@jest/globals'; - -export const mockApiClient = { - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), - patch: jest.fn(), - delete: jest.fn(), - interceptors: { - request: { - use: jest.fn(), - }, - response: { - use: jest.fn(), - }, - }, -}; - -export const resetApiClientMocks = () => { - mockApiClient.get.mockReset(); - mockApiClient.post.mockReset(); - mockApiClient.put.mockReset(); - mockApiClient.patch.mockReset(); - mockApiClient.delete.mockReset(); -}; - -export const mockApiResponse = (data: T) => ({ - data, - status: 200, - statusText: 'OK', - headers: {}, - config: {}, -}); - -export const mockApiError = ( - message: string, - status = 400, - data: unknown = {} -) => { - const error = new Error(message) as Error & { - response: { data: unknown; status: number }; - isAxiosError: boolean; - }; - error.response = { data, status }; - error.isAxiosError = true; - return error; -}; - -export default mockApiClient; diff --git a/src/app/(auth)/_layout.tsx b/src/app/(auth)/_layout.tsx deleted file mode 100644 index 7b5288c..0000000 --- a/src/app/(auth)/_layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Stack } from 'expo-router'; - -export default function AuthLayout() { - return ( - - - - - - ); -} diff --git a/src/app/(auth)/login.tsx b/src/app/(auth)/login.tsx deleted file mode 100644 index 7260433..0000000 --- a/src/app/(auth)/login.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { View, Text, StyleSheet, TouchableOpacity, TextInput } from 'react-native'; -import { Link, router } from 'expo-router'; -import { useState } from 'react'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useAuthStore } from '@stores/auth.store'; - -export default function LoginScreen() { - const [phone, setPhone] = useState(''); - const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); - const { login } = useAuthStore(); - - const handleLogin = async () => { - if (!phone || !password) return; - - setLoading(true); - try { - await login(phone, password); - router.replace('/(tabs)'); - } catch (error) { - console.error('Login error:', error); - } finally { - setLoading(false); - } - }; - - return ( - - - MiInventario - Inicia sesion para continuar - - - - - - - - {loading ? 'Iniciando...' : 'Iniciar Sesion'} - - - - - - No tienes cuenta? - - - Registrate - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - }, - content: { - flex: 1, - padding: 24, - justifyContent: 'center', - }, - title: { - fontSize: 32, - fontWeight: 'bold', - textAlign: 'center', - marginBottom: 8, - color: '#1a1a1a', - }, - subtitle: { - fontSize: 16, - textAlign: 'center', - marginBottom: 32, - color: '#666', - }, - form: { - gap: 16, - }, - input: { - borderWidth: 1, - borderColor: '#ddd', - borderRadius: 12, - padding: 16, - fontSize: 16, - }, - button: { - backgroundColor: '#2563eb', - padding: 16, - borderRadius: 12, - alignItems: 'center', - marginTop: 8, - }, - buttonDisabled: { - opacity: 0.6, - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - footer: { - flexDirection: 'row', - justifyContent: 'center', - marginTop: 24, - }, - footerText: { - color: '#666', - }, - link: { - color: '#2563eb', - fontWeight: '600', - }, -}); diff --git a/src/app/(auth)/register.tsx b/src/app/(auth)/register.tsx deleted file mode 100644 index 799d6f9..0000000 --- a/src/app/(auth)/register.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { View, Text, StyleSheet, TouchableOpacity, TextInput } from 'react-native'; -import { Link, router } from 'expo-router'; -import { useState } from 'react'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useAuthStore } from '@stores/auth.store'; - -export default function RegisterScreen() { - const [phone, setPhone] = useState(''); - const [name, setName] = useState(''); - const [loading, setLoading] = useState(false); - const { initiateRegistration } = useAuthStore(); - - const handleRegister = async () => { - if (!phone || !name) return; - - setLoading(true); - try { - await initiateRegistration(phone, name); - router.push({ - pathname: '/(auth)/verify-otp', - params: { phone }, - }); - } catch (error) { - console.error('Registration error:', error); - } finally { - setLoading(false); - } - }; - - return ( - - - Crear Cuenta - Ingresa tus datos para registrarte - - - - - - - - {loading ? 'Enviando...' : 'Continuar'} - - - - - - Ya tienes cuenta? - - - Inicia Sesion - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - }, - content: { - flex: 1, - padding: 24, - justifyContent: 'center', - }, - title: { - fontSize: 32, - fontWeight: 'bold', - textAlign: 'center', - marginBottom: 8, - color: '#1a1a1a', - }, - subtitle: { - fontSize: 16, - textAlign: 'center', - marginBottom: 32, - color: '#666', - }, - form: { - gap: 16, - }, - input: { - borderWidth: 1, - borderColor: '#ddd', - borderRadius: 12, - padding: 16, - fontSize: 16, - }, - button: { - backgroundColor: '#2563eb', - padding: 16, - borderRadius: 12, - alignItems: 'center', - marginTop: 8, - }, - buttonDisabled: { - opacity: 0.6, - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - footer: { - flexDirection: 'row', - justifyContent: 'center', - marginTop: 24, - }, - footerText: { - color: '#666', - }, - link: { - color: '#2563eb', - fontWeight: '600', - }, -}); diff --git a/src/app/(auth)/verify-otp.tsx b/src/app/(auth)/verify-otp.tsx deleted file mode 100644 index 8b987ee..0000000 --- a/src/app/(auth)/verify-otp.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { View, Text, StyleSheet, TouchableOpacity, TextInput } from 'react-native'; -import { router, useLocalSearchParams } from 'expo-router'; -import { useState } from 'react'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useAuthStore } from '@stores/auth.store'; - -export default function VerifyOtpScreen() { - const { phone } = useLocalSearchParams<{ phone: string }>(); - const [otp, setOtp] = useState(''); - const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); - const { verifyOtp } = useAuthStore(); - - const handleVerify = async () => { - if (!otp || !password || !phone) return; - - setLoading(true); - try { - await verifyOtp(phone, otp, password); - router.replace('/(tabs)'); - } catch (error) { - console.error('Verification error:', error); - } finally { - setLoading(false); - } - }; - - return ( - - - Verificar Codigo - - Ingresa el codigo enviado a {phone} - - - - - - - - - {loading ? 'Verificando...' : 'Verificar y Crear Cuenta'} - - - - - - Reenviar codigo - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - }, - content: { - flex: 1, - padding: 24, - justifyContent: 'center', - }, - title: { - fontSize: 32, - fontWeight: 'bold', - textAlign: 'center', - marginBottom: 8, - color: '#1a1a1a', - }, - subtitle: { - fontSize: 16, - textAlign: 'center', - marginBottom: 32, - color: '#666', - }, - form: { - gap: 16, - }, - input: { - borderWidth: 1, - borderColor: '#ddd', - borderRadius: 12, - padding: 16, - fontSize: 16, - textAlign: 'center', - }, - button: { - backgroundColor: '#2563eb', - padding: 16, - borderRadius: 12, - alignItems: 'center', - marginTop: 8, - }, - buttonDisabled: { - opacity: 0.6, - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - resendButton: { - alignItems: 'center', - marginTop: 24, - }, - resendText: { - color: '#2563eb', - fontSize: 14, - }, -}); diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx deleted file mode 100644 index 768e66b..0000000 --- a/src/app/(tabs)/_layout.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Tabs } from 'expo-router'; -import { Text } from 'react-native'; - -export default function TabsLayout() { - return ( - - 🏠, - }} - /> - 📷, - }} - /> - 📦, - }} - /> - 👤, - }} - /> - - ); -} diff --git a/src/app/(tabs)/index.tsx b/src/app/(tabs)/index.tsx deleted file mode 100644 index 776dbb9..0000000 --- a/src/app/(tabs)/index.tsx +++ /dev/null @@ -1,542 +0,0 @@ -import { View, Text, StyleSheet, ScrollView, TouchableOpacity, RefreshControl } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { router } from 'expo-router'; -import { useEffect, useCallback, useState } from 'react'; -import Animated, { FadeIn, FadeInDown, FadeInRight, Layout } from 'react-native-reanimated'; -import { useAuthStore } from '@stores/auth.store'; -import { useCreditsStore } from '@stores/credits.store'; -import { useStoresStore } from '@stores/stores.store'; -import { useInventoryStore } from '@stores/inventory.store'; -import { useNotificationsStore } from '@stores/notifications.store'; -import { useFadeIn, usePressScale } from '../../hooks/useAnimations'; -import { Skeleton, SkeletonText, SkeletonStat } from '../../components/ui/Skeleton'; - -const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity); - -function ActionCard({ - icon, - title, - description, - onPress, - index, -}: { - icon: string; - title: string; - description: string; - onPress: () => void; - index: number; -}) { - const { animatedStyle, onPressIn, onPressOut } = usePressScale(0.98); - - return ( - - - - {icon} - - - {title} - {description} - - - - - ); -} - -function StatCard({ - value, - label, - index, -}: { - value: number; - label: string; - index: number; -}) { - return ( - - {value} - {label} - - ); -} - -function HomeSkeleton() { - return ( - - {/* Header Skeleton */} - - - - - - - - {/* Credits Card Skeleton */} - - - {/* Actions Skeleton */} - - - - - - {/* Stats Skeleton */} - - - - - - - - ); -} - -export default function HomeScreen() { - const { user } = useAuthStore(); - const { balance, fetchBalance, isLoading: creditsLoading } = useCreditsStore(); - const { stores, currentStore, fetchStores, isLoading: storesLoading } = useStoresStore(); - const { items, fetchItems, isLoading: inventoryLoading } = useInventoryStore(); - const { unreadCount, fetchUnreadCount } = useNotificationsStore(); - const [refreshing, setRefreshing] = useState(false); - const [initialLoad, setInitialLoad] = useState(true); - - const loadData = useCallback(async () => { - await Promise.all([ - fetchBalance(), - fetchStores(true), - fetchUnreadCount(), - ]); - setInitialLoad(false); - }, [fetchBalance, fetchStores, fetchUnreadCount]); - - useEffect(() => { - loadData(); - }, [loadData]); - - useEffect(() => { - if (currentStore) { - fetchItems(currentStore.id, true); - } - }, [currentStore, fetchItems]); - - const onRefresh = async () => { - setRefreshing(true); - await loadData(); - setRefreshing(false); - }; - - const isLoading = initialLoad && (creditsLoading || storesLoading); - - if (isLoading) { - return ( - - - - - - ); - } - - return ( - - - } - > - {/* Header */} - - - - Hola, {user?.name || 'Usuario'} - - {currentStore ? currentStore.name : 'Selecciona una tienda'} - - - router.push('/notifications')} - > - 🔔 - {unreadCount > 0 && ( - - - {unreadCount > 9 ? '9+' : unreadCount} - - - )} - - - - - {/* Credits Card */} - - - Creditos disponibles - router.push('/credits/history')}> - Ver historial - - - {balance?.balance ?? 0} - router.push('/credits/buy')} - > - Comprar Creditos - - - - {/* Store Selector */} - {stores.length > 1 && ( - - Tienda Activa - - {stores.map((store, index) => ( - - useStoresStore.getState().selectStore(store)} - > - - {store.name} - - - - ))} - - - )} - - {/* Quick Actions */} - - - Acciones Rapidas - - - router.push('/(tabs)/scan')} - index={0} - /> - - router.push('/(tabs)/inventory')} - index={1} - /> - - router.push('/referrals')} - index={2} - /> - - - {/* Stats */} - - - Resumen - - - - - - - - - {/* Low Stock Alert */} - {items.filter(i => i.quantity < 5).length > 0 && ( - - ⚠️ - - Stock Bajo - - {items.filter(i => i.quantity < 5).length} productos con menos de 5 unidades - - - router.push('/(tabs)/inventory?filter=low-stock')}> - Ver - - - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - paddingBottom: 32, - }, - header: { - marginBottom: 20, - }, - headerTop: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - greeting: { - fontSize: 28, - fontWeight: 'bold', - color: '#1a1a1a', - }, - subtitle: { - fontSize: 16, - color: '#666', - marginTop: 4, - }, - notificationButton: { - position: 'relative', - padding: 8, - }, - notificationIcon: { - fontSize: 24, - }, - notificationBadge: { - position: 'absolute', - top: 4, - right: 4, - backgroundColor: '#ef4444', - borderRadius: 10, - minWidth: 18, - height: 18, - justifyContent: 'center', - alignItems: 'center', - paddingHorizontal: 4, - }, - notificationBadgeText: { - color: '#fff', - fontSize: 10, - fontWeight: 'bold', - }, - creditsCard: { - backgroundColor: '#2563eb', - borderRadius: 16, - padding: 20, - marginBottom: 20, - }, - creditsHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - creditsLabel: { - color: 'rgba(255,255,255,0.8)', - fontSize: 14, - }, - creditsHistoryLink: { - color: 'rgba(255,255,255,0.8)', - fontSize: 14, - textDecorationLine: 'underline', - }, - creditsAmount: { - color: '#fff', - fontSize: 48, - fontWeight: 'bold', - marginVertical: 8, - }, - buyButton: { - backgroundColor: 'rgba(255,255,255,0.2)', - paddingVertical: 12, - paddingHorizontal: 20, - borderRadius: 8, - alignSelf: 'flex-start', - }, - buyButtonText: { - color: '#fff', - fontWeight: '600', - }, - storeSelector: { - marginBottom: 20, - }, - storeChip: { - backgroundColor: '#fff', - paddingVertical: 8, - paddingHorizontal: 16, - borderRadius: 20, - marginRight: 8, - borderWidth: 1, - borderColor: '#e5e5e5', - }, - storeChipActive: { - backgroundColor: '#2563eb', - borderColor: '#2563eb', - }, - storeChipText: { - color: '#666', - fontWeight: '500', - }, - storeChipTextActive: { - color: '#fff', - }, - actionsSection: { - marginBottom: 20, - }, - sectionTitle: { - fontSize: 18, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 12, - }, - actionCard: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - flexDirection: 'row', - alignItems: 'center', - marginBottom: 12, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - actionIconContainer: { - width: 48, - height: 48, - borderRadius: 12, - backgroundColor: '#f0f9ff', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - actionIcon: { - fontSize: 24, - }, - actionContent: { - flex: 1, - }, - actionTitle: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - }, - actionDescription: { - fontSize: 14, - color: '#666', - marginTop: 2, - }, - actionArrow: { - fontSize: 24, - color: '#ccc', - }, - statsSection: { - marginBottom: 20, - }, - statsGrid: { - flexDirection: 'row', - gap: 12, - }, - statCard: { - flex: 1, - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - alignItems: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - statValue: { - fontSize: 28, - fontWeight: 'bold', - color: '#1a1a1a', - }, - statLabel: { - fontSize: 12, - color: '#666', - marginTop: 4, - }, - alertCard: { - backgroundColor: '#fef3c7', - borderRadius: 12, - padding: 16, - flexDirection: 'row', - alignItems: 'center', - borderWidth: 1, - borderColor: '#fcd34d', - }, - alertIcon: { - fontSize: 24, - marginRight: 12, - }, - alertContent: { - flex: 1, - }, - alertTitle: { - fontSize: 14, - fontWeight: '600', - color: '#92400e', - }, - alertDescription: { - fontSize: 12, - color: '#a16207', - marginTop: 2, - }, - alertAction: { - color: '#2563eb', - fontWeight: '600', - fontSize: 14, - }, -}); diff --git a/src/app/(tabs)/inventory.tsx b/src/app/(tabs)/inventory.tsx deleted file mode 100644 index 7631170..0000000 --- a/src/app/(tabs)/inventory.tsx +++ /dev/null @@ -1,554 +0,0 @@ -import { - View, - Text, - StyleSheet, - FlatList, - TouchableOpacity, - TextInput, - RefreshControl, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useEffect, useState, useCallback, useMemo } from 'react'; -import { router } from 'expo-router'; -import Animated, { FadeIn, FadeInDown, FadeInRight, FadeOut, Layout } from 'react-native-reanimated'; -import { useInventoryStore } from '@stores/inventory.store'; -import { useStoresStore } from '@stores/stores.store'; -import { usePressScale } from '../../hooks/useAnimations'; -import { InventoryListSkeleton } from '../../components/skeletons/InventoryItemSkeleton'; - -const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity); - -interface InventoryItem { - id: string; - name: string; - quantity: number; - category?: string; - barcode?: string; - price?: number; - detectionConfidence?: number; - isManuallyEdited?: boolean; -} - -function InventoryItemCard({ - item, - index, -}: { - item: InventoryItem; - index: number; -}) { - const { animatedStyle, onPressIn, onPressOut } = usePressScale(0.98); - - return ( - - router.push(`/inventory/${item.id}`)} - onPressIn={onPressIn} - onPressOut={onPressOut} - activeOpacity={1} - > - - - - {item.name} - - {item.isManuallyEdited && ( - - Editado - - )} - - {item.category || 'Sin categoria'} - {item.barcode && ( - Codigo: {item.barcode} - )} - {item.detectionConfidence && ( - - - - )} - - - - {item.quantity} - - - unidades - - - - - ); -} - -export default function InventoryScreen() { - const { items, total, isLoading, error, fetchItems, searchQuery, setSearchQuery } = - useInventoryStore(); - const { currentStore } = useStoresStore(); - const [refreshing, setRefreshing] = useState(false); - const [filter, setFilter] = useState<'all' | 'low-stock'>('all'); - const [initialLoad, setInitialLoad] = useState(true); - - useEffect(() => { - if (currentStore) { - fetchItems(currentStore.id, true).then(() => setInitialLoad(false)); - } - }, [currentStore, fetchItems]); - - const onRefresh = useCallback(async () => { - if (!currentStore) return; - setRefreshing(true); - await fetchItems(currentStore.id, true); - setRefreshing(false); - }, [currentStore, fetchItems]); - - const filteredItems = useMemo(() => { - let result = items; - - // Apply search filter - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (item) => - item.name.toLowerCase().includes(query) || - item.category?.toLowerCase().includes(query) || - item.barcode?.includes(query) - ); - } - - // Apply low stock filter - if (filter === 'low-stock') { - result = result.filter((item) => item.quantity < 5); - } - - return result; - }, [items, searchQuery, filter]); - - const renderItem = useCallback( - ({ item, index }: { item: InventoryItem; index: number }) => ( - - ), - [] - ); - - const EmptyState = () => ( - - 📦 - - {searchQuery ? 'Sin resultados' : 'Sin inventario'} - - - {searchQuery - ? `No se encontraron productos que coincidan con "${searchQuery}"` - : 'Escanea tu primer anaquel para comenzar a registrar tu inventario'} - - {!searchQuery && ( - router.push('/(tabs)/scan')} - > - Escanear Anaquel - - )} - - ); - - if (!currentStore) { - return ( - - - 🏪 - Sin tienda seleccionada - - Crea o selecciona una tienda para ver su inventario - - router.push('/stores/new')} - > - Crear Tienda - - - - ); - } - - const showSkeleton = isLoading && initialLoad && items.length === 0; - - return ( - - {/* Header */} - - - - Inventario - - {currentStore.name} - {total} productos - - - - - {/* Search */} - - - {searchQuery && ( - setSearchQuery('')} - > - - - )} - - - {/* Filters */} - - setFilter('all')} - > - - Todos - - - setFilter('low-stock')} - > - - Stock bajo - - - - - - {/* List */} - {showSkeleton ? ( - - - - ) : filteredItems.length === 0 ? ( - - ) : ( - item.id} - contentContainerStyle={styles.list} - showsVerticalScrollIndicator={false} - refreshControl={ - - } - /> - )} - - {/* Error */} - {error && ( - - {error} - - Reintentar - - - )} - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - header: { - padding: 16, - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - headerTop: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - title: { - fontSize: 28, - fontWeight: 'bold', - color: '#1a1a1a', - }, - subtitle: { - fontSize: 14, - color: '#666', - marginTop: 4, - }, - searchContainer: { - position: 'relative', - marginTop: 16, - }, - searchInput: { - backgroundColor: '#f5f5f5', - borderRadius: 8, - paddingHorizontal: 16, - paddingVertical: 12, - fontSize: 16, - color: '#1a1a1a', - }, - clearSearch: { - position: 'absolute', - right: 12, - top: 12, - padding: 4, - }, - clearSearchText: { - fontSize: 16, - color: '#999', - }, - filtersContainer: { - flexDirection: 'row', - marginTop: 12, - gap: 8, - }, - filterChip: { - paddingVertical: 6, - paddingHorizontal: 12, - borderRadius: 16, - backgroundColor: '#f5f5f5', - borderWidth: 1, - borderColor: '#e5e5e5', - }, - filterChipActive: { - backgroundColor: '#2563eb', - borderColor: '#2563eb', - }, - filterChipText: { - fontSize: 14, - color: '#666', - }, - filterChipTextActive: { - color: '#fff', - }, - list: { - padding: 16, - }, - itemCard: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 12, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - itemInfo: { - flex: 1, - marginRight: 12, - }, - itemHeader: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - itemName: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - flex: 1, - }, - editedBadge: { - backgroundColor: '#dbeafe', - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 4, - }, - editedBadgeText: { - fontSize: 10, - color: '#2563eb', - fontWeight: '500', - }, - itemCategory: { - fontSize: 14, - color: '#666', - marginTop: 4, - }, - itemBarcode: { - fontSize: 12, - color: '#999', - marginTop: 2, - }, - confidenceContainer: { - height: 4, - backgroundColor: '#e5e5e5', - borderRadius: 2, - marginTop: 8, - overflow: 'hidden', - }, - confidenceBar: { - height: '100%', - backgroundColor: '#22c55e', - borderRadius: 2, - }, - itemQuantity: { - alignItems: 'center', - backgroundColor: '#f0f9ff', - paddingVertical: 8, - paddingHorizontal: 16, - borderRadius: 8, - minWidth: 70, - }, - itemQuantityLow: { - backgroundColor: '#fef2f2', - }, - quantityValue: { - fontSize: 20, - fontWeight: 'bold', - color: '#2563eb', - }, - quantityValueLow: { - color: '#ef4444', - }, - quantityLabel: { - fontSize: 12, - color: '#666', - }, - quantityLabelLow: { - color: '#ef4444', - }, - emptyState: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - }, - emptyIcon: { - fontSize: 64, - marginBottom: 16, - }, - emptyTitle: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 8, - }, - emptyDescription: { - fontSize: 16, - color: '#666', - textAlign: 'center', - marginBottom: 24, - }, - emptyButton: { - backgroundColor: '#2563eb', - paddingVertical: 12, - paddingHorizontal: 24, - borderRadius: 8, - }, - emptyButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - loadingText: { - marginTop: 12, - fontSize: 16, - color: '#666', - }, - footerLoader: { - paddingVertical: 16, - }, - errorBanner: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - backgroundColor: '#fef2f2', - padding: 16, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - borderTopWidth: 1, - borderTopColor: '#fecaca', - }, - errorText: { - color: '#ef4444', - flex: 1, - }, - errorRetry: { - color: '#2563eb', - fontWeight: '600', - }, -}); diff --git a/src/app/(tabs)/profile.tsx b/src/app/(tabs)/profile.tsx deleted file mode 100644 index 601493f..0000000 --- a/src/app/(tabs)/profile.tsx +++ /dev/null @@ -1,531 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - Alert, - RefreshControl, - Share, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { router } from 'expo-router'; -import { useEffect, useState, useCallback } from 'react'; -import * as Clipboard from 'expo-clipboard'; -import { useAuthStore } from '@stores/auth.store'; -import { useCreditsStore } from '@stores/credits.store'; -import { useReferralsStore } from '@stores/referrals.store'; - -export default function ProfileScreen() { - const { user, logout } = useAuthStore(); - const { balance, fetchBalance, isLoading: creditsLoading } = useCreditsStore(); - const { stats, fetchStats, isLoading: referralsLoading } = useReferralsStore(); - const [refreshing, setRefreshing] = useState(false); - const [copied, setCopied] = useState(false); - - const loadData = useCallback(async () => { - await Promise.all([fetchBalance(), fetchStats()]); - }, [fetchBalance, fetchStats]); - - useEffect(() => { - loadData(); - }, [loadData]); - - const onRefresh = async () => { - setRefreshing(true); - await loadData(); - setRefreshing(false); - }; - - const handleLogout = () => { - Alert.alert( - 'Cerrar Sesion', - 'Estas seguro que deseas cerrar sesion?', - [ - { text: 'Cancelar', style: 'cancel' }, - { - text: 'Cerrar Sesion', - style: 'destructive', - onPress: async () => { - await logout(); - router.replace('/(auth)/login'); - }, - }, - ] - ); - }; - - const copyReferralCode = async () => { - if (stats?.referralCode) { - await Clipboard.setStringAsync(stats.referralCode); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } - }; - - const shareReferralCode = async () => { - if (stats?.referralCode) { - try { - await Share.share({ - message: `Te invito a usar MiInventario, la app que te ayuda a controlar tu inventario con solo grabar un video. Usa mi codigo ${stats.referralCode} y ambos ganaremos creditos gratis!`, - }); - } catch { - // User cancelled share - } - } - }; - - const MenuItem = ({ - icon, - label, - value, - onPress, - destructive = false, - }: { - icon: string; - label: string; - value?: string; - onPress: () => void; - destructive?: boolean; - }) => ( - - {icon} - - {label} - - {value && {value}} - - - ); - - return ( - - - } - > - {/* Header */} - - - - {user?.name?.charAt(0).toUpperCase() || 'U'} - - - {user?.name || 'Usuario'} - {user?.phone || ''} - - - {/* Credits Card */} - - - Tu Balance - {balance?.balance ?? 0} - creditos - - - - {balance?.totalPurchased ?? 0} - Comprados - - - - {balance?.totalFromReferrals ?? 0} - Por referidos - - - - {balance?.totalConsumed ?? 0} - Usados - - - router.push('/credits/buy')} - > - Comprar Creditos - - - - {/* Referral Card */} - - Invita y Gana - - Comparte tu codigo y gana 5 creditos por cada amigo que se registre - - - Tu codigo: - - {stats?.referralCode || '---'} - - - - - {copied ? '✓' : '📋'} - - {copied ? 'Copiado!' : 'Copiar'} - - - - 📤 - - Compartir - - - - - - {stats?.totalReferrals ?? 0} - Invitados - - - {stats?.completedReferrals ?? 0} - Completados - - - {stats?.totalCreditsEarned ?? 0} - Creditos ganados - - - - - {/* Menu Sections */} - - Cuenta - - router.push('/profile/edit')} - /> - router.push('/stores')} - /> - router.push('/payments/methods')} - /> - - - - - Creditos - - router.push('/credits/buy')} - /> - router.push('/credits/history')} - /> - router.push('/referrals')} - /> - - - - - Soporte - - router.push('/help')} - /> - router.push('/support')} - /> - router.push('/legal/terms')} - /> - router.push('/legal/privacy')} - /> - - - - - - - - - - MiInventario v1.0.0 - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - scroll: { - flex: 1, - }, - header: { - backgroundColor: '#fff', - alignItems: 'center', - paddingVertical: 24, - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - avatar: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: '#2563eb', - justifyContent: 'center', - alignItems: 'center', - marginBottom: 12, - }, - avatarText: { - fontSize: 32, - fontWeight: 'bold', - color: '#fff', - }, - name: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - }, - phone: { - fontSize: 16, - color: '#666', - marginTop: 4, - }, - creditsCard: { - backgroundColor: '#2563eb', - margin: 16, - borderRadius: 16, - padding: 20, - marginBottom: 8, - }, - creditsMain: { - alignItems: 'center', - marginBottom: 16, - }, - creditsLabel: { - color: 'rgba(255,255,255,0.8)', - fontSize: 14, - }, - creditsAmount: { - color: '#fff', - fontSize: 56, - fontWeight: 'bold', - marginVertical: 4, - }, - creditsUnit: { - color: 'rgba(255,255,255,0.8)', - fontSize: 16, - }, - creditsStats: { - flexDirection: 'row', - justifyContent: 'space-around', - borderTopWidth: 1, - borderTopColor: 'rgba(255,255,255,0.2)', - paddingTop: 16, - marginBottom: 16, - }, - creditsStat: { - alignItems: 'center', - flex: 1, - }, - creditsDivider: { - width: 1, - backgroundColor: 'rgba(255,255,255,0.2)', - }, - creditsStatValue: { - color: '#fff', - fontSize: 18, - fontWeight: 'bold', - }, - creditsStatLabel: { - color: 'rgba(255,255,255,0.7)', - fontSize: 12, - marginTop: 2, - }, - buyCreditsButton: { - backgroundColor: 'rgba(255,255,255,0.2)', - paddingVertical: 12, - borderRadius: 8, - alignItems: 'center', - }, - buyCreditsButtonText: { - color: '#fff', - fontWeight: '600', - fontSize: 16, - }, - referralCard: { - backgroundColor: '#fff', - margin: 16, - marginTop: 8, - borderRadius: 16, - padding: 20, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.05, - shadowRadius: 4, - elevation: 3, - }, - referralTitle: { - fontSize: 20, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 4, - }, - referralDescription: { - fontSize: 14, - color: '#666', - marginBottom: 16, - }, - referralCodeContainer: { - marginBottom: 16, - }, - referralCodeLabel: { - fontSize: 12, - color: '#666', - marginBottom: 4, - }, - referralCodeBox: { - backgroundColor: '#f5f5f5', - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 8, - alignItems: 'center', - }, - referralCode: { - fontSize: 24, - fontWeight: 'bold', - color: '#2563eb', - letterSpacing: 2, - }, - referralActions: { - flexDirection: 'row', - gap: 12, - marginBottom: 16, - }, - referralActionButton: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 12, - borderRadius: 8, - backgroundColor: '#f5f5f5', - gap: 8, - }, - referralActionButtonPrimary: { - backgroundColor: '#2563eb', - }, - referralActionIcon: { - fontSize: 16, - }, - referralActionText: { - fontSize: 14, - fontWeight: '600', - color: '#666', - }, - referralActionTextPrimary: { - color: '#fff', - }, - referralStats: { - flexDirection: 'row', - justifyContent: 'space-around', - borderTopWidth: 1, - borderTopColor: '#eee', - paddingTop: 16, - }, - referralStat: { - alignItems: 'center', - }, - referralStatValue: { - fontSize: 20, - fontWeight: 'bold', - color: '#1a1a1a', - }, - referralStatLabel: { - fontSize: 12, - color: '#666', - marginTop: 2, - }, - section: { - marginTop: 16, - }, - sectionTitle: { - fontSize: 14, - fontWeight: '600', - color: '#666', - marginBottom: 8, - paddingHorizontal: 16, - textTransform: 'uppercase', - }, - menuGroup: { - backgroundColor: '#fff', - borderTopWidth: 1, - borderBottomWidth: 1, - borderColor: '#eee', - }, - menuItem: { - flexDirection: 'row', - alignItems: 'center', - padding: 16, - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - menuIcon: { - fontSize: 20, - marginRight: 12, - }, - menuLabel: { - flex: 1, - fontSize: 16, - color: '#1a1a1a', - }, - menuLabelDestructive: { - color: '#ef4444', - }, - menuValue: { - fontSize: 14, - color: '#666', - marginRight: 8, - }, - menuArrow: { - fontSize: 20, - color: '#ccc', - }, - version: { - textAlign: 'center', - color: '#999', - fontSize: 14, - marginVertical: 24, - }, -}); diff --git a/src/app/(tabs)/scan.tsx b/src/app/(tabs)/scan.tsx deleted file mode 100644 index 43c288c..0000000 --- a/src/app/(tabs)/scan.tsx +++ /dev/null @@ -1,467 +0,0 @@ -import { View, Text, StyleSheet, TouchableOpacity, Alert, ActivityIndicator } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { Camera, CameraType, CameraRecordingOptions } from 'expo-camera'; -import { useState, useRef, useEffect } from 'react'; -import { router } from 'expo-router'; -import * as FileSystem from 'expo-file-system'; -import { videosService } from '@services/api/videos.service'; -import { useStoresStore } from '@stores/stores.store'; -import { useCreditsStore } from '@stores/credits.store'; - -type ProcessingStatus = 'idle' | 'recording' | 'uploading' | 'processing' | 'completed' | 'failed'; - -export default function ScanScreen() { - const [permission, requestPermission] = Camera.useCameraPermissions(); - const [audioPermission, requestAudioPermission] = Camera.useMicrophonePermissions(); - const [status, setStatus] = useState('idle'); - const [progress, setProgress] = useState(0); - const [recordingDuration, setRecordingDuration] = useState(0); - const [errorMessage, setErrorMessage] = useState(null); - const cameraRef = useRef(null); - const recordingTimer = useRef(null); - const pollingTimer = useRef(null); - - const { currentStore, fetchStores } = useStoresStore(); - const { fetchBalance } = useCreditsStore(); - - useEffect(() => { - fetchStores(); - return () => { - if (recordingTimer.current) clearInterval(recordingTimer.current); - if (pollingTimer.current) clearInterval(pollingTimer.current); - }; - }, []); - - if (!permission || !audioPermission) { - return ( - - - - Cargando permisos... - - - ); - } - - if (!permission.granted || !audioPermission.granted) { - return ( - - - - Necesitamos acceso a la camara y microfono para escanear tu inventario - - { - await requestPermission(); - await requestAudioPermission(); - }} - > - Dar Permisos - - - - ); - } - - if (!currentStore) { - return ( - - - - Primero debes crear o seleccionar una tienda - - router.push('/stores/new')} - > - Crear Tienda - - - - ); - } - - const startRecording = async () => { - if (!cameraRef.current) return; - - setStatus('recording'); - setRecordingDuration(0); - setErrorMessage(null); - - // Start duration timer - recordingTimer.current = setInterval(() => { - setRecordingDuration(prev => prev + 1); - }, 1000); - - try { - const options: CameraRecordingOptions = { - maxDuration: 30, // Max 30 seconds - }; - - const video = await cameraRef.current.recordAsync(options); - - if (recordingTimer.current) { - clearInterval(recordingTimer.current); - } - - await processVideo(video.uri); - } catch (error) { - console.error('Recording error:', error); - setStatus('failed'); - setErrorMessage('Error al grabar video'); - if (recordingTimer.current) { - clearInterval(recordingTimer.current); - } - } - }; - - const stopRecording = async () => { - if (!cameraRef.current) return; - - try { - cameraRef.current.stopRecording(); - } catch (error) { - console.error('Stop recording error:', error); - } - }; - - const processVideo = async (videoUri: string) => { - if (!currentStore) return; - - setStatus('uploading'); - setProgress(0); - - try { - // Get file info - const fileInfo = await FileSystem.getInfoAsync(videoUri); - const fileSize = (fileInfo as any).size || 0; - const fileName = `scan_${Date.now()}.mp4`; - - // Initiate upload - const { videoId, uploadUrl } = await videosService.initiateUpload( - currentStore.id, - fileName, - fileSize - ); - - // Upload video - await videosService.uploadVideo(uploadUrl, videoUri, (uploadProgress) => { - setProgress(Math.round(uploadProgress * 50)); // 0-50% for upload - }); - - // Confirm upload - await videosService.confirmUpload(currentStore.id, videoId); - - setStatus('processing'); - setProgress(50); - - // Poll for processing status - await pollProcessingStatus(currentStore.id, videoId); - - } catch (error) { - console.error('Processing error:', error); - setStatus('failed'); - setErrorMessage(error instanceof Error ? error.message : 'Error al procesar video'); - } - }; - - const pollProcessingStatus = async (storeId: string, videoId: string) => { - const maxAttempts = 60; // 2 minutes max - let attempts = 0; - - return new Promise((resolve, reject) => { - pollingTimer.current = setInterval(async () => { - attempts++; - - if (attempts > maxAttempts) { - if (pollingTimer.current) clearInterval(pollingTimer.current); - setStatus('failed'); - setErrorMessage('Tiempo de espera agotado'); - reject(new Error('Timeout')); - return; - } - - try { - const result = await videosService.getStatus(storeId, videoId); - - // Update progress (50-100% for processing) - const processingProgress = 50 + (result.progress / 2); - setProgress(Math.round(processingProgress)); - - if (result.status === 'completed') { - if (pollingTimer.current) clearInterval(pollingTimer.current); - setStatus('completed'); - setProgress(100); - - // Refresh credits balance - await fetchBalance(); - - // Show success and navigate - Alert.alert( - 'Escaneo Completado', - `Se detectaron ${result.resultItems || 0} productos`, - [ - { - text: 'Ver Inventario', - onPress: () => router.replace('/(tabs)/inventory'), - }, - { - text: 'Nuevo Escaneo', - onPress: () => resetState(), - }, - ] - ); - - resolve(); - } else if (result.status === 'failed') { - if (pollingTimer.current) clearInterval(pollingTimer.current); - setStatus('failed'); - setErrorMessage(result.errorMessage || 'Error al procesar'); - reject(new Error(result.errorMessage)); - } - } catch (error) { - console.error('Polling error:', error); - } - }, 2000); - }); - }; - - const resetState = () => { - setStatus('idle'); - setProgress(0); - setRecordingDuration(0); - setErrorMessage(null); - }; - - const formatDuration = (seconds: number) => { - const mins = Math.floor(seconds / 60); - const secs = seconds % 60; - return `${mins}:${secs.toString().padStart(2, '0')}`; - }; - - const isRecording = status === 'recording'; - const isProcessing = status === 'uploading' || status === 'processing'; - - return ( - - - - {/* Header */} - - {currentStore.name} - - {isRecording ? `Grabando ${formatDuration(recordingDuration)}` : - isProcessing ? 'Procesando...' : - status === 'completed' ? 'Completado' : - status === 'failed' ? 'Error' : - 'Escanear Anaquel'} - - - {isRecording ? 'Toca para detener' : - isProcessing ? `${progress}% completado` : - 'Mueve la camara lentamente por el anaquel'} - - - - {/* Progress bar for processing */} - {isProcessing && ( - - - - - - {status === 'uploading' ? 'Subiendo video...' : 'Detectando productos...'} - - - )} - - {/* Error message */} - {status === 'failed' && errorMessage && ( - - {errorMessage} - - Intentar de nuevo - - - )} - - {/* Controls */} - - {status === 'idle' && ( - <> - - - - Iniciar Grabacion - - )} - - {isRecording && ( - <> - - - - Detener (max 30s) - - )} - - {isProcessing && ( - - )} - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000', - }, - centered: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - backgroundColor: '#fff', - }, - loadingText: { - marginTop: 12, - fontSize: 16, - color: '#666', - }, - permissionText: { - fontSize: 16, - textAlign: 'center', - color: '#666', - marginBottom: 24, - }, - permissionButton: { - backgroundColor: '#2563eb', - paddingVertical: 12, - paddingHorizontal: 24, - borderRadius: 8, - }, - permissionButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - camera: { - flex: 1, - }, - overlay: { - flex: 1, - justifyContent: 'space-between', - }, - header: { - padding: 16, - backgroundColor: 'rgba(0,0,0,0.6)', - }, - storeName: { - color: 'rgba(255,255,255,0.7)', - fontSize: 12, - textAlign: 'center', - marginBottom: 4, - }, - headerText: { - color: '#fff', - fontSize: 20, - fontWeight: 'bold', - textAlign: 'center', - }, - headerSubtext: { - color: 'rgba(255,255,255,0.8)', - fontSize: 14, - textAlign: 'center', - marginTop: 4, - }, - progressContainer: { - padding: 20, - alignItems: 'center', - }, - progressBar: { - width: '80%', - height: 8, - backgroundColor: 'rgba(255,255,255,0.3)', - borderRadius: 4, - overflow: 'hidden', - }, - progressFill: { - height: '100%', - backgroundColor: '#22c55e', - borderRadius: 4, - }, - progressText: { - color: '#fff', - fontSize: 14, - marginTop: 8, - }, - errorContainer: { - padding: 20, - alignItems: 'center', - }, - errorText: { - color: '#ef4444', - fontSize: 16, - textAlign: 'center', - marginBottom: 12, - }, - retryButton: { - backgroundColor: '#ef4444', - paddingVertical: 10, - paddingHorizontal: 20, - borderRadius: 8, - }, - retryButtonText: { - color: '#fff', - fontSize: 14, - fontWeight: '600', - }, - controls: { - alignItems: 'center', - paddingBottom: 40, - }, - recordButton: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: 'rgba(255,255,255,0.3)', - justifyContent: 'center', - alignItems: 'center', - borderWidth: 4, - borderColor: '#fff', - }, - recordButtonActive: { - borderColor: '#ef4444', - }, - recordInner: { - width: 60, - height: 60, - borderRadius: 30, - backgroundColor: '#ef4444', - }, - recordInnerActive: { - width: 30, - height: 30, - borderRadius: 4, - }, - recordText: { - color: '#fff', - fontSize: 14, - marginTop: 12, - }, -}); diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx deleted file mode 100644 index 2455fd3..0000000 --- a/src/app/_layout.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Stack } from 'expo-router'; -import { StatusBar } from 'expo-status-bar'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import { StyleSheet, View } from 'react-native'; -import { OfflineBanner } from '../components/ui/OfflineBanner'; -import { ThemeProvider } from '../theme/ThemeContext'; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5 minutes - retry: 2, - }, - }, -}); - -export default function RootLayout() { - return ( - - - - - - - - - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, -}); diff --git a/src/app/credits/_layout.tsx b/src/app/credits/_layout.tsx deleted file mode 100644 index c1a2e09..0000000 --- a/src/app/credits/_layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Stack } from 'expo-router'; - -export default function CreditsLayout() { - return ( - - - - - ); -} diff --git a/src/app/credits/buy.tsx b/src/app/credits/buy.tsx deleted file mode 100644 index 1a9e3a1..0000000 --- a/src/app/credits/buy.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - ActivityIndicator, - Alert, - Linking, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useEffect, useState } from 'react'; -import { router } from 'expo-router'; -import { usePaymentsStore } from '@stores/payments.store'; -import { useCreditsStore } from '@stores/credits.store'; -import { CreditPackage } from '@services/api/payments.service'; - -type PaymentMethod = 'card' | 'oxxo' | '7eleven'; - -export default function BuyCreditsScreen() { - const { packages, fetchPackages, createPayment, isLoading, isProcessing, error } = - usePaymentsStore(); - const { fetchBalance } = useCreditsStore(); - const [selectedPackage, setSelectedPackage] = useState(null); - const [selectedMethod, setSelectedMethod] = useState('oxxo'); - - useEffect(() => { - fetchPackages(); - }, [fetchPackages]); - - const handlePurchase = async () => { - if (!selectedPackage) { - Alert.alert('Error', 'Selecciona un paquete de creditos'); - return; - } - - const response = await createPayment({ - packageId: selectedPackage.id, - method: selectedMethod, - }); - - if (response) { - if (response.status === 'completed') { - await fetchBalance(); - Alert.alert( - 'Compra Exitosa', - `Se agregaron ${selectedPackage.credits} creditos a tu cuenta`, - [{ text: 'OK', onPress: () => router.back() }] - ); - } else if (response.voucherUrl) { - Alert.alert( - 'Pago Pendiente', - `Tu ficha de pago esta lista. Codigo: ${response.voucherCode}\n\nTienes hasta ${new Date(response.expiresAt || '').toLocaleDateString()} para pagar.`, - [ - { text: 'Cancelar', style: 'cancel' }, - { - text: 'Ver Ficha', - onPress: () => Linking.openURL(response.voucherUrl!), - }, - ] - ); - } - } - }; - - const formatPrice = (price: number) => { - return `$${price.toFixed(2)} MXN`; - }; - - const PaymentMethodButton = ({ - method, - label, - icon, - description, - }: { - method: PaymentMethod; - label: string; - icon: string; - description: string; - }) => ( - setSelectedMethod(method)} - > - {icon} - - - {label} - - {description} - - - {selectedMethod === method && } - - - ); - - if (isLoading && packages.length === 0) { - return ( - - - - Cargando paquetes... - - - ); - } - - return ( - - - {/* Packages */} - Selecciona un paquete - - {packages.map((pkg) => ( - setSelectedPackage(pkg)} - > - {pkg.popular && ( - - Popular - - )} - {pkg.credits} - creditos - {formatPrice(pkg.priceMXN)} - - ${(pkg.priceMXN / pkg.credits).toFixed(2)}/credito - - - ))} - - - {/* Payment Methods */} - Metodo de pago - - - - - - - {/* Info */} - - ℹ️ - - {selectedMethod === 'card' - ? 'El pago con tarjeta se procesa inmediatamente y los creditos se agregan al instante.' - : 'Recibiras una ficha de pago. Los creditos se agregan automaticamente al pagar.'} - - - - {/* Error */} - {error && ( - - {error} - - )} - - - {/* Footer */} - - {selectedPackage && ( - - Total a pagar: - - {formatPrice(selectedPackage.priceMXN)} - - - )} - - {isProcessing ? ( - - ) : ( - - {selectedMethod === 'card' ? 'Pagar Ahora' : 'Generar Ficha de Pago'} - - )} - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - paddingBottom: 32, - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - loadingText: { - marginTop: 12, - fontSize: 16, - color: '#666', - }, - sectionTitle: { - fontSize: 18, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 12, - marginTop: 8, - }, - packagesGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - marginBottom: 24, - }, - packageCard: { - width: '47%', - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - alignItems: 'center', - borderWidth: 2, - borderColor: 'transparent', - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - packageCardSelected: { - borderColor: '#2563eb', - backgroundColor: '#f0f9ff', - }, - packageCardPopular: { - borderColor: '#f59e0b', - }, - popularBadge: { - position: 'absolute', - top: -8, - backgroundColor: '#f59e0b', - paddingHorizontal: 8, - paddingVertical: 2, - borderRadius: 8, - }, - popularBadgeText: { - color: '#fff', - fontSize: 10, - fontWeight: 'bold', - }, - packageCredits: { - fontSize: 36, - fontWeight: 'bold', - color: '#2563eb', - }, - packageCreditsLabel: { - fontSize: 14, - color: '#666', - marginBottom: 8, - }, - packagePrice: { - fontSize: 18, - fontWeight: '600', - color: '#1a1a1a', - }, - packagePerCredit: { - fontSize: 12, - color: '#999', - marginTop: 4, - }, - methodsContainer: { - gap: 12, - marginBottom: 24, - }, - methodButton: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - borderWidth: 2, - borderColor: 'transparent', - }, - methodButtonSelected: { - borderColor: '#2563eb', - backgroundColor: '#f0f9ff', - }, - methodIcon: { - fontSize: 28, - marginRight: 12, - }, - methodInfo: { - flex: 1, - }, - methodLabel: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - }, - methodLabelSelected: { - color: '#2563eb', - }, - methodDescription: { - fontSize: 13, - color: '#666', - marginTop: 2, - }, - methodRadio: { - width: 24, - height: 24, - borderRadius: 12, - borderWidth: 2, - borderColor: '#ccc', - justifyContent: 'center', - alignItems: 'center', - }, - methodRadioSelected: { - borderColor: '#2563eb', - }, - methodRadioInner: { - width: 12, - height: 12, - borderRadius: 6, - backgroundColor: '#2563eb', - }, - infoCard: { - flexDirection: 'row', - backgroundColor: '#f0f9ff', - borderRadius: 12, - padding: 16, - borderWidth: 1, - borderColor: '#bfdbfe', - }, - infoIcon: { - fontSize: 20, - marginRight: 12, - }, - infoText: { - flex: 1, - fontSize: 14, - color: '#1e40af', - lineHeight: 20, - }, - errorCard: { - backgroundColor: '#fef2f2', - borderRadius: 12, - padding: 16, - marginTop: 16, - borderWidth: 1, - borderColor: '#fecaca', - }, - errorText: { - color: '#ef4444', - fontSize: 14, - }, - footer: { - backgroundColor: '#fff', - padding: 16, - borderTopWidth: 1, - borderTopColor: '#eee', - }, - footerSummary: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 12, - }, - footerLabel: { - fontSize: 16, - color: '#666', - }, - footerPrice: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - }, - purchaseButton: { - backgroundColor: '#2563eb', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - }, - purchaseButtonDisabled: { - backgroundColor: '#93c5fd', - }, - purchaseButtonText: { - color: '#fff', - fontSize: 18, - fontWeight: '600', - }, -}); diff --git a/src/app/credits/history.tsx b/src/app/credits/history.tsx deleted file mode 100644 index 6c3d229..0000000 --- a/src/app/credits/history.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import { - View, - Text, - StyleSheet, - FlatList, - TouchableOpacity, - RefreshControl, - ActivityIndicator, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useEffect, useState, useCallback } from 'react'; -import { useCreditsStore } from '@stores/credits.store'; - -interface Transaction { - id: string; - type: string; - amount: number; - description: string; - createdAt: string; -} - -export default function CreditsHistoryScreen() { - const { - transactions, - transactionsHasMore, - fetchTransactions, - isLoading, - } = useCreditsStore(); - const [refreshing, setRefreshing] = useState(false); - - useEffect(() => { - fetchTransactions(true); - }, [fetchTransactions]); - - const onRefresh = useCallback(async () => { - setRefreshing(true); - await fetchTransactions(true); - setRefreshing(false); - }, [fetchTransactions]); - - const loadMore = () => { - if (transactionsHasMore && !isLoading) { - fetchTransactions(false); - } - }; - - const getTransactionIcon = (type: string) => { - switch (type) { - case 'purchase': - return '💰'; - case 'consumption': - return '📷'; - case 'referral_bonus': - return '🎁'; - default: - return '📝'; - } - }; - - const getTransactionColor = (type: string, amount: number) => { - if (amount > 0) return '#22c55e'; - return '#ef4444'; - }; - - const formatDate = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleDateString('es-MX', { - day: 'numeric', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - }; - - const renderItem = ({ item }: { item: Transaction }) => ( - - - - {getTransactionIcon(item.type)} - - - - {item.description} - {formatDate(item.createdAt)} - - - {item.amount > 0 ? '+' : ''}{item.amount} - - - ); - - const EmptyState = () => ( - - 📋 - Sin transacciones - - Aqui veras tu historial de creditos comprados y utilizados - - - ); - - return ( - - {isLoading && transactions.length === 0 ? ( - - - Cargando historial... - - ) : transactions.length === 0 ? ( - - ) : ( - item.id} - contentContainerStyle={styles.list} - refreshControl={ - - } - onEndReached={loadMore} - onEndReachedThreshold={0.5} - ListFooterComponent={ - isLoading ? ( - - ) : null - } - ItemSeparatorComponent={() => } - /> - )} - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - loadingText: { - marginTop: 12, - fontSize: 16, - color: '#666', - }, - list: { - padding: 16, - }, - transactionCard: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - }, - transactionIcon: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: '#f5f5f5', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - transactionIconText: { - fontSize: 20, - }, - transactionInfo: { - flex: 1, - }, - transactionDescription: { - fontSize: 15, - fontWeight: '500', - color: '#1a1a1a', - }, - transactionDate: { - fontSize: 13, - color: '#666', - marginTop: 2, - }, - transactionAmount: { - fontSize: 18, - fontWeight: 'bold', - }, - separator: { - height: 8, - }, - emptyState: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - }, - emptyIcon: { - fontSize: 64, - marginBottom: 16, - }, - emptyTitle: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 8, - }, - emptyDescription: { - fontSize: 16, - color: '#666', - textAlign: 'center', - }, - footerLoader: { - paddingVertical: 16, - }, -}); diff --git a/src/app/help/index.tsx b/src/app/help/index.tsx deleted file mode 100644 index 44d127a..0000000 --- a/src/app/help/index.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { Stack, router } from 'expo-router'; -import { useState } from 'react'; - -interface FAQItem { - id: string; - question: string; - answer: string; - category: string; -} - -const faqs: FAQItem[] = [ - { - id: '1', - question: 'Como escaneo mi inventario?', - answer: 'Ve a la pestana "Escanear" y graba un video moviendo tu telefono lentamente por los anaqueles. La IA detectara automaticamente los productos y los agregara a tu inventario.', - category: 'escaneo', - }, - { - id: '2', - question: 'Cuantos creditos necesito por escaneo?', - answer: 'Cada escaneo de video consume 1 credito. Al registrarte recibes 5 creditos gratis para que pruebes la app.', - category: 'creditos', - }, - { - id: '3', - question: 'Como compro mas creditos?', - answer: 'Ve a tu perfil y toca "Comprar Creditos". Puedes pagar con tarjeta de credito/debito o en efectivo en OXXO.', - category: 'creditos', - }, - { - id: '4', - question: 'Como gano creditos gratis?', - answer: 'Invita a tus amigos usando tu codigo de referido. Por cada amigo que se registre, ambos reciben 5 creditos gratis.', - category: 'creditos', - }, - { - id: '5', - question: 'Como creo una tienda?', - answer: 'Ve a la pestana "Tiendas" y toca el boton "Nueva Tienda". Llena los datos de tu negocio y listo.', - category: 'tiendas', - }, - { - id: '6', - question: 'Puedo tener varias tiendas?', - answer: 'Si, puedes crear multiples tiendas y cambiar entre ellas. Cada tienda tiene su propio inventario.', - category: 'tiendas', - }, - { - id: '7', - question: 'Como edito mi inventario?', - answer: 'En la pestana "Inventario" puedes ver todos los productos detectados. Toca cualquier producto para editar su cantidad, precio o nombre.', - category: 'inventario', - }, - { - id: '8', - question: 'Que tan precisa es la deteccion?', - answer: 'La IA tiene una precision del 90-95%. Te recomendamos revisar los productos detectados y hacer ajustes si es necesario.', - category: 'escaneo', - }, - { - id: '9', - question: 'El pago en OXXO es seguro?', - answer: 'Si, utilizamos Stripe para procesar todos los pagos de forma segura. Al elegir OXXO recibiras un codigo para pagar en cualquier tienda.', - category: 'pagos', - }, - { - id: '10', - question: 'Cuando recibo mis creditos al pagar en OXXO?', - answer: 'Los creditos se acreditan automaticamente entre 24-48 horas despues de realizar el pago en tienda.', - category: 'pagos', - }, -]; - -const categories = [ - { id: 'todos', label: 'Todos' }, - { id: 'escaneo', label: 'Escaneo' }, - { id: 'creditos', label: 'Creditos' }, - { id: 'tiendas', label: 'Tiendas' }, - { id: 'inventario', label: 'Inventario' }, - { id: 'pagos', label: 'Pagos' }, -]; - -export default function HelpScreen() { - const [selectedCategory, setSelectedCategory] = useState('todos'); - const [expandedId, setExpandedId] = useState(null); - - const filteredFaqs = - selectedCategory === 'todos' - ? faqs - : faqs.filter((faq) => faq.category === selectedCategory); - - const toggleExpand = (id: string) => { - setExpandedId(expandedId === id ? null : id); - }; - - return ( - - - - - Como podemos ayudarte? - - Encuentra respuestas a las preguntas mas frecuentes - - - - - {categories.map((category) => ( - setSelectedCategory(category.id)} - > - - {category.label} - - - ))} - - - - {filteredFaqs.map((faq) => ( - toggleExpand(faq.id)} - activeOpacity={0.7} - > - - {faq.question} - - {expandedId === faq.id ? '−' : '+'} - - - {expandedId === faq.id && ( - {faq.answer} - )} - - ))} - - - - No encontraste lo que buscabas? - router.push('/support')} - > - Contactar Soporte - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - scroll: { - flex: 1, - }, - header: { - padding: 20, - backgroundColor: '#2563eb', - }, - headerTitle: { - fontSize: 24, - fontWeight: 'bold', - color: '#fff', - marginBottom: 4, - }, - headerSubtitle: { - fontSize: 14, - color: 'rgba(255,255,255,0.8)', - }, - categoriesContainer: { - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - categoriesContent: { - padding: 12, - gap: 8, - }, - categoryChip: { - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 20, - backgroundColor: '#f5f5f5', - marginRight: 8, - }, - categoryChipActive: { - backgroundColor: '#2563eb', - }, - categoryChipText: { - fontSize: 14, - color: '#666', - fontWeight: '500', - }, - categoryChipTextActive: { - color: '#fff', - }, - faqsList: { - padding: 16, - gap: 8, - }, - faqItem: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - faqHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - faqQuestion: { - flex: 1, - fontSize: 15, - fontWeight: '600', - color: '#1a1a1a', - marginRight: 12, - }, - faqArrow: { - fontSize: 20, - color: '#2563eb', - fontWeight: 'bold', - }, - faqAnswer: { - marginTop: 12, - fontSize: 14, - color: '#666', - lineHeight: 20, - }, - supportSection: { - margin: 16, - padding: 20, - backgroundColor: '#fff', - borderRadius: 12, - alignItems: 'center', - }, - supportTitle: { - fontSize: 16, - color: '#1a1a1a', - marginBottom: 12, - textAlign: 'center', - }, - supportButton: { - backgroundColor: '#2563eb', - paddingHorizontal: 24, - paddingVertical: 12, - borderRadius: 8, - }, - supportButtonText: { - fontSize: 14, - fontWeight: '600', - color: '#fff', - }, -}); diff --git a/src/app/index.tsx b/src/app/index.tsx deleted file mode 100644 index 988d40a..0000000 --- a/src/app/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Redirect } from 'expo-router'; -import { useAuthStore } from '@stores/auth.store'; - -export default function Index() { - const { isAuthenticated } = useAuthStore(); - - if (isAuthenticated) { - return ; - } - - return ; -} diff --git a/src/app/inventory/[id].tsx b/src/app/inventory/[id].tsx deleted file mode 100644 index acbbe62..0000000 --- a/src/app/inventory/[id].tsx +++ /dev/null @@ -1,603 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TextInput, - TouchableOpacity, - ActivityIndicator, - Alert, - KeyboardAvoidingView, - Platform, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useState, useEffect } from 'react'; -import { router, useLocalSearchParams, Stack } from 'expo-router'; -import { useInventoryStore } from '@stores/inventory.store'; -import { InventoryItem } from '@services/api/inventory.service'; - -export default function InventoryDetailScreen() { - const { id } = useLocalSearchParams<{ id: string }>(); - const { items, updateItem, deleteItem, isLoading, error } = useInventoryStore(); - const [item, setItem] = useState(null); - const [name, setName] = useState(''); - const [quantity, setQuantity] = useState(''); - const [category, setCategory] = useState(''); - const [barcode, setBarcode] = useState(''); - const [price, setPrice] = useState(''); - const [isEditing, setIsEditing] = useState(false); - - useEffect(() => { - const foundItem = items.find((i) => i.id === id); - if (foundItem) { - setItem(foundItem); - setName(foundItem.name); - setQuantity(foundItem.quantity.toString()); - setCategory(foundItem.category || ''); - setBarcode(foundItem.barcode || ''); - setPrice(foundItem.price?.toString() || ''); - } - }, [id, items]); - - const handleSave = async () => { - if (!name.trim()) { - Alert.alert('Error', 'El nombre del producto es requerido'); - return; - } - - if (!id) return; - - try { - await updateItem(id, { - name: name.trim(), - quantity: parseInt(quantity, 10) || 0, - category: category.trim() || undefined, - barcode: barcode.trim() || undefined, - price: price ? parseFloat(price) : undefined, - }); - - Alert.alert('Listo', 'El producto ha sido actualizado'); - setIsEditing(false); - } catch { - // Error handled by store - } - }; - - const handleDelete = () => { - Alert.alert( - 'Eliminar Producto', - 'Estas seguro de eliminar este producto del inventario?', - [ - { text: 'Cancelar', style: 'cancel' }, - { - text: 'Eliminar', - style: 'destructive', - onPress: async () => { - if (!id) return; - try { - await deleteItem(id); - Alert.alert('Listo', 'El producto ha sido eliminado', [ - { text: 'OK', onPress: () => router.back() }, - ]); - } catch { - // Error handled by store - } - }, - }, - ] - ); - }; - - const adjustQuantity = (delta: number) => { - const current = parseInt(quantity, 10) || 0; - const newValue = Math.max(0, current + delta); - setQuantity(newValue.toString()); - }; - - if (!item) { - return ( - - - - - - ); - } - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString('es-MX', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - }; - - return ( - <> - - !isEditing ? ( - setIsEditing(true)} - > - Editar - - ) : null, - }} - /> - - - - {/* Quantity Card */} - - Cantidad en inventario - {isEditing ? ( - - adjustQuantity(-1)} - > - - - - - adjustQuantity(1)} - > - + - - - ) : ( - - {item.quantity} - - )} - {item.quantity < 5 && !isEditing && ( - - Stock bajo - - )} - - - {/* Details */} - - Informacion del producto - - - Nombre - {isEditing ? ( - - ) : ( - {item.name} - )} - - - - Categoria - {isEditing ? ( - - ) : ( - - {item.category || 'Sin categoria'} - - )} - - - - Codigo de barras - {isEditing ? ( - - ) : ( - - {item.barcode || 'Sin codigo'} - - )} - - - - Precio - {isEditing ? ( - - ) : ( - - {item.price ? `$${item.price.toFixed(2)}` : 'Sin precio'} - - )} - - - - {/* Detection Info */} - {item.detectionConfidence && ( - - Deteccion automatica - - - Confianza - - {(item.detectionConfidence * 100).toFixed(0)}% - - - - - - {item.isManuallyEdited && ( - - - Editado manualmente - - - )} - - - )} - - {/* Metadata */} - - Historial - - - Creado - - {formatDate(item.createdAt)} - - - - Actualizado - - {formatDate(item.updatedAt)} - - - {item.lastDetectedAt && ( - - Ultima deteccion - - {formatDate(item.lastDetectedAt)} - - - )} - - - - {/* Error */} - {error && ( - - {error} - - )} - - {/* Delete Button */} - {isEditing && ( - - Eliminar Producto - - )} - - - {/* Footer */} - {isEditing && ( - - { - setName(item.name); - setQuantity(item.quantity.toString()); - setCategory(item.category || ''); - setBarcode(item.barcode || ''); - setPrice(item.price?.toString() || ''); - setIsEditing(false); - }} - > - Cancelar - - - {isLoading ? ( - - ) : ( - Guardar - )} - - - )} - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - keyboardAvoid: { - flex: 1, - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - }, - editHeaderButton: { - marginRight: 8, - }, - editHeaderButtonText: { - color: '#2563eb', - fontSize: 16, - fontWeight: '600', - }, - quantityCard: { - backgroundColor: '#fff', - borderRadius: 16, - padding: 24, - alignItems: 'center', - marginBottom: 16, - }, - quantityLabel: { - fontSize: 14, - color: '#666', - marginBottom: 8, - }, - quantityValue: { - fontSize: 64, - fontWeight: 'bold', - color: '#1a1a1a', - }, - quantityValueLow: { - color: '#ef4444', - }, - quantityEditor: { - flexDirection: 'row', - alignItems: 'center', - gap: 16, - }, - quantityButton: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: '#2563eb', - justifyContent: 'center', - alignItems: 'center', - }, - quantityButtonText: { - color: '#fff', - fontSize: 24, - fontWeight: 'bold', - }, - quantityInput: { - width: 100, - fontSize: 48, - fontWeight: 'bold', - color: '#1a1a1a', - }, - lowStockBadge: { - backgroundColor: '#fef2f2', - paddingHorizontal: 12, - paddingVertical: 6, - borderRadius: 8, - marginTop: 12, - }, - lowStockBadgeText: { - color: '#ef4444', - fontSize: 14, - fontWeight: '600', - }, - section: { - marginBottom: 16, - }, - sectionTitle: { - fontSize: 14, - fontWeight: '600', - color: '#666', - marginBottom: 8, - textTransform: 'uppercase', - }, - field: { - backgroundColor: '#fff', - paddingHorizontal: 16, - paddingVertical: 12, - borderBottomWidth: 1, - borderBottomColor: '#f5f5f5', - }, - fieldLabel: { - fontSize: 12, - color: '#666', - marginBottom: 4, - }, - fieldValue: { - fontSize: 16, - color: '#1a1a1a', - }, - fieldInput: { - fontSize: 16, - color: '#1a1a1a', - padding: 0, - }, - detectionCard: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - }, - detectionRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, - }, - detectionLabel: { - fontSize: 14, - color: '#666', - }, - detectionValue: { - fontSize: 16, - fontWeight: '600', - color: '#22c55e', - }, - confidenceBar: { - height: 8, - backgroundColor: '#e5e5e5', - borderRadius: 4, - overflow: 'hidden', - }, - confidenceBarFill: { - height: '100%', - backgroundColor: '#22c55e', - borderRadius: 4, - }, - editedBadge: { - backgroundColor: '#dbeafe', - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 4, - alignSelf: 'flex-start', - marginTop: 12, - }, - editedBadgeText: { - color: '#2563eb', - fontSize: 12, - fontWeight: '500', - }, - metaCard: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - }, - metaRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingVertical: 8, - borderBottomWidth: 1, - borderBottomColor: '#f5f5f5', - }, - metaLabel: { - fontSize: 14, - color: '#666', - }, - metaValue: { - fontSize: 14, - color: '#1a1a1a', - }, - errorCard: { - backgroundColor: '#fef2f2', - borderRadius: 12, - padding: 16, - marginBottom: 16, - borderWidth: 1, - borderColor: '#fecaca', - }, - errorText: { - color: '#ef4444', - fontSize: 14, - }, - deleteButton: { - alignItems: 'center', - paddingVertical: 16, - marginTop: 16, - }, - deleteButtonText: { - color: '#ef4444', - fontSize: 16, - fontWeight: '600', - }, - footer: { - flexDirection: 'row', - gap: 12, - backgroundColor: '#fff', - padding: 16, - borderTopWidth: 1, - borderTopColor: '#eee', - }, - cancelButton: { - flex: 1, - backgroundColor: '#f5f5f5', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - }, - cancelButtonText: { - color: '#666', - fontSize: 16, - fontWeight: '600', - }, - saveButton: { - flex: 1, - backgroundColor: '#2563eb', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - }, - saveButtonDisabled: { - backgroundColor: '#93c5fd', - }, - saveButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, -}); diff --git a/src/app/inventory/_layout.tsx b/src/app/inventory/_layout.tsx deleted file mode 100644 index 321d889..0000000 --- a/src/app/inventory/_layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Stack } from 'expo-router'; - -export default function InventoryLayout() { - return ( - - - - - ); -} diff --git a/src/app/inventory/export.tsx b/src/app/inventory/export.tsx deleted file mode 100644 index 6f62c37..0000000 --- a/src/app/inventory/export.tsx +++ /dev/null @@ -1,492 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - ActivityIndicator, - Alert, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useState } from 'react'; -import * as Linking from 'expo-linking'; -import * as Sharing from 'expo-sharing'; -import * as FileSystem from 'expo-file-system'; -import { useStoresStore } from '@stores/stores.store'; -import { - exportsService, - ExportFormat, - ExportStatusResponse, -} from '@services/api/exports.service'; - -type ExportStep = 'select' | 'processing' | 'complete' | 'error'; - -export default function ExportInventoryScreen() { - const { currentStore } = useStoresStore(); - const [format, setFormat] = useState('CSV'); - const [lowStockOnly, setLowStockOnly] = useState(false); - const [step, setStep] = useState('select'); - const [progress, setProgress] = useState(null); - const [downloadUrl, setDownloadUrl] = useState(null); - const [filename, setFilename] = useState(''); - const [errorMessage, setErrorMessage] = useState(null); - - const handleExport = async () => { - if (!currentStore) { - Alert.alert('Error', 'No hay tienda seleccionada'); - return; - } - - setStep('processing'); - setErrorMessage(null); - - try { - // Request export - const { jobId } = await exportsService.requestInventoryExport( - currentStore.id, - format, - lowStockOnly ? { lowStockOnly: true } : undefined, - ); - - // Poll for completion - const status = await exportsService.pollExportStatus( - currentStore.id, - jobId, - (s) => setProgress(s), - ); - - if (status.status === 'FAILED') { - setStep('error'); - setErrorMessage(status.errorMessage || 'Error desconocido'); - return; - } - - // Get download URL - const download = await exportsService.getDownloadUrl(currentStore.id, jobId); - setDownloadUrl(download.url); - setFilename(download.filename); - setStep('complete'); - } catch (error) { - setStep('error'); - setErrorMessage(error instanceof Error ? error.message : 'Error al exportar'); - } - }; - - const handleDownload = async () => { - if (!downloadUrl) return; - - try { - await Linking.openURL(downloadUrl); - } catch { - Alert.alert('Error', 'No se pudo abrir el enlace de descarga'); - } - }; - - const handleShare = async () => { - if (!downloadUrl || !filename) return; - - try { - // Download file first - const localUri = FileSystem.documentDirectory + filename; - const download = await FileSystem.downloadAsync(downloadUrl, localUri); - - // Share - if (await Sharing.isAvailableAsync()) { - await Sharing.shareAsync(download.uri); - } else { - Alert.alert('Error', 'Compartir no esta disponible en este dispositivo'); - } - } catch { - Alert.alert('Error', 'No se pudo compartir el archivo'); - } - }; - - const handleReset = () => { - setStep('select'); - setProgress(null); - setDownloadUrl(null); - setFilename(''); - setErrorMessage(null); - }; - - const renderFormatOption = (value: ExportFormat, label: string, description: string) => ( - setFormat(value)} - > - - - {format === value && } - - {label} - - {description} - - ); - - if (step === 'processing') { - return ( - - - - Generando exportacion... - {progress && ( - - Estado: {progress.status} - {progress.totalRows !== undefined && ` (${progress.totalRows} productos)`} - - )} - - - ); - } - - if (step === 'complete') { - return ( - - - - - - Exportacion lista - {filename} - - - - Descargar - - - - Compartir - - - - - Nueva exportacion - - - - ); - } - - if (step === 'error') { - return ( - - - - ! - - Error al exportar - {errorMessage} - - - Intentar de nuevo - - - - ); - } - - return ( - - - Formato de exportacion - {renderFormatOption( - 'CSV', - 'CSV', - 'Archivo de texto separado por comas. Compatible con Excel, Google Sheets y otros.', - )} - {renderFormatOption( - 'EXCEL', - 'Excel (.xlsx)', - 'Archivo de Excel con formato y estilos. Ideal para reportes profesionales.', - )} - - Filtros - setLowStockOnly(!lowStockOnly)} - > - - {lowStockOnly && } - - - Solo productos con stock bajo - - Incluir unicamente productos que necesitan reabastecimiento - - - - - - Que incluye el archivo? - - • Nombre del producto{'\n'} - • Cantidad en inventario{'\n'} - • Categoria{'\n'} - • Codigo de barras{'\n'} - • Precio y costo{'\n'} - • Fecha de ultima actualizacion - - - - - - - Exportar Inventario - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - }, - centerContent: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - }, - sectionTitle: { - fontSize: 14, - fontWeight: '600', - color: '#666', - marginBottom: 12, - textTransform: 'uppercase', - }, - sectionTitleMargin: { - marginTop: 24, - }, - optionCard: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 12, - borderWidth: 2, - borderColor: 'transparent', - }, - optionCardSelected: { - borderColor: '#2563eb', - backgroundColor: '#eff6ff', - }, - optionHeader: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 8, - }, - radio: { - width: 24, - height: 24, - borderRadius: 12, - borderWidth: 2, - borderColor: '#ccc', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - radioSelected: { - borderColor: '#2563eb', - }, - radioInner: { - width: 12, - height: 12, - borderRadius: 6, - backgroundColor: '#2563eb', - }, - optionLabel: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - }, - optionDescription: { - fontSize: 14, - color: '#666', - marginLeft: 36, - }, - checkboxRow: { - flexDirection: 'row', - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - }, - checkbox: { - width: 24, - height: 24, - borderRadius: 6, - borderWidth: 2, - borderColor: '#ccc', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - checkboxChecked: { - borderColor: '#2563eb', - backgroundColor: '#2563eb', - }, - checkboxCheck: { - color: '#fff', - fontSize: 14, - fontWeight: 'bold', - }, - checkboxContent: { - flex: 1, - }, - checkboxLabel: { - fontSize: 16, - fontWeight: '500', - color: '#1a1a1a', - marginBottom: 4, - }, - checkboxDescription: { - fontSize: 14, - color: '#666', - }, - infoCard: { - backgroundColor: '#eff6ff', - borderRadius: 12, - padding: 16, - marginTop: 24, - }, - infoTitle: { - fontSize: 14, - fontWeight: '600', - color: '#1e40af', - marginBottom: 8, - }, - infoText: { - fontSize: 14, - color: '#1e40af', - lineHeight: 22, - }, - footer: { - backgroundColor: '#fff', - padding: 16, - borderTopWidth: 1, - borderTopColor: '#eee', - }, - exportButton: { - backgroundColor: '#2563eb', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - }, - exportButtonDisabled: { - backgroundColor: '#93c5fd', - }, - exportButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - processingTitle: { - fontSize: 18, - fontWeight: '600', - color: '#1a1a1a', - marginTop: 24, - }, - processingStatus: { - fontSize: 14, - color: '#666', - marginTop: 8, - }, - successIcon: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: '#dcfce7', - justifyContent: 'center', - alignItems: 'center', - marginBottom: 24, - }, - successIconText: { - fontSize: 40, - color: '#22c55e', - }, - successTitle: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 8, - }, - successFilename: { - fontSize: 14, - color: '#666', - marginBottom: 32, - }, - actionButtons: { - width: '100%', - gap: 12, - marginBottom: 24, - }, - primaryButton: { - backgroundColor: '#2563eb', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - }, - primaryButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - secondaryButton: { - backgroundColor: '#f5f5f5', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - }, - secondaryButtonText: { - color: '#1a1a1a', - fontSize: 16, - fontWeight: '600', - }, - linkButton: { - paddingVertical: 12, - }, - linkButtonText: { - color: '#2563eb', - fontSize: 16, - fontWeight: '500', - }, - errorIcon: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: '#fef2f2', - justifyContent: 'center', - alignItems: 'center', - marginBottom: 24, - }, - errorIconText: { - fontSize: 40, - color: '#ef4444', - fontWeight: 'bold', - }, - errorTitle: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 8, - }, - errorMessage: { - fontSize: 14, - color: '#666', - textAlign: 'center', - marginBottom: 32, - }, -}); diff --git a/src/app/legal/privacy.tsx b/src/app/legal/privacy.tsx deleted file mode 100644 index bf4c7b0..0000000 --- a/src/app/legal/privacy.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { Stack } from 'expo-router'; - -export default function PrivacyScreen() { - return ( - - - - Ultima actualizacion: Enero 2025 - - - 1. Informacion que Recopilamos - - Recopilamos la siguiente informacion:{'\n\n'} - Informacion de cuenta:{'\n'} - - Numero de telefono{'\n'} - - Nombre{'\n'} - - Correo electronico (opcional){'\n\n'} - Informacion de negocio:{'\n'} - - Nombre de la tienda{'\n'} - - Ubicacion{'\n'} - - Giro del negocio{'\n\n'} - Videos e imagenes:{'\n'} - - Videos grabados para escaneo de inventario{'\n'} - - Imagenes extraidas para procesamiento de IA - - - - - 2. Como Usamos tu Informacion - - Usamos tu informacion para:{'\n\n'} - - Procesar y detectar productos en tus videos{'\n'} - - Gestionar tu cuenta y tiendas{'\n'} - - Procesar pagos de creditos{'\n'} - - Enviarte notificaciones sobre tu cuenta{'\n'} - - Mejorar nuestros servicios y algoritmos de IA{'\n'} - - Responder a tus solicitudes de soporte - - - - - 3. Almacenamiento de Videos - - Los videos que subes son procesados para detectar productos y luego se eliminan - automaticamente despues de 30 dias. Las imagenes extraidas para el procesamiento - de IA se eliminan inmediatamente despues del analisis. - - - - - 4. Compartir Informacion - - No vendemos tu informacion personal. Compartimos datos solo con:{'\n\n'} - - Stripe: Para procesar pagos de forma segura{'\n'} - - Proveedores de IA: OpenAI/Anthropic para detectar productos{'\n'} - - Firebase: Para enviar notificaciones push{'\n'} - - AWS/MinIO: Para almacenar videos temporalmente - - - - - 5. Seguridad - - Implementamos medidas de seguridad para proteger tu informacion:{'\n\n'} - - Conexiones cifradas (HTTPS/TLS){'\n'} - - Almacenamiento seguro de contrasenas (hash + salt){'\n'} - - Tokens de autenticacion JWT{'\n'} - - Acceso restringido a datos personales - - - - - 6. Tus Derechos - - Tienes derecho a:{'\n\n'} - - Acceder a tu informacion personal{'\n'} - - Corregir datos inexactos{'\n'} - - Solicitar la eliminacion de tu cuenta{'\n'} - - Revocar el consentimiento para notificaciones{'\n'} - - Exportar tus datos de inventario - - - - - 7. Retencion de Datos - - Conservamos tu informacion mientras mantengas una cuenta activa. Si eliminas tu cuenta, - eliminaremos tus datos personales dentro de 30 dias, excepto cuando la ley requiera - mantener ciertos registros. - - - - - 8. Cookies y Tecnologias - - Usamos tecnologias como almacenamiento local para mantener tu sesion activa y - recordar tus preferencias. No usamos cookies de terceros para publicidad. - - - - - 9. Menores de Edad - - MiInventario no esta dirigido a menores de 18 anos. No recopilamos intencionalmente - informacion de menores de edad. - - - - - 10. Cambios a esta Politica - - Podemos actualizar esta politica periodicamente. Te notificaremos sobre cambios - significativos a traves de la aplicacion o por correo electronico. - - - - - 11. Contacto - - Para preguntas sobre privacidad, contactanos en:{'\n'} - Email: privacidad@miinventario.com{'\n'} - Telefono: +52 55 1234 5678 - - - - - - Al usar MiInventario, aceptas esta Politica de Privacidad y el procesamiento de - tu informacion como se describe aqui. - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - paddingBottom: 32, - }, - lastUpdated: { - fontSize: 12, - color: '#666', - marginBottom: 20, - fontStyle: 'italic', - }, - section: { - marginBottom: 24, - }, - sectionTitle: { - fontSize: 16, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 8, - }, - paragraph: { - fontSize: 14, - color: '#444', - lineHeight: 22, - }, - bold: { - fontWeight: '600', - color: '#1a1a1a', - }, - footer: { - marginTop: 20, - padding: 16, - backgroundColor: '#f5f5f5', - borderRadius: 8, - }, - footerText: { - fontSize: 13, - color: '#666', - textAlign: 'center', - fontStyle: 'italic', - }, -}); diff --git a/src/app/legal/terms.tsx b/src/app/legal/terms.tsx deleted file mode 100644 index 6cdb371..0000000 --- a/src/app/legal/terms.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { Stack } from 'expo-router'; - -export default function TermsScreen() { - return ( - - - - Ultima actualizacion: Enero 2025 - - - 1. Aceptacion de Terminos - - Al descargar, instalar o usar la aplicacion MiInventario, aceptas estos Terminos y Condiciones. - Si no estas de acuerdo con alguno de estos terminos, no debes usar la aplicacion. - - - - - 2. Descripcion del Servicio - - MiInventario es una aplicacion movil que utiliza inteligencia artificial para ayudarte a - gestionar el inventario de tu negocio mediante el escaneo de video de tus productos. - - - - - 3. Registro y Cuenta - - Para usar MiInventario debes crear una cuenta proporcionando informacion veraz y actualizada. - Eres responsable de mantener la confidencialidad de tu cuenta y contrasena. - - - - - 4. Sistema de Creditos - - MiInventario utiliza un sistema de creditos para el procesamiento de videos:{'\n\n'} - - Cada escaneo de video consume 1 credito{'\n'} - - Los creditos comprados no son reembolsables{'\n'} - - Los creditos no tienen fecha de expiracion{'\n'} - - Los creditos no son transferibles entre cuentas - - - - - 5. Pagos - - Los pagos se procesan a traves de Stripe de forma segura. Aceptamos tarjetas de credito/debito - y pagos en efectivo a traves de OXXO. Los precios estan en pesos mexicanos (MXN) e incluyen IVA. - - - - - 6. Uso Aceptable - - Te comprometes a:{'\n\n'} - - Usar la aplicacion solo para fines legales{'\n'} - - No intentar acceder a cuentas de otros usuarios{'\n'} - - No interferir con el funcionamiento del servicio{'\n'} - - No usar la aplicacion para fines fraudulentos - - - - - 7. Propiedad Intelectual - - MiInventario y todo su contenido, caracteristicas y funcionalidad son propiedad de - MiInventario y estan protegidos por leyes de propiedad intelectual. - - - - - 8. Limitacion de Responsabilidad - - MiInventario se proporciona "tal cual" sin garantias de ningun tipo. No garantizamos - la precision del 100% en la deteccion de productos. Debes verificar la informacion generada - por la aplicacion. - - - - - 9. Modificaciones - - Nos reservamos el derecho de modificar estos terminos en cualquier momento. - Te notificaremos sobre cambios importantes a traves de la aplicacion o por correo electronico. - - - - - 10. Contacto - - Para preguntas sobre estos terminos, contactanos en:{'\n'} - Email: legal@miinventario.com{'\n'} - Telefono: +52 55 1234 5678 - - - - - - Al usar MiInventario, confirmas que has leido y aceptado estos Terminos y Condiciones. - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - paddingBottom: 32, - }, - lastUpdated: { - fontSize: 12, - color: '#666', - marginBottom: 20, - fontStyle: 'italic', - }, - section: { - marginBottom: 24, - }, - sectionTitle: { - fontSize: 16, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 8, - }, - paragraph: { - fontSize: 14, - color: '#444', - lineHeight: 22, - }, - footer: { - marginTop: 20, - padding: 16, - backgroundColor: '#f5f5f5', - borderRadius: 8, - }, - footerText: { - fontSize: 13, - color: '#666', - textAlign: 'center', - fontStyle: 'italic', - }, -}); diff --git a/src/app/notifications/_layout.tsx b/src/app/notifications/_layout.tsx deleted file mode 100644 index 02b718f..0000000 --- a/src/app/notifications/_layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Stack } from 'expo-router'; - -export default function NotificationsLayout() { - return ( - - - - ); -} diff --git a/src/app/notifications/index.tsx b/src/app/notifications/index.tsx deleted file mode 100644 index 1addd41..0000000 --- a/src/app/notifications/index.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import { - View, - Text, - StyleSheet, - FlatList, - TouchableOpacity, - RefreshControl, - ActivityIndicator, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useEffect, useState, useCallback } from 'react'; -import { router, Stack } from 'expo-router'; -import { useNotificationsStore } from '@stores/notifications.store'; -import { Notification, NotificationType } from '@services/api/notifications.service'; - -export default function NotificationsScreen() { - const { - notifications, - unreadCount, - hasMore, - fetchNotifications, - markAsRead, - markAllAsRead, - isLoading, - } = useNotificationsStore(); - const [refreshing, setRefreshing] = useState(false); - - useEffect(() => { - fetchNotifications(true); - }, [fetchNotifications]); - - const onRefresh = useCallback(async () => { - setRefreshing(true); - await fetchNotifications(true); - setRefreshing(false); - }, [fetchNotifications]); - - const loadMore = () => { - if (hasMore && !isLoading) { - fetchNotifications(false); - } - }; - - const handleNotificationPress = async (notification: Notification) => { - if (!notification.isRead) { - await markAsRead(notification.id); - } - - // Navigate based on notification type - const data = notification.data as Record | undefined; - switch (notification.type) { - case 'VIDEO_PROCESSING_COMPLETE': - if (data?.videoId && data?.storeId) { - router.push(`/inventory?storeId=${data.storeId}`); - } - break; - case 'PAYMENT_COMPLETE': - router.push('/credits/history'); - break; - case 'REFERRAL_BONUS': - router.push('/referrals'); - break; - case 'LOW_CREDITS': - router.push('/credits/buy'); - break; - default: - break; - } - }; - - const getNotificationIcon = (type: NotificationType) => { - switch (type) { - case 'VIDEO_PROCESSING_COMPLETE': - return '✅'; - case 'VIDEO_PROCESSING_FAILED': - return '❌'; - case 'LOW_CREDITS': - return '⚠️'; - case 'PAYMENT_COMPLETE': - return '💰'; - case 'PAYMENT_FAILED': - return '💳'; - case 'REFERRAL_BONUS': - return '🎁'; - default: - return '📢'; - } - }; - - const formatDate = (dateString: string) => { - const date = new Date(dateString); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / (1000 * 60)); - const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffMins < 60) { - return diffMins <= 1 ? 'Hace un momento' : `Hace ${diffMins} min`; - } - if (diffHours < 24) { - return diffHours === 1 ? 'Hace 1 hora' : `Hace ${diffHours} horas`; - } - if (diffDays < 7) { - return diffDays === 1 ? 'Ayer' : `Hace ${diffDays} dias`; - } - return date.toLocaleDateString('es-MX', { - day: 'numeric', - month: 'short', - }); - }; - - const renderNotification = ({ item }: { item: Notification }) => ( - handleNotificationPress(item)} - > - - - {getNotificationIcon(item.type)} - - - - - - {item.title} - - {!item.isRead && } - - - {item.body} - - {formatDate(item.createdAt)} - - - ); - - const EmptyState = () => ( - - 🔔 - Sin notificaciones - - Aqui veras las notificaciones sobre tus escaneos, pagos y referidos - - - ); - - return ( - <> - - unreadCount > 0 ? ( - - Marcar leidas - - ) : null, - }} - /> - - {isLoading && notifications.length === 0 ? ( - - - Cargando notificaciones... - - ) : notifications.length === 0 ? ( - - ) : ( - item.id} - contentContainerStyle={styles.list} - refreshControl={ - - } - onEndReached={loadMore} - onEndReachedThreshold={0.5} - ListFooterComponent={ - isLoading ? ( - - ) : null - } - ItemSeparatorComponent={() => } - /> - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - loadingText: { - marginTop: 12, - fontSize: 16, - color: '#666', - }, - markAllButton: { - marginRight: 8, - }, - markAllButtonText: { - color: '#2563eb', - fontSize: 14, - fontWeight: '600', - }, - list: { - padding: 16, - }, - notificationCard: { - flexDirection: 'row', - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - }, - notificationCardUnread: { - backgroundColor: '#f0f9ff', - }, - notificationIcon: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: '#f5f5f5', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - notificationIconText: { - fontSize: 20, - }, - notificationContent: { - flex: 1, - }, - notificationHeader: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 4, - }, - notificationTitle: { - flex: 1, - fontSize: 15, - fontWeight: '500', - color: '#1a1a1a', - }, - notificationTitleUnread: { - fontWeight: '600', - }, - unreadDot: { - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: '#2563eb', - marginLeft: 8, - }, - notificationBody: { - fontSize: 14, - color: '#666', - lineHeight: 20, - marginBottom: 4, - }, - notificationTime: { - fontSize: 12, - color: '#999', - }, - separator: { - height: 8, - }, - emptyState: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - }, - emptyIcon: { - fontSize: 64, - marginBottom: 16, - }, - emptyTitle: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 8, - }, - emptyDescription: { - fontSize: 16, - color: '#666', - textAlign: 'center', - }, - footerLoader: { - paddingVertical: 16, - }, -}); diff --git a/src/app/payments/methods.tsx b/src/app/payments/methods.tsx deleted file mode 100644 index 3da8c7c..0000000 --- a/src/app/payments/methods.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { Stack, router } from 'expo-router'; - -interface PaymentMethod { - id: string; - type: 'card' | 'oxxo' | '7eleven'; - name: string; - description: string; - icon: string; - available: boolean; -} - -const paymentMethods: PaymentMethod[] = [ - { - id: 'card', - type: 'card', - name: 'Tarjeta de Credito/Debito', - description: 'Visa, Mastercard, American Express', - icon: '💳', - available: true, - }, - { - id: 'oxxo', - type: 'oxxo', - name: 'OXXO', - description: 'Paga en efectivo en cualquier OXXO', - icon: '🏪', - available: true, - }, - { - id: '7eleven', - type: '7eleven', - name: '7-Eleven', - description: 'Proximamente disponible', - icon: '🏬', - available: false, - }, -]; - -export default function PaymentMethodsScreen() { - return ( - - - - - Metodos Disponibles - - Selecciona tu metodo de pago preferido al comprar creditos - - - - - {paymentMethods.map((method) => ( - - - {method.icon} - - - - {method.name} - - {method.description} - - {method.available ? ( - - - - ) : ( - - Pronto - - )} - - ))} - - - - Sobre los pagos - - - 🔒 - - Pagos Seguros - - Todos los pagos son procesados de forma segura a traves de Stripe - - - - - - - - Creditos Instantaneos - - Los creditos se acreditan inmediatamente al pagar con tarjeta - - - - - - 🏪 - - Pago en Efectivo - - Recibe un voucher para pagar en OXXO. Los creditos se acreditan en 24-48 horas - - - - - - router.push('/credits/buy')} - > - Comprar Creditos - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - scroll: { - flex: 1, - }, - section: { - padding: 16, - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - sectionTitle: { - fontSize: 20, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 4, - }, - sectionDescription: { - fontSize: 14, - color: '#666', - }, - methodsList: { - padding: 16, - gap: 12, - }, - methodCard: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#fff', - padding: 16, - borderRadius: 12, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - methodCardDisabled: { - opacity: 0.6, - }, - methodIcon: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: '#f5f5f5', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - methodIconText: { - fontSize: 24, - }, - methodInfo: { - flex: 1, - }, - methodName: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 2, - }, - methodNameDisabled: { - color: '#999', - }, - methodDescription: { - fontSize: 13, - color: '#666', - }, - checkmark: { - width: 28, - height: 28, - borderRadius: 14, - backgroundColor: '#22c55e', - justifyContent: 'center', - alignItems: 'center', - }, - checkmarkText: { - color: '#fff', - fontSize: 14, - fontWeight: 'bold', - }, - comingSoon: { - backgroundColor: '#f5f5f5', - paddingHorizontal: 10, - paddingVertical: 4, - borderRadius: 12, - }, - comingSoonText: { - fontSize: 12, - color: '#999', - fontWeight: '500', - }, - infoSection: { - margin: 16, - marginTop: 8, - padding: 16, - backgroundColor: '#fff', - borderRadius: 12, - }, - infoTitle: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 16, - }, - infoItem: { - flexDirection: 'row', - marginBottom: 16, - }, - infoIcon: { - fontSize: 20, - marginRight: 12, - }, - infoContent: { - flex: 1, - }, - infoLabel: { - fontSize: 14, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 2, - }, - infoText: { - fontSize: 13, - color: '#666', - lineHeight: 18, - }, - buyButton: { - margin: 16, - marginTop: 8, - backgroundColor: '#2563eb', - paddingVertical: 16, - borderRadius: 12, - alignItems: 'center', - }, - buyButtonText: { - fontSize: 16, - fontWeight: '600', - color: '#fff', - }, -}); diff --git a/src/app/profile/edit.tsx b/src/app/profile/edit.tsx deleted file mode 100644 index dfe6618..0000000 --- a/src/app/profile/edit.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import { - View, - Text, - StyleSheet, - TextInput, - TouchableOpacity, - ScrollView, - KeyboardAvoidingView, - Platform, - Alert, - ActivityIndicator, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { router, Stack } from 'expo-router'; -import { useState, useEffect } from 'react'; -import { useAuthStore } from '@stores/auth.store'; -import { usersService, UpdateProfileRequest } from '@services/api/users.service'; - -export default function EditProfileScreen() { - const { user, setUser } = useAuthStore(); - const [name, setName] = useState(user?.name || ''); - const [email, setEmail] = useState(user?.email || ''); - const [isLoading, setIsLoading] = useState(false); - const [isFetching, setIsFetching] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - loadProfile(); - }, []); - - const loadProfile = async () => { - try { - const profile = await usersService.getProfile(); - setName(profile.name); - setEmail(profile.email || ''); - } catch (err) { - console.error('Error loading profile:', err); - } finally { - setIsFetching(false); - } - }; - - const validateEmail = (email: string) => { - if (!email) return true; // Email is optional - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - }; - - const handleSave = async () => { - setError(null); - - if (!name.trim()) { - setError('El nombre es requerido'); - return; - } - - if (!validateEmail(email)) { - setError('Email invalido'); - return; - } - - setIsLoading(true); - - try { - const updateData: UpdateProfileRequest = { - name: name.trim(), - }; - - if (email.trim()) { - updateData.email = email.trim(); - } - - const updatedProfile = await usersService.updateProfile(updateData); - - setUser({ - id: updatedProfile.id, - phone: updatedProfile.phone, - name: updatedProfile.name, - email: updatedProfile.email, - }); - - Alert.alert('Exito', 'Perfil actualizado correctamente', [ - { text: 'OK', onPress: () => router.back() }, - ]); - } catch (err) { - setError(err instanceof Error ? err.message : 'Error al actualizar perfil'); - } finally { - setIsLoading(false); - } - }; - - if (isFetching) { - return ( - - - - - - - ); - } - - return ( - - - - - - - - {name?.charAt(0).toUpperCase() || 'U'} - - - - - - - Nombre - - - - - Telefono - - {user?.phone} - - - El numero de telefono no puede ser modificado - - - - - Email (opcional) - - - - {error && ( - - {error} - - )} - - - - - router.back()} - disabled={isLoading} - > - Cancelar - - - {isLoading ? ( - - ) : ( - Guardar - )} - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - keyboardView: { - flex: 1, - }, - scroll: { - flex: 1, - }, - scrollContent: { - paddingBottom: 20, - }, - avatarContainer: { - alignItems: 'center', - paddingVertical: 24, - backgroundColor: '#fff', - }, - avatar: { - width: 100, - height: 100, - borderRadius: 50, - backgroundColor: '#2563eb', - justifyContent: 'center', - alignItems: 'center', - }, - avatarText: { - fontSize: 40, - fontWeight: 'bold', - color: '#fff', - }, - form: { - padding: 16, - }, - inputGroup: { - marginBottom: 20, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#666', - marginBottom: 8, - }, - input: { - backgroundColor: '#fff', - paddingHorizontal: 16, - paddingVertical: 12, - borderRadius: 8, - fontSize: 16, - borderWidth: 1, - borderColor: '#e5e5e5', - }, - disabledInput: { - backgroundColor: '#f5f5f5', - paddingHorizontal: 16, - paddingVertical: 12, - borderRadius: 8, - borderWidth: 1, - borderColor: '#e5e5e5', - }, - disabledInputText: { - fontSize: 16, - color: '#999', - }, - helperText: { - fontSize: 12, - color: '#999', - marginTop: 4, - }, - errorContainer: { - backgroundColor: '#fef2f2', - padding: 12, - borderRadius: 8, - marginTop: 8, - }, - errorText: { - color: '#ef4444', - fontSize: 14, - }, - footer: { - flexDirection: 'row', - padding: 16, - backgroundColor: '#fff', - borderTopWidth: 1, - borderTopColor: '#eee', - gap: 12, - }, - cancelButton: { - flex: 1, - paddingVertical: 14, - borderRadius: 8, - alignItems: 'center', - backgroundColor: '#f5f5f5', - }, - cancelButtonText: { - fontSize: 16, - fontWeight: '600', - color: '#666', - }, - saveButton: { - flex: 1, - paddingVertical: 14, - borderRadius: 8, - alignItems: 'center', - backgroundColor: '#2563eb', - }, - saveButtonDisabled: { - opacity: 0.6, - }, - saveButtonText: { - fontSize: 16, - fontWeight: '600', - color: '#fff', - }, -}); diff --git a/src/app/referrals/_layout.tsx b/src/app/referrals/_layout.tsx deleted file mode 100644 index b6f51b0..0000000 --- a/src/app/referrals/_layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Stack } from 'expo-router'; - -export default function ReferralsLayout() { - return ( - - - - ); -} diff --git a/src/app/referrals/index.tsx b/src/app/referrals/index.tsx deleted file mode 100644 index 11e1b92..0000000 --- a/src/app/referrals/index.tsx +++ /dev/null @@ -1,460 +0,0 @@ -import { - View, - Text, - StyleSheet, - FlatList, - TouchableOpacity, - RefreshControl, - ActivityIndicator, - Share, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useEffect, useState, useCallback } from 'react'; -import * as Clipboard from 'expo-clipboard'; -import { useReferralsStore } from '@stores/referrals.store'; -import { Referral } from '@services/api/referrals.service'; - -export default function ReferralsScreen() { - const { - stats, - referrals, - hasMore, - fetchStats, - fetchReferrals, - isLoading, - } = useReferralsStore(); - const [refreshing, setRefreshing] = useState(false); - const [copied, setCopied] = useState(false); - - useEffect(() => { - fetchStats(); - fetchReferrals(true); - }, [fetchStats, fetchReferrals]); - - const onRefresh = useCallback(async () => { - setRefreshing(true); - await Promise.all([fetchStats(), fetchReferrals(true)]); - setRefreshing(false); - }, [fetchStats, fetchReferrals]); - - const loadMore = () => { - if (hasMore && !isLoading) { - fetchReferrals(false); - } - }; - - const copyCode = async () => { - if (stats?.referralCode) { - await Clipboard.setStringAsync(stats.referralCode); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } - }; - - const shareCode = async () => { - if (stats?.referralCode) { - try { - await Share.share({ - message: `Te invito a usar MiInventario, la app que te ayuda a controlar tu inventario con solo grabar un video. Usa mi codigo ${stats.referralCode} y ambos ganaremos creditos gratis!`, - }); - } catch { - // User cancelled - } - } - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'REWARDED': - return '#22c55e'; - case 'QUALIFIED': - return '#3b82f6'; - case 'REGISTERED': - return '#f59e0b'; - default: - return '#9ca3af'; - } - }; - - const getStatusLabel = (status: string) => { - switch (status) { - case 'REWARDED': - return 'Completado'; - case 'QUALIFIED': - return 'Calificado'; - case 'REGISTERED': - return 'Registrado'; - default: - return 'Pendiente'; - } - }; - - const formatDate = (dateString?: string) => { - if (!dateString) return ''; - const date = new Date(dateString); - return date.toLocaleDateString('es-MX', { - day: 'numeric', - month: 'short', - year: 'numeric', - }); - }; - - const renderReferral = ({ item }: { item: Referral }) => ( - - - - {item.referred?.name?.charAt(0).toUpperCase() || '?'} - - - - - {item.referred?.name || 'Usuario'} - - - Registrado: {formatDate(item.registeredAt || item.createdAt)} - - - - - - {getStatusLabel(item.status)} - - - {item.status === 'REWARDED' && ( - +{item.referrerBonusCredits} - )} - - - ); - - const ListHeader = () => ( - - {/* Share Card */} - - Tu codigo de referido - - {stats?.referralCode || '---'} - - - - {copied ? '✓' : '📋'} - - {copied ? 'Copiado!' : 'Copiar'} - - - - 📤 - - Compartir - - - - - - {/* Stats */} - - - {stats?.totalReferrals ?? 0} - Invitados - - - {stats?.completedReferrals ?? 0} - Completados - - - - {stats?.totalCreditsEarned ?? 0} - - Creditos - - - - {/* How it works */} - - Como funciona - - - 1 - - Comparte tu codigo con amigos - - - - 2 - - Tu amigo se registra con el codigo - - - - 3 - - - Ambos reciben 5 creditos cuando tu amigo hace su primer escaneo - - - - - {referrals.length > 0 && ( - Tus referidos - )} - - ); - - const EmptyReferrals = () => ( - - 👥 - Sin referidos aun - - Comparte tu codigo y empieza a ganar creditos - - - ); - - return ( - - item.id} - ListHeaderComponent={ListHeader} - ListEmptyComponent={ - isLoading ? null : - } - contentContainerStyle={styles.list} - refreshControl={ - - } - onEndReached={loadMore} - onEndReachedThreshold={0.5} - ListFooterComponent={ - isLoading && referrals.length > 0 ? ( - - ) : null - } - ItemSeparatorComponent={() => } - /> - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - list: { - padding: 16, - }, - header: { - marginBottom: 16, - }, - shareCard: { - backgroundColor: '#2563eb', - borderRadius: 16, - padding: 20, - marginBottom: 16, - }, - shareTitle: { - color: 'rgba(255,255,255,0.8)', - fontSize: 14, - textAlign: 'center', - marginBottom: 8, - }, - codeContainer: { - backgroundColor: 'rgba(255,255,255,0.15)', - borderRadius: 8, - paddingVertical: 12, - marginBottom: 16, - }, - codeText: { - color: '#fff', - fontSize: 28, - fontWeight: 'bold', - textAlign: 'center', - letterSpacing: 3, - }, - shareActions: { - flexDirection: 'row', - gap: 12, - }, - shareButton: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: 'rgba(255,255,255,0.15)', - borderRadius: 8, - paddingVertical: 12, - gap: 8, - }, - shareButtonPrimary: { - backgroundColor: '#fff', - }, - shareButtonIcon: { - fontSize: 16, - }, - shareButtonText: { - color: '#fff', - fontWeight: '600', - }, - shareButtonTextPrimary: { - color: '#2563eb', - }, - statsContainer: { - flexDirection: 'row', - gap: 12, - marginBottom: 16, - }, - statCard: { - flex: 1, - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - alignItems: 'center', - }, - statValue: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - }, - statValueHighlight: { - color: '#22c55e', - }, - statLabel: { - fontSize: 12, - color: '#666', - marginTop: 4, - }, - howItWorks: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 16, - }, - howItWorksTitle: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 12, - }, - step: { - flexDirection: 'row', - alignItems: 'flex-start', - marginBottom: 12, - }, - stepNumber: { - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: '#2563eb', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - stepNumberText: { - color: '#fff', - fontSize: 12, - fontWeight: 'bold', - }, - stepText: { - flex: 1, - fontSize: 14, - color: '#666', - lineHeight: 20, - }, - listTitle: { - fontSize: 18, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 12, - }, - referralCard: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - }, - referralAvatar: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: '#e0e7ff', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - referralAvatarText: { - fontSize: 18, - fontWeight: 'bold', - color: '#4f46e5', - }, - referralInfo: { - flex: 1, - }, - referralName: { - fontSize: 16, - fontWeight: '500', - color: '#1a1a1a', - }, - referralDate: { - fontSize: 13, - color: '#666', - marginTop: 2, - }, - referralStatus: { - alignItems: 'flex-end', - }, - statusBadge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 6, - }, - statusBadgeText: { - fontSize: 12, - fontWeight: '600', - }, - bonusText: { - fontSize: 14, - fontWeight: 'bold', - color: '#22c55e', - marginTop: 4, - }, - separator: { - height: 8, - }, - emptyReferrals: { - alignItems: 'center', - paddingVertical: 32, - }, - emptyIcon: { - fontSize: 48, - marginBottom: 12, - }, - emptyTitle: { - fontSize: 18, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 4, - }, - emptyDescription: { - fontSize: 14, - color: '#666', - }, - footerLoader: { - paddingVertical: 16, - }, -}); diff --git a/src/app/reports/_layout.tsx b/src/app/reports/_layout.tsx deleted file mode 100644 index 3e2bb84..0000000 --- a/src/app/reports/_layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Stack } from 'expo-router'; - -export default function ReportsLayout() { - return ( - - - - - - - ); -} diff --git a/src/app/reports/categories.tsx b/src/app/reports/categories.tsx deleted file mode 100644 index dd4f041..0000000 --- a/src/app/reports/categories.tsx +++ /dev/null @@ -1,479 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - ActivityIndicator, - TouchableOpacity, - RefreshControl, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useState, useEffect, useCallback } from 'react'; -import { useStoresStore } from '@stores/stores.store'; -import { reportsService, CategoriesReport, CategoryDetail } from '@services/api/reports.service'; - -const CATEGORY_COLORS = [ - '#3b82f6', - '#22c55e', - '#f59e0b', - '#ef4444', - '#8b5cf6', - '#06b6d4', - '#ec4899', - '#84cc16', -]; - -export default function CategoriesReportScreen() { - const { currentStore } = useStoresStore(); - const [report, setReport] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isRefreshing, setIsRefreshing] = useState(false); - const [error, setError] = useState(null); - const [expandedCategory, setExpandedCategory] = useState(null); - - const fetchReport = useCallback(async (showRefresh = false) => { - if (!currentStore) return; - - if (showRefresh) { - setIsRefreshing(true); - } else { - setIsLoading(true); - } - setError(null); - - try { - const data = await reportsService.getCategoriesReport(currentStore.id); - setReport(data); - } catch (err) { - setError(err instanceof Error ? err.message : 'Error al cargar reporte'); - } finally { - setIsLoading(false); - setIsRefreshing(false); - } - }, [currentStore]); - - useEffect(() => { - fetchReport(); - }, [fetchReport]); - - const formatCurrency = (value: number) => { - return `$${value.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; - }; - - const formatPercent = (value: number) => { - return `${value.toFixed(1)}%`; - }; - - const toggleCategory = (name: string) => { - setExpandedCategory(expandedCategory === name ? null : name); - }; - - const renderCategoryBar = (categories: CategoryDetail[]) => { - return ( - - {categories.map((cat, index) => ( - - ))} - - ); - }; - - const renderCategoryCard = (category: CategoryDetail, index: number) => { - const isExpanded = expandedCategory === category.name; - const color = CATEGORY_COLORS[index % CATEGORY_COLORS.length]; - - return ( - toggleCategory(category.name)} - activeOpacity={0.7} - > - - - - - {category.name || 'Sin categoria'} - {category.itemCount} productos - - - - {formatPercent(category.percentOfTotal)} - {isExpanded ? '▲' : '▼'} - - - - {isExpanded && ( - - - - Valor total - {formatCurrency(category.totalValue)} - - - Precio promedio - {formatCurrency(category.averagePrice)} - - - - {category.lowStockCount > 0 && ( - - - - {category.lowStockCount} productos con stock bajo - - - - )} - - {category.topItems.length > 0 && ( - - Productos principales: - {category.topItems.map((item, i) => ( - - {item.name} - x{item.quantity} - - ))} - - )} - - )} - - ); - }; - - if (isLoading) { - return ( - - - - - - ); - } - - if (error) { - return ( - - - {error} - fetchReport()}> - Reintentar - - - - ); - } - - if (!report) { - return ( - - - No hay datos disponibles - - - ); - } - - return ( - - fetchReport(true)} /> - } - > - {/* Summary Card */} - - - - {report.summary.totalCategories} - Categorias - - - - {report.summary.totalItems} - Productos - - - - {formatCurrency(report.summary.totalValue)} - Valor Total - - - - - {/* Distribution Bar */} - Distribucion - {renderCategoryBar(report.categories)} - - {/* Legend */} - - {report.categories.slice(0, 4).map((cat, index) => ( - - - {cat.name || 'Sin cat.'} - - ))} - {report.categories.length > 4 && ( - +{report.categories.length - 4} mas - )} - - - {/* Category Cards */} - Desglose por categoria - {report.categories.map((category, index) => renderCategoryCard(category, index))} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - errorContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - }, - errorText: { - fontSize: 16, - color: '#ef4444', - textAlign: 'center', - marginBottom: 16, - }, - retryButton: { - backgroundColor: '#2563eb', - borderRadius: 8, - paddingHorizontal: 24, - paddingVertical: 12, - }, - retryButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - emptyContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - emptyText: { - fontSize: 16, - color: '#666', - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - }, - summaryCard: { - backgroundColor: '#fff', - borderRadius: 16, - padding: 20, - marginBottom: 24, - }, - summaryStats: { - flexDirection: 'row', - alignItems: 'center', - }, - summaryStat: { - flex: 1, - alignItems: 'center', - }, - summaryDivider: { - width: 1, - height: 40, - backgroundColor: '#e5e5e5', - }, - summaryStatValue: { - fontSize: 20, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 4, - }, - summaryStatLabel: { - fontSize: 12, - color: '#666', - }, - sectionTitle: { - fontSize: 14, - fontWeight: '600', - color: '#666', - marginBottom: 12, - textTransform: 'uppercase', - }, - sectionTitleMargin: { - marginTop: 16, - }, - barContainer: { - flexDirection: 'row', - height: 24, - borderRadius: 12, - overflow: 'hidden', - backgroundColor: '#e5e5e5', - }, - barSegment: { - height: '100%', - }, - legend: { - flexDirection: 'row', - flexWrap: 'wrap', - marginTop: 12, - gap: 12, - }, - legendItem: { - flexDirection: 'row', - alignItems: 'center', - }, - legendDot: { - width: 12, - height: 12, - borderRadius: 6, - marginRight: 6, - }, - legendText: { - fontSize: 12, - color: '#666', - maxWidth: 80, - }, - legendMore: { - fontSize: 12, - color: '#666', - fontStyle: 'italic', - }, - categoryCard: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 8, - }, - categoryHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - categoryLeft: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - }, - categoryDot: { - width: 16, - height: 16, - borderRadius: 8, - marginRight: 12, - }, - categoryInfo: { - flex: 1, - }, - categoryName: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - }, - categoryCount: { - fontSize: 12, - color: '#666', - }, - categoryRight: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - categoryPercent: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - }, - expandIcon: { - fontSize: 10, - color: '#666', - }, - categoryExpanded: { - marginTop: 16, - paddingTop: 16, - borderTopWidth: 1, - borderTopColor: '#f5f5f5', - }, - statRow: { - flexDirection: 'row', - marginBottom: 12, - }, - stat: { - flex: 1, - }, - statLabel: { - fontSize: 12, - color: '#666', - marginBottom: 4, - }, - statValue: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - }, - alertRow: { - marginBottom: 12, - }, - alertBadge: { - backgroundColor: '#fef2f2', - paddingHorizontal: 12, - paddingVertical: 6, - borderRadius: 6, - alignSelf: 'flex-start', - }, - alertBadgeText: { - fontSize: 12, - color: '#ef4444', - fontWeight: '500', - }, - topItems: { - backgroundColor: '#f9fafb', - borderRadius: 8, - padding: 12, - }, - topItemsTitle: { - fontSize: 12, - fontWeight: '600', - color: '#666', - marginBottom: 8, - }, - topItem: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingVertical: 4, - }, - topItemName: { - fontSize: 14, - color: '#1a1a1a', - flex: 1, - marginRight: 12, - }, - topItemQuantity: { - fontSize: 14, - color: '#666', - fontWeight: '500', - }, -}); diff --git a/src/app/reports/index.tsx b/src/app/reports/index.tsx deleted file mode 100644 index 2de0ec5..0000000 --- a/src/app/reports/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { router } from 'expo-router'; - -interface ReportCardProps { - title: string; - description: string; - icon: string; - route: string; - color: string; -} - -const ReportCard = ({ title, description, icon, route, color }: ReportCardProps) => ( - router.push(route as any)} - activeOpacity={0.7} - > - - {icon} - - - {title} - {description} - - - -); - -export default function ReportsIndexScreen() { - return ( - - - Reportes disponibles - - - - - - - - - Exportar reportes - - Todos los reportes pueden exportarse en formato CSV o Excel desde la - pantalla de cada reporte. - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - }, - sectionTitle: { - fontSize: 14, - fontWeight: '600', - color: '#666', - marginBottom: 12, - textTransform: 'uppercase', - }, - card: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 12, - }, - iconContainer: { - width: 48, - height: 48, - borderRadius: 12, - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - icon: { - fontSize: 24, - }, - cardContent: { - flex: 1, - }, - cardTitle: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 4, - }, - cardDescription: { - fontSize: 14, - color: '#666', - }, - chevron: { - fontSize: 24, - color: '#ccc', - marginLeft: 8, - }, - infoCard: { - backgroundColor: '#eff6ff', - borderRadius: 12, - padding: 16, - marginTop: 12, - }, - infoTitle: { - fontSize: 14, - fontWeight: '600', - color: '#1e40af', - marginBottom: 4, - }, - infoText: { - fontSize: 14, - color: '#1e40af', - lineHeight: 20, - }, -}); diff --git a/src/app/reports/movements.tsx b/src/app/reports/movements.tsx deleted file mode 100644 index 20a756e..0000000 --- a/src/app/reports/movements.tsx +++ /dev/null @@ -1,371 +0,0 @@ -import { - View, - Text, - StyleSheet, - FlatList, - ActivityIndicator, - TouchableOpacity, - RefreshControl, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useState, useEffect, useCallback } from 'react'; -import { useStoresStore } from '@stores/stores.store'; -import { reportsService, MovementsReport, MovementRecord } from '@services/api/reports.service'; - -const MOVEMENT_TYPES: Record = { - DETECTION: { label: 'Deteccion', color: '#2563eb', bgColor: '#dbeafe' }, - MANUAL_ADJUST: { label: 'Ajuste', color: '#7c3aed', bgColor: '#ede9fe' }, - SALE: { label: 'Venta', color: '#ef4444', bgColor: '#fef2f2' }, - PURCHASE: { label: 'Compra', color: '#22c55e', bgColor: '#dcfce7' }, -}; - -export default function MovementsReportScreen() { - const { currentStore } = useStoresStore(); - const [report, setReport] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isRefreshing, setIsRefreshing] = useState(false); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const [error, setError] = useState(null); - const [page, setPage] = useState(1); - - const fetchReport = useCallback(async (pageNum = 1, refresh = false) => { - if (!currentStore) return; - - if (pageNum === 1) { - if (refresh) { - setIsRefreshing(true); - } else { - setIsLoading(true); - } - } else { - setIsLoadingMore(true); - } - setError(null); - - try { - const data = await reportsService.getMovementsReport(currentStore.id, { - page: pageNum, - limit: 20, - }); - - if (pageNum === 1) { - setReport(data); - } else if (report) { - setReport({ - ...data, - movements: [...report.movements, ...data.movements], - }); - } - setPage(pageNum); - } catch (err) { - setError(err instanceof Error ? err.message : 'Error al cargar reporte'); - } finally { - setIsLoading(false); - setIsRefreshing(false); - setIsLoadingMore(false); - } - }, [currentStore, report]); - - useEffect(() => { - fetchReport(); - }, [currentStore]); - - const handleLoadMore = () => { - if (!report?.hasMore || isLoadingMore) return; - fetchReport(page + 1); - }; - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString('es-MX', { - day: 'numeric', - month: 'short', - hour: '2-digit', - minute: '2-digit', - }); - }; - - const renderMovementItem = ({ item }: { item: MovementRecord }) => { - const typeConfig = MOVEMENT_TYPES[item.type] || MOVEMENT_TYPES.MANUAL_ADJUST; - const isPositive = item.change > 0; - - return ( - - - - - {typeConfig.label} - - - {formatDate(item.date)} - - {item.itemName} - - - - {item.quantityBefore} → {item.quantityAfter} - - - - {isPositive ? '+' : ''}{item.change} - - - {item.reason && ( - {item.reason} - )} - - ); - }; - - const renderHeader = () => { - if (!report) return null; - - return ( - - {/* Summary Card */} - - - - {report.summary.totalMovements} - Movimientos - - - - = 0 ? styles.changePositive : styles.changeNegative, - ]}> - {report.summary.netChange >= 0 ? '+' : ''}{report.summary.netChange} - - Cambio neto - - - - - - +{report.summary.itemsIncreased} - - Aumentos - - - - - -{report.summary.itemsDecreased} - - Disminuciones - - - - - Historial de movimientos - - ); - }; - - const renderFooter = () => { - if (!isLoadingMore) return null; - return ( - - - - ); - }; - - if (isLoading) { - return ( - - - - - - ); - } - - if (error) { - return ( - - - {error} - fetchReport()}> - Reintentar - - - - ); - } - - return ( - - item.id} - ListHeaderComponent={renderHeader} - ListFooterComponent={renderFooter} - contentContainerStyle={styles.content} - refreshControl={ - fetchReport(1, true)} /> - } - onEndReached={handleLoadMore} - onEndReachedThreshold={0.3} - ListEmptyComponent={ - - No hay movimientos registrados - - } - /> - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - errorContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - }, - errorText: { - fontSize: 16, - color: '#ef4444', - textAlign: 'center', - marginBottom: 16, - }, - retryButton: { - backgroundColor: '#2563eb', - borderRadius: 8, - paddingHorizontal: 24, - paddingVertical: 12, - }, - retryButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - emptyContainer: { - paddingVertical: 48, - alignItems: 'center', - }, - emptyText: { - fontSize: 16, - color: '#666', - }, - content: { - padding: 16, - }, - headerSection: { - marginBottom: 8, - }, - summaryCard: { - backgroundColor: '#fff', - borderRadius: 16, - padding: 20, - marginBottom: 24, - }, - summaryRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 16, - }, - summaryItem: { - flex: 1, - alignItems: 'center', - }, - summaryDivider: { - width: 1, - height: 40, - backgroundColor: '#e5e5e5', - }, - summaryValue: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 4, - }, - summaryLabel: { - fontSize: 12, - color: '#666', - }, - sectionTitle: { - fontSize: 14, - fontWeight: '600', - color: '#666', - marginBottom: 12, - textTransform: 'uppercase', - }, - movementCard: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 8, - }, - movementHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, - }, - typeBadge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 4, - }, - typeBadgeText: { - fontSize: 12, - fontWeight: '600', - }, - movementDate: { - fontSize: 12, - color: '#666', - }, - movementItem: { - fontSize: 16, - fontWeight: '500', - color: '#1a1a1a', - marginBottom: 8, - }, - movementDetails: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - quantityChange: { - flexDirection: 'row', - alignItems: 'center', - }, - quantityLabel: { - fontSize: 14, - color: '#666', - }, - changeValue: { - fontSize: 18, - fontWeight: '700', - }, - changePositive: { - color: '#22c55e', - }, - changeNegative: { - color: '#ef4444', - }, - reasonText: { - fontSize: 12, - color: '#666', - fontStyle: 'italic', - marginTop: 8, - }, - loadingMore: { - paddingVertical: 16, - alignItems: 'center', - }, -}); diff --git a/src/app/reports/valuation.tsx b/src/app/reports/valuation.tsx deleted file mode 100644 index 265182e..0000000 --- a/src/app/reports/valuation.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - ActivityIndicator, - TouchableOpacity, - RefreshControl, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useState, useEffect, useCallback } from 'react'; -import { router } from 'expo-router'; -import { useStoresStore } from '@stores/stores.store'; -import { reportsService, ValuationReport } from '@services/api/reports.service'; - -export default function ValuationReportScreen() { - const { currentStore } = useStoresStore(); - const [report, setReport] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isRefreshing, setIsRefreshing] = useState(false); - const [error, setError] = useState(null); - - const fetchReport = useCallback(async (showRefresh = false) => { - if (!currentStore) return; - - if (showRefresh) { - setIsRefreshing(true); - } else { - setIsLoading(true); - } - setError(null); - - try { - const data = await reportsService.getValuationReport(currentStore.id); - setReport(data); - } catch (err) { - setError(err instanceof Error ? err.message : 'Error al cargar reporte'); - } finally { - setIsLoading(false); - setIsRefreshing(false); - } - }, [currentStore]); - - useEffect(() => { - fetchReport(); - }, [fetchReport]); - - const formatCurrency = (value: number) => { - return `$${value.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; - }; - - const formatPercent = (value: number) => { - return `${value.toFixed(1)}%`; - }; - - const handleExport = () => { - router.push('/inventory/export' as any); - }; - - if (isLoading) { - return ( - - - - - - ); - } - - if (error) { - return ( - - - {error} - fetchReport()}> - Reintentar - - - - ); - } - - if (!report) { - return ( - - - No hay datos disponibles - - - ); - } - - return ( - - fetchReport(true)} /> - } - > - {/* Summary Card */} - - Valor Total del Inventario - {formatCurrency(report.summary.totalPrice)} - - - Costo - {formatCurrency(report.summary.totalCost)} - - - - Margen - - {formatPercent(report.summary.potentialMarginPercent)} - - - - {report.summary.totalItems} productos - - - {/* By Category */} - Por Categoria - {report.byCategory.map((cat, index) => ( - - - {cat.category || 'Sin categoria'} - {cat.itemCount} productos - - - - Valor - {formatCurrency(cat.totalPrice)} - - - Costo - {formatCurrency(cat.totalCost)} - - - Margen - - {formatCurrency(cat.margin)} - - - - - ))} - - {/* Top Items */} - Top Productos por Valor - {report.items.slice(0, 10).map((item, index) => ( - - - {index + 1} - - - {item.name} - {item.category || 'Sin categoria'} - - - {formatCurrency(item.totalPrice)} - x{item.quantity} - - - ))} - - - - - Exportar Reporte - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - errorContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - }, - errorText: { - fontSize: 16, - color: '#ef4444', - textAlign: 'center', - marginBottom: 16, - }, - retryButton: { - backgroundColor: '#2563eb', - borderRadius: 8, - paddingHorizontal: 24, - paddingVertical: 12, - }, - retryButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - emptyContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - emptyText: { - fontSize: 16, - color: '#666', - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - }, - summaryCard: { - backgroundColor: '#1e40af', - borderRadius: 16, - padding: 24, - marginBottom: 24, - }, - summaryTitle: { - fontSize: 14, - color: 'rgba(255,255,255,0.8)', - marginBottom: 8, - }, - summaryValue: { - fontSize: 36, - fontWeight: 'bold', - color: '#fff', - marginBottom: 24, - }, - summaryRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 16, - }, - summaryItem: { - flex: 1, - alignItems: 'center', - }, - summaryDivider: { - width: 1, - height: 40, - backgroundColor: 'rgba(255,255,255,0.3)', - }, - summaryItemLabel: { - fontSize: 12, - color: 'rgba(255,255,255,0.7)', - marginBottom: 4, - }, - summaryItemValue: { - fontSize: 18, - fontWeight: '600', - color: '#fff', - }, - marginValue: { - color: '#4ade80', - }, - totalItems: { - fontSize: 12, - color: 'rgba(255,255,255,0.6)', - textAlign: 'center', - }, - sectionTitle: { - fontSize: 14, - fontWeight: '600', - color: '#666', - marginBottom: 12, - textTransform: 'uppercase', - }, - categoryCard: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 12, - }, - categoryHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 12, - }, - categoryName: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - }, - categoryCount: { - fontSize: 14, - color: '#666', - }, - categoryStats: { - flexDirection: 'row', - }, - categoryStat: { - flex: 1, - }, - categoryStatLabel: { - fontSize: 12, - color: '#666', - marginBottom: 4, - }, - categoryStatValue: { - fontSize: 14, - fontWeight: '600', - color: '#1a1a1a', - }, - itemRow: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#fff', - borderRadius: 8, - padding: 12, - marginBottom: 8, - }, - itemRank: { - width: 28, - height: 28, - borderRadius: 14, - backgroundColor: '#f5f5f5', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - itemRankText: { - fontSize: 12, - fontWeight: '600', - color: '#666', - }, - itemInfo: { - flex: 1, - }, - itemName: { - fontSize: 14, - fontWeight: '500', - color: '#1a1a1a', - }, - itemCategory: { - fontSize: 12, - color: '#666', - }, - itemValue: { - alignItems: 'flex-end', - }, - itemValueText: { - fontSize: 14, - fontWeight: '600', - color: '#1a1a1a', - }, - itemQuantity: { - fontSize: 12, - color: '#666', - }, - footer: { - backgroundColor: '#fff', - padding: 16, - borderTopWidth: 1, - borderTopColor: '#eee', - }, - exportButton: { - backgroundColor: '#2563eb', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - }, - exportButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, -}); diff --git a/src/app/stores/[id].tsx b/src/app/stores/[id].tsx deleted file mode 100644 index affc7ad..0000000 --- a/src/app/stores/[id].tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TextInput, - TouchableOpacity, - ActivityIndicator, - Alert, - KeyboardAvoidingView, - Platform, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useState, useEffect } from 'react'; -import { router, useLocalSearchParams } from 'expo-router'; -import { useStoresStore } from '@stores/stores.store'; - -export default function EditStoreScreen() { - const { id } = useLocalSearchParams<{ id: string }>(); - const { stores, updateStore, deleteStore, isLoading, error } = useStoresStore(); - const [name, setName] = useState(''); - const [address, setAddress] = useState(''); - const [city, setCity] = useState(''); - const [giro, setGiro] = useState(''); - const [isLoadingData, setIsLoadingData] = useState(true); - - useEffect(() => { - const store = stores.find((s) => s.id === id); - if (store) { - setName(store.name); - setAddress(store.address || ''); - setCity(store.city || ''); - setGiro(store.giro || ''); - } - setIsLoadingData(false); - }, [id, stores]); - - const handleUpdate = async () => { - if (!name.trim()) { - Alert.alert('Error', 'El nombre de la tienda es requerido'); - return; - } - - if (!id) return; - - const store = await updateStore(id, { - name: name.trim(), - address: address.trim() || undefined, - city: city.trim() || undefined, - giro: giro.trim() || undefined, - }); - - if (store) { - Alert.alert('Listo', 'La tienda ha sido actualizada', [ - { text: 'OK', onPress: () => router.back() }, - ]); - } - }; - - const handleDelete = () => { - Alert.alert( - 'Eliminar Tienda', - 'Estas seguro de eliminar esta tienda? Esta accion no se puede deshacer y perderas todo el inventario asociado.', - [ - { text: 'Cancelar', style: 'cancel' }, - { - text: 'Eliminar', - style: 'destructive', - onPress: async () => { - if (!id) return; - const success = await deleteStore(id); - if (success) { - Alert.alert('Listo', 'La tienda ha sido eliminada', [ - { text: 'OK', onPress: () => router.back() }, - ]); - } - }, - }, - ] - ); - }; - - if (isLoadingData) { - return ( - - - - - - ); - } - - return ( - - - - - Nombre de la tienda * - - - - - Direccion - - - - - Ciudad - - - - - Giro - - - - {error && ( - - {error} - - )} - - - Eliminar Tienda - - - - - - {isLoading ? ( - - ) : ( - Guardar Cambios - )} - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - keyboardAvoid: { - flex: 1, - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - }, - formGroup: { - marginBottom: 20, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 8, - }, - input: { - backgroundColor: '#fff', - borderRadius: 12, - paddingHorizontal: 16, - paddingVertical: 14, - fontSize: 16, - color: '#1a1a1a', - borderWidth: 1, - borderColor: '#e5e5e5', - }, - errorCard: { - backgroundColor: '#fef2f2', - borderRadius: 12, - padding: 16, - marginBottom: 16, - borderWidth: 1, - borderColor: '#fecaca', - }, - errorText: { - color: '#ef4444', - fontSize: 14, - }, - deleteButton: { - alignItems: 'center', - paddingVertical: 16, - marginTop: 16, - }, - deleteButtonText: { - color: '#ef4444', - fontSize: 16, - fontWeight: '600', - }, - footer: { - backgroundColor: '#fff', - padding: 16, - borderTopWidth: 1, - borderTopColor: '#eee', - }, - saveButton: { - backgroundColor: '#2563eb', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - }, - saveButtonDisabled: { - backgroundColor: '#93c5fd', - }, - saveButtonText: { - color: '#fff', - fontSize: 18, - fontWeight: '600', - }, -}); diff --git a/src/app/stores/_layout.tsx b/src/app/stores/_layout.tsx deleted file mode 100644 index d65a481..0000000 --- a/src/app/stores/_layout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Stack } from 'expo-router'; - -export default function StoresLayout() { - return ( - - - - - - ); -} diff --git a/src/app/stores/index.tsx b/src/app/stores/index.tsx deleted file mode 100644 index 0765b60..0000000 --- a/src/app/stores/index.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import { - View, - Text, - StyleSheet, - FlatList, - TouchableOpacity, - RefreshControl, - ActivityIndicator, - Alert, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useEffect, useState, useCallback } from 'react'; -import { router, Stack } from 'expo-router'; -import { useStoresStore } from '@stores/stores.store'; -import { Store } from '@services/api/stores.service'; - -export default function StoresScreen() { - const { - stores, - currentStore, - hasMore, - fetchStores, - selectStore, - deleteStore, - isLoading, - } = useStoresStore(); - const [refreshing, setRefreshing] = useState(false); - - useEffect(() => { - fetchStores(true); - }, [fetchStores]); - - const onRefresh = useCallback(async () => { - setRefreshing(true); - await fetchStores(true); - setRefreshing(false); - }, [fetchStores]); - - const loadMore = () => { - if (hasMore && !isLoading) { - fetchStores(false); - } - }; - - const handleSelectStore = (store: Store) => { - selectStore(store); - router.back(); - }; - - const handleDeleteStore = (store: Store) => { - Alert.alert( - 'Eliminar Tienda', - `Estas seguro de eliminar "${store.name}"? Esta accion no se puede deshacer.`, - [ - { text: 'Cancelar', style: 'cancel' }, - { - text: 'Eliminar', - style: 'destructive', - onPress: async () => { - const success = await deleteStore(store.id); - if (success) { - Alert.alert('Listo', 'La tienda ha sido eliminada'); - } - }, - }, - ] - ); - }; - - const renderStore = ({ item }: { item: Store }) => ( - handleSelectStore(item)} - onLongPress={() => handleDeleteStore(item)} - > - - 🏪 - - - {item.name} - {item.address && ( - - {item.address} - - )} - - {currentStore?.id === item.id && ( - - Activa - - )} - router.push(`/stores/${item.id}`)} - > - ✏️ - - - ); - - const EmptyState = () => ( - - 🏪 - Sin tiendas - - Crea tu primera tienda para comenzar a usar MiInventario - - router.push('/stores/new')} - > - Crear Tienda - - - ); - - return ( - <> - ( - router.push('/stores/new')} - > - + Nueva - - ), - }} - /> - - {isLoading && stores.length === 0 ? ( - - - Cargando tiendas... - - ) : stores.length === 0 ? ( - - ) : ( - <> - - Toca para seleccionar, manten presionado para eliminar - - item.id} - contentContainerStyle={styles.list} - refreshControl={ - - } - onEndReached={loadMore} - onEndReachedThreshold={0.5} - ListFooterComponent={ - isLoading ? ( - - ) : null - } - ItemSeparatorComponent={() => } - /> - - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - loadingText: { - marginTop: 12, - fontSize: 16, - color: '#666', - }, - addButton: { - marginRight: 8, - }, - addButtonText: { - color: '#2563eb', - fontSize: 16, - fontWeight: '600', - }, - hint: { - fontSize: 13, - color: '#666', - textAlign: 'center', - paddingVertical: 12, - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - list: { - padding: 16, - }, - storeCard: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - borderWidth: 2, - borderColor: 'transparent', - }, - storeCardActive: { - borderColor: '#2563eb', - backgroundColor: '#f0f9ff', - }, - storeIcon: { - width: 48, - height: 48, - borderRadius: 12, - backgroundColor: '#f5f5f5', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - storeIconText: { - fontSize: 24, - }, - storeInfo: { - flex: 1, - }, - storeName: { - fontSize: 17, - fontWeight: '600', - color: '#1a1a1a', - }, - storeAddress: { - fontSize: 14, - color: '#666', - marginTop: 2, - }, - activeBadge: { - backgroundColor: '#dcfce7', - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 6, - marginRight: 8, - }, - activeBadgeText: { - color: '#16a34a', - fontSize: 12, - fontWeight: '600', - }, - editButton: { - padding: 8, - }, - editButtonText: { - fontSize: 18, - }, - separator: { - height: 8, - }, - emptyState: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - }, - emptyIcon: { - fontSize: 64, - marginBottom: 16, - }, - emptyTitle: { - fontSize: 24, - fontWeight: 'bold', - color: '#1a1a1a', - marginBottom: 8, - }, - emptyDescription: { - fontSize: 16, - color: '#666', - textAlign: 'center', - marginBottom: 24, - }, - emptyButton: { - backgroundColor: '#2563eb', - paddingVertical: 12, - paddingHorizontal: 24, - borderRadius: 8, - }, - emptyButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, - footerLoader: { - paddingVertical: 16, - }, -}); diff --git a/src/app/stores/new.tsx b/src/app/stores/new.tsx deleted file mode 100644 index db0c901..0000000 --- a/src/app/stores/new.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TextInput, - TouchableOpacity, - ActivityIndicator, - Alert, - KeyboardAvoidingView, - Platform, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useState } from 'react'; -import { router } from 'expo-router'; -import { useStoresStore } from '@stores/stores.store'; - -export default function NewStoreScreen() { - const { createStore, isLoading, error } = useStoresStore(); - const [name, setName] = useState(''); - const [address, setAddress] = useState(''); - const [city, setCity] = useState(''); - const [giro, setGiro] = useState(''); - - const handleCreate = async () => { - if (!name.trim()) { - Alert.alert('Error', 'El nombre de la tienda es requerido'); - return; - } - - const store = await createStore({ - name: name.trim(), - address: address.trim() || undefined, - city: city.trim() || undefined, - giro: giro.trim() || undefined, - }); - - if (store) { - Alert.alert('Listo', 'Tu tienda ha sido creada', [ - { text: 'OK', onPress: () => router.back() }, - ]); - } - }; - - return ( - - - - - Nombre de la tienda * - - - - - Direccion - - - - - Ciudad - - - - - Giro - - - - {error && ( - - {error} - - )} - - - 💡 - - Puedes agregar mas tiendas despues desde tu perfil. Cada tienda - tiene su propio inventario independiente. - - - - - - - {isLoading ? ( - - ) : ( - Crear Tienda - )} - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - keyboardAvoid: { - flex: 1, - }, - scroll: { - flex: 1, - }, - content: { - padding: 16, - }, - formGroup: { - marginBottom: 20, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 8, - }, - input: { - backgroundColor: '#fff', - borderRadius: 12, - paddingHorizontal: 16, - paddingVertical: 14, - fontSize: 16, - color: '#1a1a1a', - borderWidth: 1, - borderColor: '#e5e5e5', - }, - errorCard: { - backgroundColor: '#fef2f2', - borderRadius: 12, - padding: 16, - marginBottom: 16, - borderWidth: 1, - borderColor: '#fecaca', - }, - errorText: { - color: '#ef4444', - fontSize: 14, - }, - infoCard: { - flexDirection: 'row', - backgroundColor: '#f0f9ff', - borderRadius: 12, - padding: 16, - borderWidth: 1, - borderColor: '#bfdbfe', - }, - infoIcon: { - fontSize: 20, - marginRight: 12, - }, - infoText: { - flex: 1, - fontSize: 14, - color: '#1e40af', - lineHeight: 20, - }, - footer: { - backgroundColor: '#fff', - padding: 16, - borderTopWidth: 1, - borderTopColor: '#eee', - }, - createButton: { - backgroundColor: '#2563eb', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - }, - createButtonDisabled: { - backgroundColor: '#93c5fd', - }, - createButtonText: { - color: '#fff', - fontSize: 18, - fontWeight: '600', - }, -}); diff --git a/src/app/support/index.tsx b/src/app/support/index.tsx deleted file mode 100644 index 0a3afff..0000000 --- a/src/app/support/index.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import { - View, - Text, - StyleSheet, - ScrollView, - TextInput, - TouchableOpacity, - Alert, - Linking, - KeyboardAvoidingView, - Platform, - ActivityIndicator, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { Stack, router } from 'expo-router'; -import { useState } from 'react'; -import { useAuthStore } from '@stores/auth.store'; - -type ContactMethod = 'whatsapp' | 'email' | 'form'; - -export default function SupportScreen() { - const { user } = useAuthStore(); - const [subject, setSubject] = useState(''); - const [message, setMessage] = useState(''); - const [isSending, setIsSending] = useState(false); - - const handleWhatsApp = () => { - const phone = '5215512345678'; // Replace with actual support number - const text = `Hola, necesito ayuda con MiInventario.\n\nUsuario: ${user?.name}\nTelefono: ${user?.phone}`; - Linking.openURL(`whatsapp://send?phone=${phone}&text=${encodeURIComponent(text)}`); - }; - - const handleEmail = () => { - const email = 'soporte@miinventario.com'; - const emailSubject = 'Soporte MiInventario'; - const body = `\n\n---\nUsuario: ${user?.name}\nTelefono: ${user?.phone}`; - Linking.openURL(`mailto:${email}?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(body)}`); - }; - - const handleSubmit = async () => { - if (!subject.trim() || !message.trim()) { - Alert.alert('Error', 'Por favor completa todos los campos'); - return; - } - - setIsSending(true); - - // Simulate sending the message - await new Promise((resolve) => setTimeout(resolve, 1500)); - - setIsSending(false); - Alert.alert( - 'Mensaje Enviado', - 'Hemos recibido tu mensaje. Te responderemos lo antes posible.', - [{ text: 'OK', onPress: () => router.back() }] - ); - }; - - const ContactCard = ({ - icon, - title, - description, - onPress, - }: { - icon: string; - title: string; - description: string; - onPress: () => void; - }) => ( - - - {icon} - - - {title} - {description} - - - - ); - - return ( - - - - - - Necesitas ayuda? - - Estamos aqui para ayudarte. Elige como prefieres contactarnos. - - - - - Contacto Rapido - - - - - - - - Enviar Mensaje - - - Asunto - - - - - Mensaje - - - - - {isSending ? ( - - ) : ( - Enviar Mensaje - )} - - - - - - - Horario de Atencion - - Lunes a Viernes: 9:00 AM - 6:00 PM{'\n'} - Sabado: 9:00 AM - 2:00 PM - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - keyboardView: { - flex: 1, - }, - scroll: { - flex: 1, - }, - header: { - padding: 20, - backgroundColor: '#2563eb', - }, - headerTitle: { - fontSize: 24, - fontWeight: 'bold', - color: '#fff', - marginBottom: 4, - }, - headerSubtitle: { - fontSize: 14, - color: 'rgba(255,255,255,0.8)', - }, - section: { - padding: 16, - }, - sectionTitle: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 12, - }, - contactCards: { - gap: 12, - }, - contactCard: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#fff', - padding: 16, - borderRadius: 12, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - contactIcon: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: '#f0f7ff', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - contactIconText: { - fontSize: 24, - }, - contactInfo: { - flex: 1, - }, - contactTitle: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 2, - }, - contactDescription: { - fontSize: 13, - color: '#666', - }, - contactArrow: { - fontSize: 20, - color: '#ccc', - }, - form: { - backgroundColor: '#fff', - padding: 16, - borderRadius: 12, - }, - inputGroup: { - marginBottom: 16, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#666', - marginBottom: 8, - }, - input: { - backgroundColor: '#f5f5f5', - paddingHorizontal: 16, - paddingVertical: 12, - borderRadius: 8, - fontSize: 16, - borderWidth: 1, - borderColor: '#e5e5e5', - }, - textArea: { - minHeight: 120, - paddingTop: 12, - }, - submitButton: { - backgroundColor: '#2563eb', - paddingVertical: 14, - borderRadius: 8, - alignItems: 'center', - marginTop: 8, - }, - submitButtonDisabled: { - opacity: 0.6, - }, - submitButtonText: { - fontSize: 16, - fontWeight: '600', - color: '#fff', - }, - infoSection: { - margin: 16, - padding: 20, - backgroundColor: '#fff', - borderRadius: 12, - alignItems: 'center', - }, - infoIcon: { - fontSize: 32, - marginBottom: 8, - }, - infoTitle: { - fontSize: 16, - fontWeight: '600', - color: '#1a1a1a', - marginBottom: 8, - }, - infoText: { - fontSize: 14, - color: '#666', - textAlign: 'center', - lineHeight: 20, - }, -}); diff --git a/src/app/validation/_layout.tsx b/src/app/validation/_layout.tsx deleted file mode 100644 index 245a0ac..0000000 --- a/src/app/validation/_layout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Stack } from 'expo-router'; - -export default function ValidationLayout() { - return ( - - - - - ); -} diff --git a/src/app/validation/complete.tsx b/src/app/validation/complete.tsx deleted file mode 100644 index 48251a3..0000000 --- a/src/app/validation/complete.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import React, { useEffect } from 'react'; -import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; -import { useRouter } from 'expo-router'; -import { Ionicons } from '@expo/vector-icons'; -import { useValidationsStore } from '../../stores/validations.store'; - -export default function ValidationCompleteScreen() { - const router = useRouter(); - const { creditsRewarded, reset } = useValidationsStore(); - - useEffect(() => { - return () => { - reset(); - }; - }, []); - - const handleContinue = () => { - router.replace('/'); - }; - - return ( - - - - - - - Gracias! - Tu validacion nos ayuda a mejorar - - {creditsRewarded !== null && creditsRewarded > 0 && ( - - - - Recompensa - +{creditsRewarded} credito - - - )} - - - Con tu ayuda: - - - - Mejoramos la deteccion de productos - - - - - - Entrenamos mejor nuestros modelos - - - - - - Tu inventario sera mas preciso - - - - - - - - Continuar - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - }, - content: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - padding: 24, - }, - iconContainer: { - width: 120, - height: 120, - borderRadius: 60, - backgroundColor: '#e8f5e9', - justifyContent: 'center', - alignItems: 'center', - marginBottom: 24, - }, - title: { - fontSize: 28, - fontWeight: 'bold', - marginBottom: 8, - }, - subtitle: { - fontSize: 16, - color: '#666', - marginBottom: 24, - textAlign: 'center', - }, - rewardCard: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#fff8e1', - padding: 16, - borderRadius: 12, - marginBottom: 32, - width: '100%', - gap: 16, - }, - rewardInfo: { - flex: 1, - }, - rewardLabel: { - fontSize: 12, - color: '#666', - }, - rewardValue: { - fontSize: 20, - fontWeight: 'bold', - color: '#f0ad4e', - }, - benefits: { - width: '100%', - }, - benefitsTitle: { - fontSize: 14, - color: '#666', - marginBottom: 12, - }, - benefitItem: { - flexDirection: 'row', - alignItems: 'center', - gap: 12, - marginBottom: 8, - }, - benefitText: { - fontSize: 14, - color: '#333', - flex: 1, - }, - footer: { - padding: 16, - borderTopWidth: 1, - borderTopColor: '#eee', - }, - button: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: '#007AFF', - padding: 16, - borderRadius: 8, - gap: 8, - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: '600', - }, -}); diff --git a/src/app/validation/items.tsx b/src/app/validation/items.tsx deleted file mode 100644 index 87b2507..0000000 --- a/src/app/validation/items.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import React, { useEffect } from 'react'; -import { - View, - Text, - ScrollView, - TouchableOpacity, - StyleSheet, - ActivityIndicator, - Alert, -} from 'react-native'; -import { useRouter } from 'expo-router'; -import { Ionicons } from '@expo/vector-icons'; -import { useValidationsStore } from '../../stores/validations.store'; -import { ValidationItemCard } from '../../components/validation/ValidationItemCard'; -import { ValidationProgressBar } from '../../components/validation/ValidationProgressBar'; -import { ValidationItemResponse } from '../../services/api/validations.service'; - -export default function ValidationItemsScreen() { - const router = useRouter(); - const { - pendingRequest, - items, - responses, - currentItemIndex, - isLoading, - error, - addResponse, - nextItem, - previousItem, - submitValidation, - skipValidation, - } = useValidationsStore(); - - useEffect(() => { - if (!pendingRequest || items.length === 0) { - router.replace('/'); - } - }, [pendingRequest, items]); - - const handleResponse = (response: Omit) => { - if (!items[currentItemIndex]) return; - - addResponse({ - ...response, - inventoryItemId: items[currentItemIndex].id, - }); - - // Auto-advance to next item - if (currentItemIndex < items.length - 1) { - setTimeout(() => nextItem(), 300); - } - }; - - const handleSubmit = async () => { - if (responses.length < items.length) { - Alert.alert( - 'Faltan items', - 'Por favor valida todos los productos antes de continuar.', - ); - return; - } - - try { - await submitValidation(); - router.replace('/validation/complete'); - } catch { - Alert.alert('Error', 'No se pudo enviar la validacion. Intenta de nuevo.'); - } - }; - - const handleSkip = () => { - Alert.alert( - 'Omitir validacion', - 'Estas seguro? No recibiras el credito de recompensa.', - [ - { text: 'Cancelar', style: 'cancel' }, - { - text: 'Omitir', - style: 'destructive', - onPress: async () => { - await skipValidation(); - router.replace('/'); - }, - }, - ], - ); - }; - - if (!pendingRequest || items.length === 0) { - return ( - - - - ); - } - - const currentItem = items[currentItemIndex]; - const currentResponse = responses.find( - (r) => r.inventoryItemId === currentItem?.id, - ); - - return ( - - - - - {currentItem && ( - - )} - - {error && ( - - {error} - - )} - - - - - - - - Anterior - - - - - - Siguiente - - - - - - - - Omitir - - - - {isLoading ? ( - - ) : ( - <> - Enviar - - - {responses.length}/{items.length} - - - - )} - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loading: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - content: { - flex: 1, - }, - scrollContent: { - padding: 16, - }, - errorContainer: { - backgroundColor: '#ffebee', - padding: 12, - borderRadius: 8, - marginTop: 12, - }, - errorText: { - color: '#dc3545', - textAlign: 'center', - }, - footer: { - backgroundColor: '#fff', - borderTopWidth: 1, - borderTopColor: '#eee', - padding: 16, - }, - navigation: { - flexDirection: 'row', - justifyContent: 'space-between', - marginBottom: 16, - }, - navButton: { - flexDirection: 'row', - alignItems: 'center', - padding: 8, - gap: 4, - }, - navButtonDisabled: { - opacity: 0.5, - }, - navText: { - color: '#007AFF', - fontSize: 16, - }, - navTextDisabled: { - color: '#ccc', - }, - actions: { - flexDirection: 'row', - gap: 12, - }, - skipButton: { - flex: 1, - padding: 14, - borderRadius: 8, - backgroundColor: '#f5f5f5', - alignItems: 'center', - }, - skipText: { - color: '#666', - fontWeight: '600', - }, - submitButton: { - flex: 2, - padding: 14, - borderRadius: 8, - backgroundColor: '#007AFF', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 8, - }, - submitButtonDisabled: { - backgroundColor: '#ccc', - }, - submitText: { - color: '#fff', - fontWeight: '600', - fontSize: 16, - }, - badge: { - backgroundColor: 'rgba(255,255,255,0.3)', - paddingHorizontal: 8, - paddingVertical: 2, - borderRadius: 10, - }, - badgeText: { - color: '#fff', - fontSize: 12, - fontWeight: '600', - }, -}); diff --git a/src/components/feedback/ConfirmItemButton.tsx b/src/components/feedback/ConfirmItemButton.tsx deleted file mode 100644 index f4846c3..0000000 --- a/src/components/feedback/ConfirmItemButton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import { TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { useFeedbackStore } from '../../stores/feedback.store'; - -interface Props { - storeId: string; - itemId: string; - onSuccess?: () => void; -} - -export function ConfirmItemButton({ storeId, itemId, onSuccess }: Props) { - const { confirmItem, isLoading } = useFeedbackStore(); - - const handleConfirm = async () => { - try { - await confirmItem(storeId, itemId); - onSuccess?.(); - } catch { - // Error is handled in store - } - }; - - return ( - - {isLoading ? ( - - ) : ( - <> - - Confirmar - - )} - - ); -} - -const styles = StyleSheet.create({ - button: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#e8f5e9', - paddingVertical: 10, - paddingHorizontal: 16, - borderRadius: 8, - gap: 8, - }, - text: { - color: '#28a745', - fontWeight: '600', - fontSize: 14, - }, -}); diff --git a/src/components/feedback/CorrectQuantityModal.tsx b/src/components/feedback/CorrectQuantityModal.tsx deleted file mode 100644 index e07f29e..0000000 --- a/src/components/feedback/CorrectQuantityModal.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - Modal, - TextInput, - TouchableOpacity, - StyleSheet, - ActivityIndicator, -} from 'react-native'; -import { useFeedbackStore } from '../../stores/feedback.store'; - -interface Props { - visible: boolean; - onClose: () => void; - storeId: string; - itemId: string; - currentQuantity: number; - itemName: string; - onSuccess?: () => void; -} - -export function CorrectQuantityModal({ - visible, - onClose, - storeId, - itemId, - currentQuantity, - itemName, - onSuccess, -}: Props) { - const [quantity, setQuantity] = useState(currentQuantity.toString()); - const [reason, setReason] = useState(''); - const { correctQuantity, isLoading, error } = useFeedbackStore(); - - const handleSubmit = async () => { - const newQuantity = parseInt(quantity, 10); - if (isNaN(newQuantity) || newQuantity < 0) return; - - try { - await correctQuantity(storeId, itemId, { - quantity: newQuantity, - reason: reason || undefined, - }); - onSuccess?.(); - onClose(); - } catch { - // Error is handled in store - } - }; - - const handleClose = () => { - setQuantity(currentQuantity.toString()); - setReason(''); - onClose(); - }; - - return ( - - - - Corregir Cantidad - {itemName} - - - Cantidad actual: - {currentQuantity} - - - Nueva cantidad: - - - Razon (opcional): - - - {error && {error}} - - - - Cancelar - - - {isLoading ? ( - - ) : ( - Guardar - )} - - - - - - ); -} - -const styles = StyleSheet.create({ - overlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.5)', - justifyContent: 'center', - alignItems: 'center', - }, - container: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 20, - width: '90%', - maxWidth: 400, - }, - title: { - fontSize: 20, - fontWeight: 'bold', - marginBottom: 4, - }, - subtitle: { - fontSize: 14, - color: '#666', - marginBottom: 16, - }, - currentValue: { - flexDirection: 'row', - backgroundColor: '#f5f5f5', - padding: 12, - borderRadius: 8, - marginBottom: 16, - }, - label: { - fontSize: 14, - color: '#666', - }, - value: { - fontSize: 14, - fontWeight: 'bold', - marginLeft: 8, - }, - inputLabel: { - fontSize: 14, - color: '#333', - marginBottom: 4, - }, - input: { - borderWidth: 1, - borderColor: '#ddd', - borderRadius: 8, - padding: 12, - fontSize: 16, - marginBottom: 12, - }, - textArea: { - height: 60, - textAlignVertical: 'top', - }, - error: { - color: '#dc3545', - fontSize: 14, - marginBottom: 12, - }, - buttons: { - flexDirection: 'row', - gap: 12, - }, - button: { - flex: 1, - padding: 14, - borderRadius: 8, - alignItems: 'center', - }, - cancelButton: { - backgroundColor: '#f5f5f5', - }, - submitButton: { - backgroundColor: '#007AFF', - }, - cancelText: { - color: '#333', - fontWeight: '600', - }, - submitText: { - color: '#fff', - fontWeight: '600', - }, -}); diff --git a/src/components/feedback/CorrectSkuModal.tsx b/src/components/feedback/CorrectSkuModal.tsx deleted file mode 100644 index f374551..0000000 --- a/src/components/feedback/CorrectSkuModal.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - Modal, - TextInput, - TouchableOpacity, - StyleSheet, - ActivityIndicator, -} from 'react-native'; -import { useFeedbackStore } from '../../stores/feedback.store'; - -interface Props { - visible: boolean; - onClose: () => void; - storeId: string; - itemId: string; - currentName: string; - currentCategory?: string; - currentBarcode?: string; - onSuccess?: () => void; -} - -export function CorrectSkuModal({ - visible, - onClose, - storeId, - itemId, - currentName, - currentCategory, - currentBarcode, - onSuccess, -}: Props) { - const [name, setName] = useState(currentName); - const [category, setCategory] = useState(currentCategory || ''); - const [barcode, setBarcode] = useState(currentBarcode || ''); - const [reason, setReason] = useState(''); - const { correctSku, isLoading, error } = useFeedbackStore(); - - const handleSubmit = async () => { - if (!name.trim()) return; - - try { - await correctSku(storeId, itemId, { - name: name.trim(), - category: category.trim() || undefined, - barcode: barcode.trim() || undefined, - reason: reason.trim() || undefined, - }); - onSuccess?.(); - onClose(); - } catch { - // Error is handled in store - } - }; - - const handleClose = () => { - setName(currentName); - setCategory(currentCategory || ''); - setBarcode(currentBarcode || ''); - setReason(''); - onClose(); - }; - - return ( - - - - Corregir Producto - - - Nombre actual: - {currentName} - - - Nombre correcto: - - - Categoria (opcional): - - - Codigo de barras (opcional): - - - Razon (opcional): - - - {error && {error}} - - - - Cancelar - - - {isLoading ? ( - - ) : ( - Guardar - )} - - - - - - ); -} - -const styles = StyleSheet.create({ - overlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.5)', - justifyContent: 'center', - alignItems: 'center', - }, - container: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 20, - width: '90%', - maxWidth: 400, - maxHeight: '90%', - }, - title: { - fontSize: 20, - fontWeight: 'bold', - marginBottom: 16, - }, - currentValue: { - backgroundColor: '#f5f5f5', - padding: 12, - borderRadius: 8, - marginBottom: 16, - }, - label: { - fontSize: 12, - color: '#666', - marginBottom: 4, - }, - value: { - fontSize: 14, - fontWeight: 'bold', - }, - inputLabel: { - fontSize: 14, - color: '#333', - marginBottom: 4, - }, - input: { - borderWidth: 1, - borderColor: '#ddd', - borderRadius: 8, - padding: 12, - fontSize: 16, - marginBottom: 12, - }, - textArea: { - height: 60, - textAlignVertical: 'top', - }, - error: { - color: '#dc3545', - fontSize: 14, - marginBottom: 12, - }, - buttons: { - flexDirection: 'row', - gap: 12, - }, - button: { - flex: 1, - padding: 14, - borderRadius: 8, - alignItems: 'center', - }, - cancelButton: { - backgroundColor: '#f5f5f5', - }, - submitButton: { - backgroundColor: '#007AFF', - }, - cancelText: { - color: '#333', - fontWeight: '600', - }, - submitText: { - color: '#fff', - fontWeight: '600', - }, -}); diff --git a/src/components/feedback/CorrectionHistoryCard.tsx b/src/components/feedback/CorrectionHistoryCard.tsx deleted file mode 100644 index 081dc38..0000000 --- a/src/components/feedback/CorrectionHistoryCard.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet } from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { CorrectionHistoryItem } from '../../services/api/feedback.service'; - -interface Props { - correction: CorrectionHistoryItem; -} - -const typeIcons: Record = { - QUANTITY: 'calculator', - SKU: 'pricetag', - CONFIRMATION: 'checkmark-circle', -}; - -const typeLabels: Record = { - QUANTITY: 'Cantidad', - SKU: 'Nombre/SKU', - CONFIRMATION: 'Confirmacion', -}; - -export function CorrectionHistoryCard({ correction }: Props) { - const icon = typeIcons[correction.type] || 'create'; - const label = typeLabels[correction.type] || correction.type; - const date = new Date(correction.createdAt); - - const renderChange = () => { - if (correction.type === 'CONFIRMATION') { - return Item confirmado como correcto; - } - - if (correction.type === 'QUANTITY') { - return ( - - {correction.previousValue.quantity} → {correction.newValue.quantity} - - ); - } - - if (correction.type === 'SKU') { - return ( - - - "{correction.previousValue.name}" → "{correction.newValue.name}" - - {correction.newValue.category !== correction.previousValue.category && ( - - Categoria: {correction.newValue.category} - - )} - - ); - } - - return null; - }; - - return ( - - - - - - - {label} - - {date.toLocaleDateString('es-MX', { - day: 'numeric', - month: 'short', - hour: '2-digit', - minute: '2-digit', - })} - - - {renderChange()} - {correction.reason && ( - "{correction.reason}" - )} - {correction.user && ( - Por: {correction.user.name} - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - backgroundColor: '#fff', - borderRadius: 8, - padding: 12, - marginBottom: 8, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.1, - shadowRadius: 2, - elevation: 2, - }, - iconContainer: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: '#e3f2fd', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - content: { - flex: 1, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 4, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#333', - }, - date: { - fontSize: 12, - color: '#999', - }, - change: { - fontSize: 14, - color: '#666', - }, - subChange: { - fontSize: 12, - color: '#999', - marginTop: 2, - }, - reason: { - fontSize: 12, - color: '#666', - fontStyle: 'italic', - marginTop: 4, - }, - user: { - fontSize: 12, - color: '#999', - marginTop: 4, - }, -}); diff --git a/src/components/skeletons/CreditCardSkeleton.tsx b/src/components/skeletons/CreditCardSkeleton.tsx deleted file mode 100644 index 6bfef65..0000000 --- a/src/components/skeletons/CreditCardSkeleton.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import { Skeleton, SkeletonText } from '../ui/Skeleton'; -import { useTheme } from '../../theme/ThemeContext'; - -/** - * Skeleton para tarjeta de balance de créditos - */ -export function CreditBalanceSkeleton() { - const { colors } = useTheme(); - - return ( - - - - - - - - - - - - - - - ); -} - -/** - * Skeleton para transacción - */ -export function TransactionSkeleton() { - const { colors } = useTheme(); - - return ( - - - - - - - - - - - ); -} - -/** - * Lista de skeletons de transacciones - */ -export function TransactionListSkeleton({ count = 5 }: { count?: number }) { - return ( - - {Array.from({ length: count }).map((_, index) => ( - - ))} - - ); -} - -/** - * Skeleton para paquete de créditos - */ -export function CreditPackageSkeleton() { - const { colors } = useTheme(); - - return ( - - - - - - - - - ); -} - -/** - * Lista de skeletons de paquetes - */ -export function CreditPackageListSkeleton({ count = 4 }: { count?: number }) { - return ( - - {Array.from({ length: count }).map((_, index) => ( - - ))} - - ); -} - -const styles = StyleSheet.create({ - balanceCard: { - borderRadius: 20, - padding: 24, - margin: 16, - }, - balanceStats: { - flexDirection: 'row', - justifyContent: 'space-around', - marginTop: 20, - paddingTop: 16, - borderTopWidth: 1, - borderTopColor: 'rgba(255,255,255,0.2)', - }, - balanceStat: { - alignItems: 'center', - }, - transaction: { - flexDirection: 'row', - alignItems: 'center', - padding: 12, - borderBottomWidth: 1, - }, - transactionIcon: { - marginRight: 12, - }, - transactionContent: { - flex: 1, - }, - package: { - flexDirection: 'row', - alignItems: 'center', - padding: 16, - borderRadius: 12, - marginBottom: 12, - }, - packageContent: { - flex: 1, - marginLeft: 12, - }, - packageList: { - padding: 16, - }, -}); diff --git a/src/components/skeletons/InventoryItemSkeleton.tsx b/src/components/skeletons/InventoryItemSkeleton.tsx deleted file mode 100644 index 42d126b..0000000 --- a/src/components/skeletons/InventoryItemSkeleton.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import { Skeleton, SkeletonText, SkeletonImage } from '../ui/Skeleton'; -import { useTheme } from '../../theme/ThemeContext'; - -/** - * Skeleton para un item de inventario - */ -export function InventoryItemSkeleton() { - const { colors } = useTheme(); - - return ( - - - - - - - - - - - - ); -} - -/** - * Lista de skeletons de inventario - */ -export function InventoryListSkeleton({ count = 8 }: { count?: number }) { - return ( - - {Array.from({ length: count }).map((_, index) => ( - - ))} - - ); -} - -/** - * Skeleton para las estadísticas de inventario - */ -export function InventoryStatsSkeleton() { - const { colors } = useTheme(); - - return ( - - {Array.from({ length: 4 }).map((_, index) => ( - - - - - ))} - - ); -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - padding: 12, - borderBottomWidth: 1, - }, - content: { - flex: 1, - marginLeft: 12, - }, - row: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - statsContainer: { - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'space-between', - padding: 16, - gap: 12, - }, - statCard: { - width: '47%', - padding: 16, - borderRadius: 12, - alignItems: 'center', - }, -}); diff --git a/src/components/skeletons/NotificationSkeleton.tsx b/src/components/skeletons/NotificationSkeleton.tsx deleted file mode 100644 index 7479ecc..0000000 --- a/src/components/skeletons/NotificationSkeleton.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import { Skeleton, SkeletonText, SkeletonCircle } from '../ui/Skeleton'; -import { useTheme } from '../../theme/ThemeContext'; - -/** - * Skeleton para una notificación - */ -export function NotificationItemSkeleton() { - const { colors } = useTheme(); - - return ( - - - - - - - - - - - - ); -} - -/** - * Lista de skeletons de notificaciones - */ -export function NotificationListSkeleton({ count = 6 }: { count?: number }) { - return ( - - {Array.from({ length: count }).map((_, index) => ( - - ))} - - ); -} - -/** - * Skeleton para el header de notificaciones - */ -export function NotificationHeaderSkeleton() { - const { colors } = useTheme(); - - return ( - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - padding: 16, - borderBottomWidth: 1, - }, - content: { - flex: 1, - marginLeft: 12, - }, - indicator: { - justifyContent: 'center', - paddingLeft: 8, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 16, - borderBottomWidth: 1, - }, -}); diff --git a/src/components/skeletons/StoreCardSkeleton.tsx b/src/components/skeletons/StoreCardSkeleton.tsx deleted file mode 100644 index 19873b2..0000000 --- a/src/components/skeletons/StoreCardSkeleton.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import { Skeleton, SkeletonText, SkeletonCircle } from '../ui/Skeleton'; -import { useTheme } from '../../theme/ThemeContext'; - -/** - * Skeleton para tarjeta de tienda - */ -export function StoreCardSkeleton() { - const { colors } = useTheme(); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -/** - * Lista de skeletons de tiendas - */ -export function StoreListSkeleton({ count = 3 }: { count?: number }) { - return ( - - {Array.from({ length: count }).map((_, index) => ( - - ))} - - ); -} - -const styles = StyleSheet.create({ - container: { - borderRadius: 16, - padding: 16, - marginBottom: 12, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - }, - headerContent: { - flex: 1, - marginLeft: 12, - }, - stats: { - flexDirection: 'row', - justifyContent: 'space-around', - marginTop: 16, - paddingTop: 16, - borderTopWidth: 1, - borderTopColor: 'rgba(0,0,0,0.1)', - }, - stat: { - alignItems: 'center', - }, - list: { - padding: 16, - }, -}); diff --git a/src/components/ui/AnimatedList.tsx b/src/components/ui/AnimatedList.tsx deleted file mode 100644 index fcfe25a..0000000 --- a/src/components/ui/AnimatedList.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import React, { useCallback } from 'react'; -import { FlatList, FlatListProps, ViewStyle, RefreshControl } from 'react-native'; -import Animated, { - useSharedValue, - useAnimatedStyle, - withTiming, - withDelay, - withSpring, - FadeIn, - SlideInRight, - Layout, -} from 'react-native-reanimated'; -import { useTheme } from '../../theme/ThemeContext'; - -const AnimatedFlatList = Animated.createAnimatedComponent(FlatList); - -interface AnimatedListProps extends Omit, 'renderItem'> { - renderItem: (info: { item: T; index: number }) => React.ReactElement; - /** Delay base entre items en ms */ - staggerDelay?: number; - /** Tipo de animación de entrada */ - animationType?: 'fade' | 'slide' | 'spring'; - /** Mostrar animación al refrescar */ - animateOnRefresh?: boolean; - /** Callback de refresh */ - onRefresh?: () => Promise | void; - /** Está refrescando */ - isRefreshing?: boolean; -} - -/** - * Item animado wrapper - */ -function AnimatedItem({ - children, - index, - staggerDelay, - animationType, -}: { - children: React.ReactNode; - index: number; - staggerDelay: number; - animationType: 'fade' | 'slide' | 'spring'; -}) { - const delay = Math.min(index * staggerDelay, 500); // Cap máximo de delay - - const entering = (() => { - switch (animationType) { - case 'slide': - return SlideInRight.delay(delay).duration(300); - case 'spring': - return FadeIn.delay(delay).springify(); - case 'fade': - default: - return FadeIn.delay(delay).duration(300); - } - })(); - - return ( - - {children} - - ); -} - -/** - * FlatList con animaciones de entrada staggered - */ -export function AnimatedList({ - data, - renderItem, - staggerDelay = 50, - animationType = 'fade', - animateOnRefresh = true, - onRefresh, - isRefreshing = false, - ...props -}: AnimatedListProps) { - const { colors } = useTheme(); - const [key, setKey] = React.useState(0); - - const handleRefresh = useCallback(async () => { - if (onRefresh) { - await onRefresh(); - if (animateOnRefresh) { - setKey((prev) => prev + 1); - } - } - }, [onRefresh, animateOnRefresh]); - - const animatedRenderItem = useCallback( - ({ item, index }: { item: T; index: number }) => ( - - {renderItem({ item, index })} - - ), - [renderItem, staggerDelay, animationType] - ); - - return ( - - ) : undefined - } - {...(props as any)} - /> - ); -} - -/** - * Hook para crear estilos animados de item de lista - */ -export function useListItemEntering(index: number, baseDelay = 50) { - const delay = Math.min(index * baseDelay, 500); - - return FadeIn.delay(delay).duration(300); -} - -/** - * Componente para animar un item individual - */ -export function AnimatedListItem({ - children, - index, - baseDelay = 50, - style, -}: { - children: React.ReactNode; - index: number; - baseDelay?: number; - style?: ViewStyle; -}) { - const entering = useListItemEntering(index, baseDelay); - - return ( - - {children} - - ); -} diff --git a/src/components/ui/OfflineBanner.tsx b/src/components/ui/OfflineBanner.tsx deleted file mode 100644 index 677b399..0000000 --- a/src/components/ui/OfflineBanner.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React, { useEffect } from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import Animated, { - useSharedValue, - useAnimatedStyle, - withTiming, - withSpring, -} from 'react-native-reanimated'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useIsOffline } from '../../hooks/useNetworkStatus'; -import { Ionicons } from '@expo/vector-icons'; - -interface OfflineBannerProps { - /** Mensaje personalizado */ - message?: string; - /** Mostrar icono de wifi */ - showIcon?: boolean; -} - -/** - * Banner que aparece cuando no hay conexión a internet - * Se muestra en la parte superior de la pantalla con animación slide - */ -export function OfflineBanner({ - message = 'Sin conexión a internet', - showIcon = true, -}: OfflineBannerProps) { - const isOffline = useIsOffline(); - const insets = useSafeAreaInsets(); - - const translateY = useSharedValue(-100); - const opacity = useSharedValue(0); - - useEffect(() => { - if (isOffline) { - translateY.value = withSpring(0, { damping: 15, stiffness: 150 }); - opacity.value = withTiming(1, { duration: 200 }); - } else { - translateY.value = withTiming(-100, { duration: 300 }); - opacity.value = withTiming(0, { duration: 200 }); - } - }, [isOffline]); - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: translateY.value }], - opacity: opacity.value, - })); - - // No renderizar nada si está online - if (!isOffline) { - return null; - } - - return ( - - - {showIcon && ( - - )} - {message} - - - ); -} - -/** - * Componente wrapper que incluye el banner offline - * Útil para envolver contenido principal de la app - */ -export function WithOfflineBanner({ children }: { children: React.ReactNode }) { - return ( - <> - - {children} - - ); -} - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - backgroundColor: '#EF4444', // Rojo para indicar problema - zIndex: 9999, - paddingBottom: 12, - }, - content: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 16, - }, - icon: { - marginRight: 8, - }, - text: { - color: '#FFFFFF', - fontSize: 14, - fontWeight: '600', - }, -}); diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/Skeleton.tsx deleted file mode 100644 index 33dd9fe..0000000 --- a/src/components/ui/Skeleton.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import React from 'react'; -import { View, StyleSheet, ViewStyle } from 'react-native'; -import Animated, { - useSharedValue, - useAnimatedStyle, - withRepeat, - withTiming, - interpolate, -} from 'react-native-reanimated'; -import { useEffect } from 'react'; -import { useTheme } from '../../theme/ThemeContext'; - -interface SkeletonProps { - width?: number | string; - height?: number; - borderRadius?: number; - style?: ViewStyle; -} - -/** - * Componente base de Skeleton con animación shimmer - */ -export function Skeleton({ - width = '100%', - height = 16, - borderRadius = 4, - style -}: SkeletonProps) { - const { colors } = useTheme(); - const shimmerValue = useSharedValue(0); - - useEffect(() => { - shimmerValue.value = withRepeat( - withTiming(1, { duration: 1200 }), - -1, - false - ); - }, []); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: interpolate(shimmerValue.value, [0, 0.5, 1], [0.3, 0.6, 0.3]), - })); - - return ( - - ); -} - -/** - * Skeleton para texto - línea simple - */ -export function SkeletonText({ - width = '80%', - height = 14, - style -}: SkeletonProps) { - return ( - - ); -} - -/** - * Skeleton circular - para avatares - */ -export function SkeletonCircle({ - size = 40, - style -}: { size?: number; style?: ViewStyle }) { - return ( - - ); -} - -/** - * Skeleton para imagen cuadrada - */ -export function SkeletonImage({ - width = 80, - height = 80, - borderRadius = 8, - style -}: SkeletonProps) { - return ( - - ); -} - -/** - * Skeleton para tarjeta completa - */ -export function SkeletonCard({ style }: { style?: ViewStyle }) { - const { colors } = useTheme(); - - return ( - - - - - - - - - - - - - ); -} - -/** - * Skeleton para item de lista - */ -export function SkeletonListItem({ style }: { style?: ViewStyle }) { - const { colors } = useTheme(); - - return ( - - - - - - - - - ); -} - -/** - * Skeleton para estadística/métrica - */ -export function SkeletonStat({ style }: { style?: ViewStyle }) { - const { colors } = useTheme(); - - return ( - - - - - ); -} - -/** - * Grupo de skeletons de lista - */ -export function SkeletonList({ - count = 5, - style -}: { count?: number; style?: ViewStyle }) { - return ( - - {Array.from({ length: count }).map((_, index) => ( - - ))} - - ); -} - -const styles = StyleSheet.create({ - card: { - padding: 16, - borderRadius: 12, - marginVertical: 8, - }, - cardHeader: { - flexDirection: 'row', - alignItems: 'center', - }, - cardHeaderText: { - flex: 1, - marginLeft: 12, - }, - listItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 12, - paddingHorizontal: 16, - borderBottomWidth: 1, - }, - listItemContent: { - flex: 1, - marginLeft: 12, - }, - stat: { - padding: 16, - borderRadius: 12, - alignItems: 'center', - minWidth: 100, - }, -}); diff --git a/src/components/validation/ValidationItemCard.tsx b/src/components/validation/ValidationItemCard.tsx deleted file mode 100644 index 6d91803..0000000 --- a/src/components/validation/ValidationItemCard.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - Image, - TextInput, - TouchableOpacity, - StyleSheet, -} from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { ValidationItem, ValidationItemResponse } from '../../services/api/validations.service'; - -interface Props { - item: ValidationItem; - onResponse: (response: Omit) => void; - existingResponse?: ValidationItemResponse; -} - -export function ValidationItemCard({ item, onResponse, existingResponse }: Props) { - const [showCorrection, setShowCorrection] = useState(false); - const [correctedQuantity, setCorrectedQuantity] = useState( - existingResponse?.correctedQuantity?.toString() || item.quantity.toString(), - ); - const [correctedName, setCorrectedName] = useState( - existingResponse?.correctedName || item.name, - ); - const [startTime] = useState(Date.now()); - - const handleCorrect = () => { - setShowCorrection(!showCorrection); - }; - - const handleMarkCorrect = () => { - onResponse({ - isCorrect: true, - responseTimeMs: Date.now() - startTime, - }); - }; - - const handleSubmitCorrection = () => { - const qty = parseInt(correctedQuantity, 10); - onResponse({ - isCorrect: false, - correctedQuantity: isNaN(qty) ? undefined : qty, - correctedName: correctedName !== item.name ? correctedName : undefined, - responseTimeMs: Date.now() - startTime, - }); - setShowCorrection(false); - }; - - const confidence = item.detectionConfidence - ? Math.round(Number(item.detectionConfidence) * 100) - : null; - - return ( - - {item.imageUrl && ( - - )} - - - {item.name} - - Cantidad: {item.quantity} - {item.category && ( - {item.category} - )} - - {confidence !== null && ( - - Confianza: - = 80 - ? styles.highConfidence - : confidence >= 60 - ? styles.mediumConfidence - : styles.lowConfidence, - ]} - > - {confidence}% - - - )} - - - {showCorrection ? ( - - Nombre correcto: - - Cantidad correcta: - - - setShowCorrection(false)} - > - Cancelar - - - Guardar - - - - ) : ( - - - - Corregir - - - - Correcto - - - )} - - {existingResponse && !showCorrection && ( - - - - {existingResponse.isCorrect ? 'Marcado correcto' : 'Corregido'} - - - )} - - ); -} - -const styles = StyleSheet.create({ - container: { - backgroundColor: '#fff', - borderRadius: 12, - padding: 16, - marginBottom: 16, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - image: { - width: '100%', - height: 150, - borderRadius: 8, - marginBottom: 12, - backgroundColor: '#f5f5f5', - }, - content: { - marginBottom: 12, - }, - name: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 4, - }, - details: { - flexDirection: 'row', - alignItems: 'center', - gap: 12, - }, - quantity: { - fontSize: 16, - color: '#333', - }, - category: { - fontSize: 14, - color: '#666', - backgroundColor: '#f0f0f0', - paddingHorizontal: 8, - paddingVertical: 2, - borderRadius: 4, - }, - confidenceContainer: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 8, - gap: 8, - }, - confidenceLabel: { - fontSize: 12, - color: '#999', - }, - confidenceBadge: { - paddingHorizontal: 8, - paddingVertical: 2, - borderRadius: 4, - }, - highConfidence: { - backgroundColor: '#e8f5e9', - }, - mediumConfidence: { - backgroundColor: '#fff3e0', - }, - lowConfidence: { - backgroundColor: '#ffebee', - }, - confidenceText: { - fontSize: 12, - fontWeight: '600', - }, - actions: { - flexDirection: 'row', - gap: 12, - }, - actionButton: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - padding: 12, - borderRadius: 8, - gap: 6, - }, - incorrectButton: { - backgroundColor: '#ffebee', - }, - correctButton: { - backgroundColor: '#e8f5e9', - }, - incorrectText: { - color: '#dc3545', - fontWeight: '600', - }, - correctText: { - color: '#28a745', - fontWeight: '600', - }, - correctionForm: { - borderTopWidth: 1, - borderTopColor: '#eee', - paddingTop: 12, - }, - correctionLabel: { - fontSize: 14, - color: '#333', - marginBottom: 4, - }, - input: { - borderWidth: 1, - borderColor: '#ddd', - borderRadius: 8, - padding: 10, - fontSize: 16, - marginBottom: 12, - }, - correctionButtons: { - flexDirection: 'row', - gap: 12, - }, - cancelCorrectionButton: { - flex: 1, - padding: 12, - borderRadius: 8, - backgroundColor: '#f5f5f5', - alignItems: 'center', - }, - saveCorrectionButton: { - flex: 1, - padding: 12, - borderRadius: 8, - backgroundColor: '#007AFF', - alignItems: 'center', - }, - cancelCorrectionText: { - color: '#666', - fontWeight: '600', - }, - saveCorrectionText: { - color: '#fff', - fontWeight: '600', - }, - responseIndicator: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginTop: 12, - paddingTop: 12, - borderTopWidth: 1, - borderTopColor: '#eee', - gap: 6, - }, - responseText: { - fontSize: 14, - fontWeight: '500', - }, -}); diff --git a/src/components/validation/ValidationProgressBar.tsx b/src/components/validation/ValidationProgressBar.tsx deleted file mode 100644 index 3b3ac0d..0000000 --- a/src/components/validation/ValidationProgressBar.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet } from 'react-native'; - -interface Props { - current: number; - total: number; - validated: number; -} - -export function ValidationProgressBar({ current, total, validated }: Props) { - const progress = (validated / total) * 100; - - return ( - - - - Producto {current + 1} de {total} - - - {validated} validados - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - paddingHorizontal: 16, - paddingVertical: 12, - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - marginBottom: 8, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#333', - }, - validated: { - fontSize: 14, - color: '#28a745', - }, - track: { - height: 4, - backgroundColor: '#e0e0e0', - borderRadius: 2, - overflow: 'hidden', - }, - fill: { - height: '100%', - backgroundColor: '#007AFF', - borderRadius: 2, - }, -}); diff --git a/src/components/validation/ValidationPromptModal.tsx b/src/components/validation/ValidationPromptModal.tsx deleted file mode 100644 index 5a29e38..0000000 --- a/src/components/validation/ValidationPromptModal.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import React from 'react'; -import { - View, - Text, - Modal, - TouchableOpacity, - StyleSheet, -} from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; -import { useValidationsStore } from '../../stores/validations.store'; - -interface Props { - visible: boolean; - onClose: () => void; - requestId: string; - creditsReward: number; - itemsCount: number; -} - -export function ValidationPromptModal({ - visible, - onClose, - requestId, - creditsReward, - itemsCount, -}: Props) { - const router = useRouter(); - const { skipValidation } = useValidationsStore(); - - const handleAccept = () => { - onClose(); - router.push('/validation/items'); - }; - - const handleSkip = async () => { - await skipValidation(); - onClose(); - }; - - return ( - - - - - - - - Ayudanos a mejorar - - Validando algunos productos nos ayudas a detectar mejor tu inventario - en el futuro. - - - - - - {itemsCount} - productos - - - - - +{creditsReward} - - credito - - - - Toma menos de 1 minuto - - - - Ahora no - - - Validar - - - - - - - ); -} - -const styles = StyleSheet.create({ - overlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.5)', - justifyContent: 'center', - alignItems: 'center', - }, - container: { - backgroundColor: '#fff', - borderRadius: 16, - padding: 24, - width: '85%', - maxWidth: 340, - alignItems: 'center', - }, - iconContainer: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: '#e3f2fd', - justifyContent: 'center', - alignItems: 'center', - marginBottom: 16, - }, - title: { - fontSize: 22, - fontWeight: 'bold', - marginBottom: 8, - textAlign: 'center', - }, - description: { - fontSize: 14, - color: '#666', - textAlign: 'center', - lineHeight: 20, - marginBottom: 20, - }, - stats: { - flexDirection: 'row', - justifyContent: 'center', - gap: 40, - marginBottom: 16, - }, - statItem: { - alignItems: 'center', - }, - statValue: { - fontSize: 24, - fontWeight: 'bold', - marginTop: 4, - }, - rewardValue: { - color: '#28a745', - }, - statLabel: { - fontSize: 12, - color: '#999', - }, - time: { - fontSize: 12, - color: '#999', - marginBottom: 20, - }, - buttons: { - flexDirection: 'row', - gap: 12, - width: '100%', - }, - button: { - flex: 1, - padding: 14, - borderRadius: 8, - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - gap: 6, - }, - skipButton: { - backgroundColor: '#f5f5f5', - }, - acceptButton: { - backgroundColor: '#007AFF', - }, - skipText: { - color: '#666', - fontWeight: '600', - }, - acceptText: { - color: '#fff', - fontWeight: '600', - }, -}); diff --git a/src/hooks/useAnimations.ts b/src/hooks/useAnimations.ts deleted file mode 100644 index f4c4296..0000000 --- a/src/hooks/useAnimations.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { useEffect } from 'react'; -import Animated, { - useSharedValue, - useAnimatedStyle, - withTiming, - withSpring, - withDelay, - Easing, - interpolate, - WithTimingConfig, - WithSpringConfig, -} from 'react-native-reanimated'; - -const DEFAULT_TIMING: WithTimingConfig = { - duration: 300, - easing: Easing.bezier(0.25, 0.1, 0.25, 1), -}; - -const DEFAULT_SPRING: WithSpringConfig = { - damping: 15, - stiffness: 150, -}; - -/** - * Hook para animación de fade in - */ -export function useFadeIn(delay = 0) { - const opacity = useSharedValue(0); - - useEffect(() => { - opacity.value = withDelay(delay, withTiming(1, DEFAULT_TIMING)); - }, [delay]); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - })); - - return { animatedStyle, opacity }; -} - -/** - * Hook para animación de slide desde abajo - */ -export function useSlideIn(delay = 0, distance = 20) { - const opacity = useSharedValue(0); - const translateY = useSharedValue(distance); - - useEffect(() => { - opacity.value = withDelay(delay, withTiming(1, DEFAULT_TIMING)); - translateY.value = withDelay(delay, withSpring(0, DEFAULT_SPRING)); - }, [delay, distance]); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - transform: [{ translateY: translateY.value }], - })); - - return { animatedStyle, opacity, translateY }; -} - -/** - * Hook para animación de slide desde la derecha - */ -export function useSlideFromRight(delay = 0, distance = 30) { - const opacity = useSharedValue(0); - const translateX = useSharedValue(distance); - - useEffect(() => { - opacity.value = withDelay(delay, withTiming(1, DEFAULT_TIMING)); - translateX.value = withDelay(delay, withSpring(0, DEFAULT_SPRING)); - }, [delay, distance]); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - transform: [{ translateX: translateX.value }], - })); - - return { animatedStyle, opacity, translateX }; -} - -/** - * Hook para efecto de escala al presionar - */ -export function usePressScale(pressedScale = 0.97) { - const scale = useSharedValue(1); - - const onPressIn = () => { - scale.value = withSpring(pressedScale, { damping: 20, stiffness: 300 }); - }; - - const onPressOut = () => { - scale.value = withSpring(1, { damping: 20, stiffness: 300 }); - }; - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ scale: scale.value }], - })); - - return { animatedStyle, onPressIn, onPressOut, scale }; -} - -/** - * Hook para animación stagger en listas - * Retorna un delay calculado basado en el índice - */ -export function useListItemAnimation(index: number, baseDelay = 50) { - const delay = index * baseDelay; - return useSlideIn(delay); -} - -/** - * Hook para animación de shimmer (skeleton loader) - */ -export function useShimmer() { - const shimmerValue = useSharedValue(0); - - useEffect(() => { - const animate = () => { - shimmerValue.value = withTiming(1, { duration: 1000 }, () => { - shimmerValue.value = 0; - animate(); - }); - }; - animate(); - - return () => { - shimmerValue.value = 0; - }; - }, []); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: interpolate(shimmerValue.value, [0, 0.5, 1], [0.3, 0.7, 0.3]), - })); - - return { animatedStyle, shimmerValue }; -} - -/** - * Hook para animación de pulso - */ -export function usePulse(minScale = 0.98, maxScale = 1.02) { - const scale = useSharedValue(1); - - useEffect(() => { - const animate = () => { - scale.value = withTiming(maxScale, { duration: 800 }, () => { - scale.value = withTiming(minScale, { duration: 800 }, () => { - animate(); - }); - }); - }; - animate(); - - return () => { - scale.value = 1; - }; - }, [minScale, maxScale]); - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ scale: scale.value }], - })); - - return { animatedStyle, scale }; -} - -/** - * Hook para animar entrada/salida de un elemento - */ -export function useToggleAnimation(isVisible: boolean) { - const opacity = useSharedValue(isVisible ? 1 : 0); - const translateY = useSharedValue(isVisible ? 0 : -20); - - useEffect(() => { - opacity.value = withTiming(isVisible ? 1 : 0, { duration: 200 }); - translateY.value = withSpring(isVisible ? 0 : -20); - }, [isVisible]); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - transform: [{ translateY: translateY.value }], - })); - - return { animatedStyle }; -} - -export { Animated }; diff --git a/src/hooks/useNetworkStatus.ts b/src/hooks/useNetworkStatus.ts deleted file mode 100644 index a7a53f9..0000000 --- a/src/hooks/useNetworkStatus.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { useState, useEffect } from 'react'; -import NetInfo, { NetInfoState, NetInfoStateType } from '@react-native-community/netinfo'; - -export interface NetworkStatus { - isConnected: boolean; - isInternetReachable: boolean | null; - type: NetInfoStateType; - isWifi: boolean; - isCellular: boolean; -} - -/** - * Hook para detectar el estado de la conexión de red - */ -export function useNetworkStatus(): NetworkStatus { - const [networkStatus, setNetworkStatus] = useState({ - isConnected: true, - isInternetReachable: true, - type: NetInfoStateType.unknown, - isWifi: false, - isCellular: false, - }); - - useEffect(() => { - const unsubscribe = NetInfo.addEventListener((state: NetInfoState) => { - setNetworkStatus({ - isConnected: state.isConnected ?? false, - isInternetReachable: state.isInternetReachable, - type: state.type, - isWifi: state.type === NetInfoStateType.wifi, - isCellular: state.type === NetInfoStateType.cellular, - }); - }); - - // Obtener estado inicial - NetInfo.fetch().then((state) => { - setNetworkStatus({ - isConnected: state.isConnected ?? false, - isInternetReachable: state.isInternetReachable, - type: state.type, - isWifi: state.type === NetInfoStateType.wifi, - isCellular: state.type === NetInfoStateType.cellular, - }); - }); - - return () => { - unsubscribe(); - }; - }, []); - - return networkStatus; -} - -/** - * Hook simplificado para verificar si hay conexión - */ -export function useIsOnline(): boolean { - const { isConnected, isInternetReachable } = useNetworkStatus(); - - // isInternetReachable puede ser null mientras se determina - if (isInternetReachable === null) { - return isConnected; - } - - return isConnected && isInternetReachable; -} - -/** - * Hook simplificado para verificar si está offline - */ -export function useIsOffline(): boolean { - return !useIsOnline(); -} diff --git a/src/services/api/__tests__/auth.service.spec.ts b/src/services/api/__tests__/auth.service.spec.ts deleted file mode 100644 index 0950934..0000000 --- a/src/services/api/__tests__/auth.service.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { authService } from '../auth.service'; -import apiClient from '../client'; - -jest.mock('../client'); - -const mockApiClient = apiClient as jest.Mocked; - -describe('Auth Service', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('login', () => { - it('should call login endpoint with credentials', async () => { - const mockResponse = { - data: { - user: { id: '1', phone: '+1234567890', name: 'Test' }, - accessToken: 'access-token', - refreshToken: 'refresh-token', - }, - }; - - mockApiClient.post.mockResolvedValue(mockResponse); - - const result = await authService.login({ - phone: '+1234567890', - password: 'password123', - }); - - expect(mockApiClient.post).toHaveBeenCalledWith('/auth/login', { - phone: '+1234567890', - password: 'password123', - }); - expect(result).toEqual(mockResponse.data); - }); - }); - - describe('initiateRegistration', () => { - it('should call registration endpoint', async () => { - mockApiClient.post.mockResolvedValue({ data: { message: 'OTP sent' } }); - - await authService.initiateRegistration({ - phone: '+1234567890', - name: 'Test User', - }); - - expect(mockApiClient.post).toHaveBeenCalledWith('/auth/register/initiate', { - phone: '+1234567890', - name: 'Test User', - }); - }); - }); - - describe('verifyOtp', () => { - it('should call OTP verification endpoint', async () => { - const mockResponse = { - data: { - user: { id: '1', phone: '+1234567890', name: 'Test' }, - accessToken: 'access-token', - refreshToken: 'refresh-token', - }, - }; - - mockApiClient.post.mockResolvedValue(mockResponse); - - const result = await authService.verifyOtp({ - phone: '+1234567890', - otp: '123456', - password: 'password123', - }); - - expect(mockApiClient.post).toHaveBeenCalledWith('/auth/register/verify', { - phone: '+1234567890', - otp: '123456', - password: 'password123', - }); - expect(result).toEqual(mockResponse.data); - }); - }); - - describe('refreshTokens', () => { - it('should call refresh endpoint', async () => { - const mockResponse = { - data: { - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }, - }; - - mockApiClient.post.mockResolvedValue(mockResponse); - - const result = await authService.refreshTokens('old-refresh-token'); - - expect(mockApiClient.post).toHaveBeenCalledWith('/auth/refresh', { - refreshToken: 'old-refresh-token', - }); - expect(result).toEqual(mockResponse.data); - }); - }); - - describe('logout', () => { - it('should call logout endpoint', async () => { - mockApiClient.post.mockResolvedValue({ data: { success: true } }); - - await authService.logout('refresh-token'); - - expect(mockApiClient.post).toHaveBeenCalledWith('/auth/logout', { - refreshToken: 'refresh-token', - }); - }); - }); -}); diff --git a/src/services/api/__tests__/inventory.service.spec.ts b/src/services/api/__tests__/inventory.service.spec.ts deleted file mode 100644 index 52fb7b3..0000000 --- a/src/services/api/__tests__/inventory.service.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { inventoryService } from '../inventory.service'; -import apiClient from '../client'; - -jest.mock('../client'); - -const mockApiClient = apiClient as jest.Mocked; - -describe('Inventory Service', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getItems', () => { - it('should fetch items with pagination', async () => { - const mockResponse = { - data: { - items: [ - { id: '1', name: 'Item 1', quantity: 10 }, - { id: '2', name: 'Item 2', quantity: 5 }, - ], - total: 2, - page: 1, - limit: 50, - hasMore: false, - }, - }; - - mockApiClient.get.mockResolvedValue(mockResponse); - - const result = await inventoryService.getItems('store-1', { page: 1, limit: 50 }); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/inventory', { - params: { page: 1, limit: 50 }, - }); - expect(result).toEqual(mockResponse.data); - }); - - it('should pass category filter', async () => { - mockApiClient.get.mockResolvedValue({ - data: { items: [], total: 0, page: 1, limit: 50, hasMore: false }, - }); - - await inventoryService.getItems('store-1', { category: 'Electronics' }); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/inventory', { - params: { category: 'Electronics' }, - }); - }); - }); - - describe('getItem', () => { - it('should fetch single item', async () => { - const mockItem = { id: '1', name: 'Item 1', quantity: 10 }; - mockApiClient.get.mockResolvedValue({ data: mockItem }); - - const result = await inventoryService.getItem('store-1', '1'); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/inventory/1'); - expect(result).toEqual(mockItem); - }); - }); - - describe('updateItem', () => { - it('should send PATCH request with updates', async () => { - const updatedItem = { id: '1', name: 'Updated', quantity: 20 }; - mockApiClient.patch.mockResolvedValue({ data: updatedItem }); - - const result = await inventoryService.updateItem('store-1', '1', { - name: 'Updated', - quantity: 20, - }); - - expect(mockApiClient.patch).toHaveBeenCalledWith('/stores/store-1/inventory/1', { - name: 'Updated', - quantity: 20, - }); - expect(result).toEqual(updatedItem); - }); - }); - - describe('deleteItem', () => { - it('should send DELETE request', async () => { - mockApiClient.delete.mockResolvedValue({ data: { success: true } }); - - await inventoryService.deleteItem('store-1', '1'); - - expect(mockApiClient.delete).toHaveBeenCalledWith('/stores/store-1/inventory/1'); - }); - }); - - describe('getStatistics', () => { - it('should fetch inventory statistics', async () => { - const mockStats = { - totalItems: 100, - totalValue: 5000, - lowStockCount: 5, - categoryBreakdown: [], - }; - mockApiClient.get.mockResolvedValue({ data: mockStats }); - - const result = await inventoryService.getStatistics('store-1'); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/inventory/statistics'); - expect(result).toEqual(mockStats); - }); - }); - - describe('getCategories', () => { - it('should fetch categories list', async () => { - const mockCategories = ['Electronics', 'Clothing', 'Food']; - mockApiClient.get.mockResolvedValue({ data: mockCategories }); - - const result = await inventoryService.getCategories('store-1'); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/inventory/categories'); - expect(result).toEqual(mockCategories); - }); - }); -}); diff --git a/src/services/api/__tests__/reports.service.spec.ts b/src/services/api/__tests__/reports.service.spec.ts deleted file mode 100644 index 53d8464..0000000 --- a/src/services/api/__tests__/reports.service.spec.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { reportsService } from '../reports.service'; -import apiClient from '../client'; - -jest.mock('../client'); - -const mockApiClient = apiClient as jest.Mocked; - -describe('Reports Service', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getValuationReport', () => { - it('should fetch valuation report', async () => { - const mockReport = { - summary: { - totalItems: 100, - totalCost: 1000, - totalPrice: 2000, - potentialMargin: 1000, - potentialMarginPercent: 50, - }, - byCategory: [], - items: [], - }; - - mockApiClient.get.mockResolvedValue({ data: mockReport }); - - const result = await reportsService.getValuationReport('store-1'); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/reports/valuation'); - expect(result).toEqual(mockReport); - }); - }); - - describe('getMovementsReport', () => { - it('should fetch movements report without params', async () => { - const mockReport = { - summary: { - period: { start: '2024-01-01', end: '2024-01-31' }, - totalMovements: 50, - netChange: 10, - itemsIncreased: 30, - itemsDecreased: 20, - }, - movements: [], - byItem: [], - total: 50, - page: 1, - limit: 50, - hasMore: false, - }; - - mockApiClient.get.mockResolvedValue({ data: mockReport }); - - const result = await reportsService.getMovementsReport('store-1'); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/reports/movements', { - params: undefined, - }); - expect(result).toEqual(mockReport); - }); - - it('should pass date range params', async () => { - mockApiClient.get.mockResolvedValue({ - data: { - summary: {}, - movements: [], - byItem: [], - total: 0, - page: 1, - limit: 50, - hasMore: false, - }, - }); - - await reportsService.getMovementsReport('store-1', { - startDate: '2024-01-01', - endDate: '2024-01-31', - }); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/reports/movements', { - params: { - startDate: '2024-01-01', - endDate: '2024-01-31', - }, - }); - }); - - it('should pass pagination params', async () => { - mockApiClient.get.mockResolvedValue({ - data: { - summary: {}, - movements: [], - byItem: [], - total: 100, - page: 2, - limit: 20, - hasMore: true, - }, - }); - - await reportsService.getMovementsReport('store-1', { - page: 2, - limit: 20, - }); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/reports/movements', { - params: { page: 2, limit: 20 }, - }); - }); - }); - - describe('getCategoriesReport', () => { - it('should fetch categories report', async () => { - const mockReport = { - summary: { - totalCategories: 5, - totalItems: 100, - totalValue: 10000, - }, - categories: [ - { - name: 'Electronics', - itemCount: 50, - percentOfTotal: 50, - totalValue: 5000, - lowStockCount: 2, - averagePrice: 100, - topItems: [], - }, - ], - }; - - mockApiClient.get.mockResolvedValue({ data: mockReport }); - - const result = await reportsService.getCategoriesReport('store-1'); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/reports/categories'); - expect(result).toEqual(mockReport); - }); - }); - - describe('getLowStockReport', () => { - it('should fetch low stock report', async () => { - const mockReport = { - summary: { - totalAlerts: 10, - criticalCount: 3, - warningCount: 7, - totalValueAtRisk: 500, - }, - items: [ - { - id: '1', - name: 'Low Stock Item', - category: 'Electronics', - quantity: 2, - minStock: 10, - shortage: 8, - estimatedReorderCost: 80, - priority: 'critical', - }, - ], - }; - - mockApiClient.get.mockResolvedValue({ data: mockReport }); - - const result = await reportsService.getLowStockReport('store-1'); - - expect(mockApiClient.get).toHaveBeenCalledWith('/stores/store-1/reports/low-stock'); - expect(result).toEqual(mockReport); - }); - }); -}); diff --git a/src/services/api/auth.service.ts b/src/services/api/auth.service.ts deleted file mode 100644 index 57c8806..0000000 --- a/src/services/api/auth.service.ts +++ /dev/null @@ -1,62 +0,0 @@ -import apiClient from './client'; - -interface LoginRequest { - phone: string; - password: string; -} - -interface RegisterRequest { - phone: string; - name: string; -} - -interface VerifyOtpRequest { - phone: string; - otp: string; - password: string; -} - -interface AuthResponse { - user: { - id: string; - phone: string; - name: string; - email?: string; - }; - accessToken: string; - refreshToken: string; - expiresIn: number; -} - -interface TokenResponse { - accessToken: string; - refreshToken: string; - expiresIn: number; -} - -export const authService = { - login: async (data: LoginRequest): Promise => { - const response = await apiClient.post('/auth/login', data); - return response.data; - }, - - initiateRegistration: async (data: RegisterRequest): Promise => { - await apiClient.post('/auth/register', data); - }, - - verifyOtp: async (data: VerifyOtpRequest): Promise => { - const response = await apiClient.post('/auth/verify-otp', data); - return response.data; - }, - - refreshTokens: async (refreshToken: string): Promise => { - const response = await apiClient.post('/auth/refresh', { - refreshToken, - }); - return response.data; - }, - - logout: async (refreshToken: string): Promise => { - await apiClient.post('/auth/logout', { refreshToken }); - }, -}; diff --git a/src/services/api/client.ts b/src/services/api/client.ts deleted file mode 100644 index 621a693..0000000 --- a/src/services/api/client.ts +++ /dev/null @@ -1,58 +0,0 @@ -import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; -import { useAuthStore } from '@stores/auth.store'; - -const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:3142/api/v1'; - -export const apiClient: AxiosInstance = axios.create({ - baseURL: API_URL, - timeout: 30000, - headers: { - 'Content-Type': 'application/json', - }, -}); - -// Request interceptor - add auth token -apiClient.interceptors.request.use( - (config: InternalAxiosRequestConfig) => { - const { accessToken } = useAuthStore.getState(); - if (accessToken && config.headers) { - config.headers.Authorization = `Bearer ${accessToken}`; - } - return config; - }, - (error) => Promise.reject(error) -); - -// Response interceptor - handle token refresh -apiClient.interceptors.response.use( - (response) => response, - async (error: AxiosError) => { - const originalRequest = error.config as InternalAxiosRequestConfig & { - _retry?: boolean; - }; - - // If 401 and not already retrying, try to refresh token - if (error.response?.status === 401 && !originalRequest._retry) { - originalRequest._retry = true; - - try { - await useAuthStore.getState().refreshTokens(); - const { accessToken } = useAuthStore.getState(); - - if (accessToken && originalRequest.headers) { - originalRequest.headers.Authorization = `Bearer ${accessToken}`; - } - - return apiClient(originalRequest); - } catch { - // Refresh failed, logout - useAuthStore.getState().logout(); - return Promise.reject(error); - } - } - - return Promise.reject(error); - } -); - -export default apiClient; diff --git a/src/services/api/credits.service.ts b/src/services/api/credits.service.ts deleted file mode 100644 index d0d72be..0000000 --- a/src/services/api/credits.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import apiClient from './client'; - -interface BalanceResponse { - balance: number; - totalPurchased: number; - totalConsumed: number; - totalFromReferrals: number; -} - -interface Transaction { - id: string; - type: 'purchase' | 'consumption' | 'referral_bonus'; - amount: number; - description: string; - createdAt: string; -} - -interface TransactionsResponse { - transactions: Transaction[]; - total: number; - page: number; - limit: number; -} - -interface PurchaseRequest { - packageId: string; - paymentMethodId: string; -} - -interface PurchaseResponse { - transactionId: string; - newBalance: number; - paymentStatus: 'completed' | 'pending' | 'failed'; - paymentUrl?: string; // For OXXO/7-Eleven vouchers -} - -export const creditsService = { - getBalance: async (): Promise => { - const response = await apiClient.get('/credits/balance'); - return response.data; - }, - - getTransactions: async ( - page = 1, - limit = 20 - ): Promise => { - const response = await apiClient.get( - '/credits/transactions', - { params: { page, limit } } - ); - return response.data; - }, - - purchase: async (data: PurchaseRequest): Promise => { - const response = await apiClient.post( - '/credits/purchase', - data - ); - return response.data; - }, -}; diff --git a/src/services/api/exports.service.ts b/src/services/api/exports.service.ts deleted file mode 100644 index 6844fc4..0000000 --- a/src/services/api/exports.service.ts +++ /dev/null @@ -1,143 +0,0 @@ -import apiClient from './client'; - -export type ExportFormat = 'CSV' | 'EXCEL'; - -export type ExportType = - | 'INVENTORY' - | 'REPORT_VALUATION' - | 'REPORT_MOVEMENTS' - | 'REPORT_CATEGORIES' - | 'REPORT_LOW_STOCK'; - -export type ExportStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED'; - -export interface ExportFilters { - category?: string; - lowStockOnly?: boolean; - startDate?: string; - endDate?: string; -} - -export interface ExportJobResponse { - jobId: string; - message: string; -} - -export interface ExportStatusResponse { - id: string; - status: ExportStatus; - format: ExportFormat; - type: ExportType; - filters?: ExportFilters; - totalRows?: number; - errorMessage?: string; - createdAt: string; - expiresAt?: string; -} - -export interface ExportDownloadResponse { - url: string; - expiresAt: string; - filename: string; -} - -export const exportsService = { - /** - * Request inventory export - */ - requestInventoryExport: async ( - storeId: string, - format: ExportFormat, - filters?: { category?: string; lowStockOnly?: boolean }, - ): Promise => { - const response = await apiClient.post( - `/stores/${storeId}/exports/inventory`, - { format, ...filters }, - ); - return response.data; - }, - - /** - * Request report export - */ - requestReportExport: async ( - storeId: string, - type: ExportType, - format: ExportFormat, - filters?: { startDate?: string; endDate?: string }, - ): Promise => { - const response = await apiClient.post( - `/stores/${storeId}/exports/report`, - { type, format, ...filters }, - ); - return response.data; - }, - - /** - * Get export status - */ - getExportStatus: async ( - storeId: string, - jobId: string, - ): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/exports/${jobId}`, - ); - return response.data; - }, - - /** - * Get download URL for completed export - */ - getDownloadUrl: async ( - storeId: string, - jobId: string, - ): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/exports/${jobId}/download`, - ); - return response.data; - }, - - /** - * Poll export status until complete or failed - */ - pollExportStatus: async ( - storeId: string, - jobId: string, - onProgress?: (status: ExportStatusResponse) => void, - maxAttempts = 60, - intervalMs = 2000, - ): Promise => { - let attempts = 0; - - return new Promise((resolve, reject) => { - const poll = async () => { - try { - const status = await exportsService.getExportStatus(storeId, jobId); - - if (onProgress) { - onProgress(status); - } - - if (status.status === 'COMPLETED' || status.status === 'FAILED') { - resolve(status); - return; - } - - attempts++; - if (attempts >= maxAttempts) { - reject(new Error('Export timed out')); - return; - } - - setTimeout(poll, intervalMs); - } catch (error) { - reject(error); - } - }; - - poll(); - }); - }, -}; diff --git a/src/services/api/feedback.service.ts b/src/services/api/feedback.service.ts deleted file mode 100644 index bb29568..0000000 --- a/src/services/api/feedback.service.ts +++ /dev/null @@ -1,115 +0,0 @@ -import apiClient from './client'; - -export interface CorrectQuantityRequest { - quantity: number; - reason?: string; -} - -export interface CorrectSkuRequest { - name: string; - category?: string; - barcode?: string; - reason?: string; -} - -export interface CorrectionResponse { - id: string; - type: 'QUANTITY' | 'SKU' | 'CONFIRMATION'; - previousValue: Record; - newValue: Record; - createdAt: string; -} - -export interface CorrectionHistoryItem { - id: string; - type: 'QUANTITY' | 'SKU' | 'CONFIRMATION'; - previousValue: Record; - newValue: Record; - reason?: string; - createdAt: string; - user?: { - id: string; - name: string; - }; -} - -export interface SubmitProductRequest { - storeId: string; - videoId?: string; - name: string; - category?: string; - barcode?: string; - imageUrl?: string; - frameTimestamp?: number; - boundingBox?: Record; -} - -export interface ProductSearchResult { - id: string; - name: string; - category?: string; - barcode?: string; - imageUrl?: string; -} - -const feedbackService = { - async correctQuantity( - storeId: string, - itemId: string, - data: CorrectQuantityRequest, - ): Promise<{ correction: CorrectionResponse; item: { id: string; name: string; quantity: number } }> { - const response = await apiClient.patch( - `/stores/${storeId}/inventory/${itemId}/correct-quantity`, - data, - ); - return response.data; - }, - - async correctSku( - storeId: string, - itemId: string, - data: CorrectSkuRequest, - ): Promise<{ correction: CorrectionResponse; item: { id: string; name: string; category?: string; barcode?: string } }> { - const response = await apiClient.patch( - `/stores/${storeId}/inventory/${itemId}/correct-sku`, - data, - ); - return response.data; - }, - - async confirmItem( - storeId: string, - itemId: string, - ): Promise<{ correction: CorrectionResponse; item: { id: string; name: string; quantity: number } }> { - const response = await apiClient.post( - `/stores/${storeId}/inventory/${itemId}/confirm`, - ); - return response.data; - }, - - async getCorrectionHistory( - storeId: string, - itemId: string, - ): Promise<{ corrections: CorrectionHistoryItem[] }> { - const response = await apiClient.get( - `/stores/${storeId}/inventory/${itemId}/history`, - ); - return response.data; - }, - - async submitProduct(data: SubmitProductRequest): Promise<{ - submission: { id: string; name: string; status: string; createdAt: string }; - }> { - const response = await apiClient.post('/products/submit', data); - return response.data; - }, - - async searchProducts(query: string, limit = 10): Promise<{ products: ProductSearchResult[] }> { - const response = await apiClient.get('/products/search', { - params: { q: query, limit }, - }); - return response.data; - }, -}; - -export default feedbackService; diff --git a/src/services/api/inventory.service.ts b/src/services/api/inventory.service.ts deleted file mode 100644 index 33dce7c..0000000 --- a/src/services/api/inventory.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import apiClient from './client'; - -export interface InventoryItem { - id: string; - name: string; - quantity: number; - category?: string; - barcode?: string; - price?: number; - imageUrl?: string; - detectionConfidence?: number; - isManuallyEdited?: boolean; - lastDetectedAt?: string; - createdAt: string; - updatedAt: string; -} - -export interface InventoryResponse { - items: InventoryItem[]; - total: number; - page: number; - limit: number; - hasMore: boolean; -} - -export const inventoryService = { - getInventory: async ( - storeId: string, - page = 1, - limit = 50 - ): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/inventory`, - { params: { page, limit } } - ); - return response.data; - }, - - getItem: async (storeId: string, itemId: string): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/inventory/${itemId}` - ); - return response.data; - }, - - updateItem: async ( - storeId: string, - itemId: string, - data: Partial - ): Promise => { - const response = await apiClient.patch( - `/stores/${storeId}/inventory/${itemId}`, - data - ); - return response.data; - }, - - deleteItem: async (storeId: string, itemId: string): Promise => { - await apiClient.delete(`/stores/${storeId}/inventory/${itemId}`); - }, -}; diff --git a/src/services/api/notifications.service.ts b/src/services/api/notifications.service.ts deleted file mode 100644 index 2690141..0000000 --- a/src/services/api/notifications.service.ts +++ /dev/null @@ -1,66 +0,0 @@ -import apiClient from './client'; - -export type NotificationType = - | 'VIDEO_PROCESSING_COMPLETE' - | 'VIDEO_PROCESSING_FAILED' - | 'LOW_CREDITS' - | 'PAYMENT_COMPLETE' - | 'PAYMENT_FAILED' - | 'REFERRAL_BONUS' - | 'SYSTEM'; - -export interface Notification { - id: string; - userId: string; - type: NotificationType; - title: string; - body: string; - data?: Record; - isRead: boolean; - isPushSent: boolean; - createdAt: string; -} - -export interface NotificationsListResponse { - notifications: Notification[]; - total: number; - page: number; - limit: number; - hasMore: boolean; -} - -export const notificationsService = { - getNotifications: async (page = 1, limit = 20): Promise => { - const response = await apiClient.get('/notifications', { - params: { page, limit }, - }); - return response.data; - }, - - getUnreadCount: async (): Promise<{ count: number }> => { - const response = await apiClient.get<{ count: number }>('/notifications/unread-count'); - return response.data; - }, - - markAsRead: async (notificationId: string): Promise<{ success: boolean }> => { - const response = await apiClient.patch<{ success: boolean }>( - `/notifications/${notificationId}/read` - ); - return response.data; - }, - - markAllAsRead: async (): Promise<{ success: boolean }> => { - const response = await apiClient.post<{ success: boolean }>( - '/notifications/mark-all-read' - ); - return response.data; - }, - - registerFcmToken: async (token: string): Promise<{ success: boolean }> => { - const response = await apiClient.post<{ success: boolean }>( - '/notifications/register-token', - { token } - ); - return response.data; - }, -}; diff --git a/src/services/api/payments.service.ts b/src/services/api/payments.service.ts deleted file mode 100644 index 8be60ed..0000000 --- a/src/services/api/payments.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import apiClient from './client'; - -export interface CreditPackage { - id: string; - name: string; - credits: number; - priceMXN: number; - popular?: boolean; -} - -export interface Payment { - id: string; - userId: string; - packageId: string; - amountMXN: number; - creditsGranted: number; - method: 'CARD' | 'OXXO' | '7ELEVEN'; - status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'REFUNDED'; - stripePaymentIntentId?: string; - voucherCode?: string; - voucherUrl?: string; - expiresAt?: string; - completedAt?: string; - createdAt: string; -} - -export interface CreatePaymentRequest { - packageId: string; - method: 'card' | 'oxxo' | '7eleven'; - paymentMethodId?: string; // Required for card payments -} - -export interface PaymentResponse { - paymentId: string; - status: 'pending' | 'completed'; - method: string; - // Card payment fields - clientSecret?: string; - // OXXO/7-Eleven fields - voucherCode?: string; - voucherUrl?: string; - expiresAt?: string; - amountMXN: number; -} - -export interface PaymentsListResponse { - payments: Payment[]; - total: number; - page: number; - limit: number; - hasMore: boolean; -} - -export const paymentsService = { - getPackages: async (): Promise => { - const response = await apiClient.get('/credits/packages'); - return response.data; - }, - - createPayment: async (data: CreatePaymentRequest): Promise => { - const response = await apiClient.post('/payments', data); - return response.data; - }, - - getPaymentHistory: async (page = 1, limit = 20): Promise => { - const response = await apiClient.get('/payments', { - params: { page, limit }, - }); - return response.data; - }, - - getPaymentById: async (paymentId: string): Promise => { - const response = await apiClient.get(`/payments/${paymentId}`); - return response.data; - }, -}; diff --git a/src/services/api/referrals.service.ts b/src/services/api/referrals.service.ts deleted file mode 100644 index bdbb419..0000000 --- a/src/services/api/referrals.service.ts +++ /dev/null @@ -1,75 +0,0 @@ -import apiClient from './client'; - -export interface ReferralStats { - referralCode: string; - totalReferrals: number; - completedReferrals: number; - pendingReferrals: number; - totalCreditsEarned: number; -} - -export interface Referral { - id: string; - referrerId: string; - referredId: string; - referralCode: string; - status: 'PENDING' | 'REGISTERED' | 'QUALIFIED' | 'REWARDED'; - referrerBonusCredits: number; - referredBonusCredits: number; - registeredAt?: string; - qualifiedAt?: string; - rewardedAt?: string; - createdAt: string; - referred?: { - id: string; - name: string; - createdAt: string; - }; -} - -export interface ReferralsListResponse { - referrals: Referral[]; - total: number; - page: number; - limit: number; - hasMore: boolean; -} - -export interface ValidateCodeResponse { - valid: boolean; - referrerName?: string; -} - -export const referralsService = { - getMyCode: async (): Promise<{ referralCode: string }> => { - const response = await apiClient.get<{ referralCode: string }>('/referrals/my-code'); - return response.data; - }, - - getStats: async (): Promise => { - const response = await apiClient.get('/referrals/stats'); - return response.data; - }, - - getReferrals: async (page = 1, limit = 20): Promise => { - const response = await apiClient.get('/referrals', { - params: { page, limit }, - }); - return response.data; - }, - - validateCode: async (code: string): Promise => { - const response = await apiClient.get('/referrals/validate', { - params: { code }, - }); - return response.data; - }, - - applyCode: async (code: string): Promise<{ success: boolean; message: string }> => { - const response = await apiClient.post<{ success: boolean; message: string }>( - '/referrals/apply', - { code } - ); - return response.data; - }, -}; diff --git a/src/services/api/reports.service.ts b/src/services/api/reports.service.ts deleted file mode 100644 index 541375b..0000000 --- a/src/services/api/reports.service.ts +++ /dev/null @@ -1,171 +0,0 @@ -import apiClient from './client'; - -// Report Types -export interface ValuationSummary { - totalItems: number; - totalCost: number; - totalPrice: number; - potentialMargin: number; - potentialMarginPercent: number; -} - -export interface ValuationByCategory { - category: string; - itemCount: number; - totalCost: number; - totalPrice: number; - margin: number; -} - -export interface ValuationItem { - id: string; - name: string; - category: string; - quantity: number; - cost: number; - price: number; - totalCost: number; - totalPrice: number; - margin: number; -} - -export interface ValuationReport { - summary: ValuationSummary; - byCategory: ValuationByCategory[]; - items: ValuationItem[]; -} - -export interface MovementsSummary { - period: { start: string; end: string }; - totalMovements: number; - netChange: number; - itemsIncreased: number; - itemsDecreased: number; -} - -export interface MovementRecord { - id: string; - date: string; - itemId: string; - itemName: string; - type: string; - change: number; - quantityBefore: number; - quantityAfter: number; - reason?: string; -} - -export interface MovementsByItem { - itemId: string; - itemName: string; - netChange: number; - movementCount: number; -} - -export interface MovementsReport { - summary: MovementsSummary; - movements: MovementRecord[]; - byItem: MovementsByItem[]; - total: number; - page: number; - limit: number; - hasMore: boolean; -} - -export interface CategorySummary { - totalCategories: number; - totalItems: number; - totalValue: number; -} - -export interface CategoryDetail { - name: string; - itemCount: number; - percentOfTotal: number; - totalValue: number; - lowStockCount: number; - averagePrice: number; - topItems: { name: string; quantity: number }[]; -} - -export interface CategoriesReport { - summary: CategorySummary; - categories: CategoryDetail[]; -} - -export interface LowStockSummary { - totalAlerts: number; - criticalCount: number; - warningCount: number; - totalValueAtRisk: number; -} - -export interface LowStockItem { - id: string; - name: string; - category: string; - quantity: number; - minStock: number; - shortage: number; - estimatedReorderCost: number; - lastMovementDate?: string; - priority: 'critical' | 'warning' | 'watch'; -} - -export interface LowStockReport { - summary: LowStockSummary; - items: LowStockItem[]; -} - -export interface MovementsQueryParams { - startDate?: string; - endDate?: string; - page?: number; - limit?: number; -} - -export const reportsService = { - /** - * Get valuation report - */ - getValuationReport: async (storeId: string): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/reports/valuation`, - ); - return response.data; - }, - - /** - * Get movements report - */ - getMovementsReport: async ( - storeId: string, - params?: MovementsQueryParams, - ): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/reports/movements`, - { params }, - ); - return response.data; - }, - - /** - * Get categories report - */ - getCategoriesReport: async (storeId: string): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/reports/categories`, - ); - return response.data; - }, - - /** - * Get low stock report - */ - getLowStockReport: async (storeId: string): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/reports/low-stock`, - ); - return response.data; - }, -}; diff --git a/src/services/api/stores.service.ts b/src/services/api/stores.service.ts deleted file mode 100644 index 4e85b87..0000000 --- a/src/services/api/stores.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import apiClient from './client'; - -export interface Store { - id: string; - name: string; - address?: string; - city?: string; - state?: string; - zipCode?: string; - giro?: string; - ownerId: string; - isActive: boolean; - createdAt: string; - updatedAt: string; -} - -export interface CreateStoreRequest { - name: string; - address?: string; - city?: string; - state?: string; - zipCode?: string; - giro?: string; -} - -export interface UpdateStoreRequest { - name?: string; - address?: string; - city?: string; - state?: string; - zipCode?: string; - giro?: string; -} - -export interface StoresListResponse { - stores: Store[]; - total: number; - page: number; - limit: number; - hasMore: boolean; -} - -export const storesService = { - getStores: async (page = 1, limit = 20): Promise => { - const response = await apiClient.get('/stores', { - params: { page, limit }, - }); - return response.data; - }, - - getStoreById: async (storeId: string): Promise => { - const response = await apiClient.get(`/stores/${storeId}`); - return response.data; - }, - - createStore: async (data: CreateStoreRequest): Promise => { - const response = await apiClient.post('/stores', data); - return response.data; - }, - - updateStore: async (storeId: string, data: UpdateStoreRequest): Promise => { - const response = await apiClient.patch(`/stores/${storeId}`, data); - return response.data; - }, - - deleteStore: async (storeId: string): Promise<{ success: boolean }> => { - const response = await apiClient.delete<{ success: boolean }>(`/stores/${storeId}`); - return response.data; - }, -}; diff --git a/src/services/api/users.service.ts b/src/services/api/users.service.ts deleted file mode 100644 index 01a716f..0000000 --- a/src/services/api/users.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import apiClient from './client'; - -export interface UserProfile { - id: string; - name: string; - email?: string; - phone: string; - businessName?: string; - location?: string; - giro?: string; -} - -export interface UpdateProfileRequest { - name?: string; - email?: string; - businessName?: string; - location?: string; - giro?: string; -} - -export const usersService = { - getProfile: async (): Promise => { - const response = await apiClient.get('/users/me'); - return response.data; - }, - - updateProfile: async (data: UpdateProfileRequest): Promise => { - const response = await apiClient.patch('/users/me', data); - return response.data; - }, - - updateFcmToken: async (fcmToken: string): Promise => { - await apiClient.patch('/users/me/fcm-token', { fcmToken }); - }, -}; diff --git a/src/services/api/validations.service.ts b/src/services/api/validations.service.ts deleted file mode 100644 index 90f6244..0000000 --- a/src/services/api/validations.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import apiClient from './client'; - -export interface ValidationRequest { - id: string; - totalItems: number; - itemsValidated: number; - expiresAt: string; - creditsRewarded: number; -} - -export interface ValidationItem { - id: string; - name: string; - quantity: number; - category?: string; - imageUrl?: string; - detectionConfidence?: number; -} - -export interface ValidationItemResponse { - inventoryItemId: string; - isCorrect: boolean; - correctedQuantity?: number; - correctedName?: string; - responseTimeMs?: number; -} - -export interface SubmitValidationRequest { - responses: ValidationItemResponse[]; -} - -export interface SubmitValidationResponse { - creditsRewarded: number; - itemsValidated: number; -} - -const validationsService = { - async check(videoId: string): Promise<{ - validationRequired: boolean; - requestId?: string; - }> { - const response = await apiClient.get(`/validations/check/${videoId}`); - return response.data; - }, - - async getItems(requestId: string): Promise<{ - request: ValidationRequest; - items: ValidationItem[]; - }> { - const response = await apiClient.get(`/validations/${requestId}/items`); - return response.data; - }, - - async submit( - requestId: string, - data: SubmitValidationRequest, - ): Promise { - const response = await apiClient.post( - `/validations/${requestId}/submit`, - data, - ); - return response.data; - }, - - async skip(requestId: string): Promise { - await apiClient.post(`/validations/${requestId}/skip`); - }, -}; - -export default validationsService; diff --git a/src/services/api/videos.service.ts b/src/services/api/videos.service.ts deleted file mode 100644 index a1c44d1..0000000 --- a/src/services/api/videos.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import apiClient from './client'; -import * as FileSystem from 'expo-file-system'; - -interface UploadResponse { - videoId: string; - uploadUrl: string; - status: 'pending' | 'uploading' | 'processing' | 'completed' | 'failed'; -} - -interface VideoStatus { - id: string; - status: 'pending' | 'uploading' | 'processing' | 'completed' | 'failed'; - progress: number; - resultItems?: number; - errorMessage?: string; -} - -interface ProcessingResult { - videoId: string; - itemsDetected: number; - items: Array<{ - name: string; - quantity: number; - confidence: number; - category?: string; - }>; - creditsUsed: number; -} - -export const videosService = { - initiateUpload: async ( - storeId: string, - fileName: string, - fileSize: number - ): Promise => { - const response = await apiClient.post( - `/stores/${storeId}/videos/initiate`, - { fileName, fileSize } - ); - return response.data; - }, - - uploadVideo: async ( - uploadUrl: string, - localUri: string, - onProgress?: (progress: number) => void - ): Promise => { - const uploadTask = FileSystem.createUploadTask( - uploadUrl, - localUri, - { - httpMethod: 'PUT', - headers: { - 'Content-Type': 'video/mp4', - }, - }, - (progressEvent) => { - const progress = - progressEvent.totalBytesSent / progressEvent.totalBytesExpectedToSend; - onProgress?.(progress); - } - ); - - await uploadTask.uploadAsync(); - }, - - confirmUpload: async (storeId: string, videoId: string): Promise => { - await apiClient.post(`/stores/${storeId}/videos/${videoId}/confirm`); - }, - - getStatus: async (storeId: string, videoId: string): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/videos/${videoId}/status` - ); - return response.data; - }, - - getResult: async ( - storeId: string, - videoId: string - ): Promise => { - const response = await apiClient.get( - `/stores/${storeId}/videos/${videoId}/result` - ); - return response.data; - }, -}; diff --git a/src/stores/__tests__/auth.store.spec.ts b/src/stores/__tests__/auth.store.spec.ts deleted file mode 100644 index 8f23acf..0000000 --- a/src/stores/__tests__/auth.store.spec.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { useAuthStore } from '../auth.store'; -import { authService } from '@services/api/auth.service'; - -// Mock the auth service -jest.mock('@services/api/auth.service'); - -const mockAuthService = authService as jest.Mocked; - -describe('Auth Store', () => { - beforeEach(() => { - // Reset store state - useAuthStore.setState({ - user: null, - accessToken: null, - refreshToken: null, - isAuthenticated: false, - isLoading: false, - }); - jest.clearAllMocks(); - }); - - describe('login', () => { - it('should set user and tokens on successful login', async () => { - const mockUser = { id: '1', phone: '+1234567890', name: 'Test User' }; - const mockResponse = { - user: mockUser, - accessToken: 'access-token', - refreshToken: 'refresh-token', - }; - - mockAuthService.login.mockResolvedValue(mockResponse); - - await useAuthStore.getState().login('+1234567890', 'password123'); - - const state = useAuthStore.getState(); - expect(state.user).toEqual(mockUser); - expect(state.accessToken).toBe('access-token'); - expect(state.refreshToken).toBe('refresh-token'); - expect(state.isAuthenticated).toBe(true); - expect(state.isLoading).toBe(false); - }); - - it('should set isLoading during login', async () => { - mockAuthService.login.mockImplementation( - () => - new Promise((resolve) => - setTimeout( - () => - resolve({ - user: { id: '1', phone: '+1234567890', name: 'Test' }, - accessToken: 'token', - refreshToken: 'refresh', - }), - 100 - ) - ) - ); - - const loginPromise = useAuthStore.getState().login('+1234567890', 'pass'); - - // Check loading state during request - expect(useAuthStore.getState().isLoading).toBe(true); - - await loginPromise; - - expect(useAuthStore.getState().isLoading).toBe(false); - }); - - it('should reset isLoading on login failure', async () => { - mockAuthService.login.mockRejectedValue(new Error('Invalid credentials')); - - await expect( - useAuthStore.getState().login('+1234567890', 'wrong') - ).rejects.toThrow('Invalid credentials'); - - expect(useAuthStore.getState().isLoading).toBe(false); - expect(useAuthStore.getState().isAuthenticated).toBe(false); - }); - }); - - describe('initiateRegistration', () => { - it('should call authService.initiateRegistration', async () => { - mockAuthService.initiateRegistration.mockResolvedValue(undefined); - - await useAuthStore.getState().initiateRegistration('+1234567890', 'Test'); - - expect(mockAuthService.initiateRegistration).toHaveBeenCalledWith({ - phone: '+1234567890', - name: 'Test', - }); - }); - }); - - describe('verifyOtp', () => { - it('should set user and tokens on successful verification', async () => { - const mockUser = { id: '1', phone: '+1234567890', name: 'Test User' }; - mockAuthService.verifyOtp.mockResolvedValue({ - user: mockUser, - accessToken: 'access-token', - refreshToken: 'refresh-token', - }); - - await useAuthStore.getState().verifyOtp('+1234567890', '123456', 'pass'); - - const state = useAuthStore.getState(); - expect(state.user).toEqual(mockUser); - expect(state.isAuthenticated).toBe(true); - }); - }); - - describe('logout', () => { - it('should clear all auth state', async () => { - // Set initial authenticated state - useAuthStore.setState({ - user: { id: '1', phone: '+1234567890', name: 'Test' }, - accessToken: 'access-token', - refreshToken: 'refresh-token', - isAuthenticated: true, - }); - - mockAuthService.logout.mockResolvedValue(undefined); - - await useAuthStore.getState().logout(); - - const state = useAuthStore.getState(); - expect(state.user).toBeNull(); - expect(state.accessToken).toBeNull(); - expect(state.refreshToken).toBeNull(); - expect(state.isAuthenticated).toBe(false); - }); - - it('should still clear state if logout API fails', async () => { - useAuthStore.setState({ - user: { id: '1', phone: '+1234567890', name: 'Test' }, - accessToken: 'access-token', - refreshToken: 'refresh-token', - isAuthenticated: true, - }); - - mockAuthService.logout.mockRejectedValue(new Error('Network error')); - - await useAuthStore.getState().logout(); - - expect(useAuthStore.getState().isAuthenticated).toBe(false); - }); - }); - - describe('refreshTokens', () => { - it('should update tokens on successful refresh', async () => { - useAuthStore.setState({ - refreshToken: 'old-refresh-token', - accessToken: 'old-access-token', - }); - - mockAuthService.refreshTokens.mockResolvedValue({ - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }); - - await useAuthStore.getState().refreshTokens(); - - const state = useAuthStore.getState(); - expect(state.accessToken).toBe('new-access-token'); - expect(state.refreshToken).toBe('new-refresh-token'); - }); - - it('should logout on refresh failure', async () => { - useAuthStore.setState({ - refreshToken: 'expired-token', - isAuthenticated: true, - }); - - mockAuthService.refreshTokens.mockRejectedValue(new Error('Invalid token')); - - await useAuthStore.getState().refreshTokens(); - - expect(useAuthStore.getState().isAuthenticated).toBe(false); - }); - - it('should not call API if no refresh token', async () => { - useAuthStore.setState({ refreshToken: null }); - - await useAuthStore.getState().refreshTokens(); - - expect(mockAuthService.refreshTokens).not.toHaveBeenCalled(); - }); - }); - - describe('setUser', () => { - it('should update user', () => { - const newUser = { id: '2', phone: '+9876543210', name: 'Updated User' }; - - useAuthStore.getState().setUser(newUser); - - expect(useAuthStore.getState().user).toEqual(newUser); - }); - }); -}); diff --git a/src/stores/__tests__/credits.store.spec.ts b/src/stores/__tests__/credits.store.spec.ts deleted file mode 100644 index c754f1a..0000000 --- a/src/stores/__tests__/credits.store.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { useCreditsStore } from '../credits.store'; -import { creditsService } from '@services/api/credits.service'; - -jest.mock('@services/api/credits.service'); - -const mockCreditsService = creditsService as jest.Mocked; - -describe('Credits Store', () => { - beforeEach(() => { - useCreditsStore.setState({ - balance: 0, - transactions: [], - isLoading: false, - error: null, - }); - jest.clearAllMocks(); - }); - - describe('fetchBalance', () => { - it('should load current balance', async () => { - mockCreditsService.getBalance.mockResolvedValue({ balance: 100 }); - - await useCreditsStore.getState().fetchBalance(); - - expect(useCreditsStore.getState().balance).toBe(100); - }); - - it('should handle errors', async () => { - mockCreditsService.getBalance.mockRejectedValue(new Error('Failed')); - - await useCreditsStore.getState().fetchBalance(); - - expect(useCreditsStore.getState().error).toBe('Failed'); - }); - }); - - describe('fetchTransactions', () => { - it('should load transaction history', async () => { - const mockTransactions = [ - { id: '1', type: 'PURCHASE', amount: 50, createdAt: new Date().toISOString() }, - { id: '2', type: 'USAGE', amount: -10, createdAt: new Date().toISOString() }, - ]; - - mockCreditsService.getTransactions.mockResolvedValue({ - transactions: mockTransactions, - total: 2, - }); - - await useCreditsStore.getState().fetchTransactions(); - - expect(useCreditsStore.getState().transactions).toHaveLength(2); - }); - }); - - describe('purchaseCredits', () => { - it('should update balance after purchase', async () => { - useCreditsStore.setState({ balance: 50 }); - - mockCreditsService.purchaseCredits.mockResolvedValue({ - newBalance: 150, - transaction: { id: '1', type: 'PURCHASE', amount: 100, createdAt: new Date().toISOString() }, - }); - - await useCreditsStore.getState().purchaseCredits('package-1', 'payment-method-1'); - - expect(useCreditsStore.getState().balance).toBe(150); - }); - - it('should add transaction to history', async () => { - const transaction = { id: '1', type: 'PURCHASE', amount: 100, createdAt: new Date().toISOString() }; - - mockCreditsService.purchaseCredits.mockResolvedValue({ - newBalance: 100, - transaction, - }); - - await useCreditsStore.getState().purchaseCredits('package-1', 'payment-method-1'); - - const transactions = useCreditsStore.getState().transactions; - expect(transactions[0]).toEqual(transaction); - }); - }); - - describe('consumeCredits', () => { - it('should decrease balance', async () => { - useCreditsStore.setState({ balance: 100 }); - - mockCreditsService.consumeCredits.mockResolvedValue({ - newBalance: 90, - transaction: { id: '1', type: 'USAGE', amount: -10, createdAt: new Date().toISOString() }, - }); - - await useCreditsStore.getState().consumeCredits(10, 'Video processing'); - - expect(useCreditsStore.getState().balance).toBe(90); - }); - }); -}); diff --git a/src/stores/__tests__/feedback.store.spec.ts b/src/stores/__tests__/feedback.store.spec.ts deleted file mode 100644 index f65f4d0..0000000 --- a/src/stores/__tests__/feedback.store.spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { useFeedbackStore } from '../feedback.store'; -import { feedbackService } from '@services/api/feedback.service'; - -jest.mock('@services/api/feedback.service'); - -const mockFeedbackService = feedbackService as jest.Mocked; - -describe('Feedback Store', () => { - beforeEach(() => { - useFeedbackStore.setState({ - corrections: [], - isLoading: false, - isSubmitting: false, - error: null, - }); - jest.clearAllMocks(); - }); - - describe('fetchCorrections', () => { - it('should load corrections history', async () => { - const mockCorrections = [ - { id: '1', itemId: 'item-1', type: 'QUANTITY', originalValue: 10, correctedValue: 15, createdAt: new Date().toISOString() }, - { id: '2', itemId: 'item-2', type: 'SKU', originalValue: 'OLD123', correctedValue: 'NEW456', createdAt: new Date().toISOString() }, - ]; - - mockFeedbackService.getCorrections.mockResolvedValue({ corrections: mockCorrections }); - - await useFeedbackStore.getState().fetchCorrections('store-1'); - - expect(useFeedbackStore.getState().corrections).toEqual(mockCorrections); - }); - - it('should handle errors', async () => { - mockFeedbackService.getCorrections.mockRejectedValue(new Error('Failed to load')); - - await useFeedbackStore.getState().fetchCorrections('store-1'); - - expect(useFeedbackStore.getState().error).toBe('Failed to load'); - }); - }); - - describe('submitQuantityCorrection', () => { - it('should submit quantity correction', async () => { - mockFeedbackService.submitCorrection.mockResolvedValue({ - id: '1', - itemId: 'item-1', - type: 'QUANTITY', - originalValue: 10, - correctedValue: 15, - createdAt: new Date().toISOString(), - }); - - const result = await useFeedbackStore.getState().submitQuantityCorrection( - 'store-1', - 'item-1', - 10, - 15, - ); - - expect(result).toBe(true); - expect(mockFeedbackService.submitCorrection).toHaveBeenCalledWith('store-1', { - itemId: 'item-1', - type: 'QUANTITY', - originalValue: 10, - correctedValue: 15, - }); - }); - - it('should add correction to list', async () => { - const newCorrection = { - id: '1', - itemId: 'item-1', - type: 'QUANTITY', - originalValue: 10, - correctedValue: 15, - createdAt: new Date().toISOString(), - }; - - mockFeedbackService.submitCorrection.mockResolvedValue(newCorrection); - - await useFeedbackStore.getState().submitQuantityCorrection( - 'store-1', - 'item-1', - 10, - 15, - ); - - expect(useFeedbackStore.getState().corrections).toContainEqual(newCorrection); - }); - - it('should handle submission errors', async () => { - mockFeedbackService.submitCorrection.mockRejectedValue(new Error('Submission failed')); - - const result = await useFeedbackStore.getState().submitQuantityCorrection( - 'store-1', - 'item-1', - 10, - 15, - ); - - expect(result).toBe(false); - expect(useFeedbackStore.getState().error).toBe('Submission failed'); - }); - }); - - describe('submitSkuCorrection', () => { - it('should submit SKU correction', async () => { - mockFeedbackService.submitCorrection.mockResolvedValue({ - id: '1', - itemId: 'item-1', - type: 'SKU', - originalValue: 'OLD123', - correctedValue: 'NEW456', - createdAt: new Date().toISOString(), - }); - - const result = await useFeedbackStore.getState().submitSkuCorrection( - 'store-1', - 'item-1', - 'OLD123', - 'NEW456', - ); - - expect(result).toBe(true); - expect(mockFeedbackService.submitCorrection).toHaveBeenCalledWith('store-1', { - itemId: 'item-1', - type: 'SKU', - originalValue: 'OLD123', - correctedValue: 'NEW456', - }); - }); - }); - - describe('confirmItem', () => { - it('should confirm item detection', async () => { - mockFeedbackService.confirmItem.mockResolvedValue({ success: true }); - - const result = await useFeedbackStore.getState().confirmItem('store-1', 'item-1'); - - expect(result).toBe(true); - expect(mockFeedbackService.confirmItem).toHaveBeenCalledWith('store-1', 'item-1'); - }); - - it('should handle confirmation errors', async () => { - mockFeedbackService.confirmItem.mockRejectedValue(new Error('Failed')); - - const result = await useFeedbackStore.getState().confirmItem('store-1', 'item-1'); - - expect(result).toBe(false); - }); - }); - - describe('clearError', () => { - it('should clear error state', () => { - useFeedbackStore.setState({ error: 'Some error' }); - - useFeedbackStore.getState().clearError(); - - expect(useFeedbackStore.getState().error).toBeNull(); - }); - }); -}); diff --git a/src/stores/__tests__/inventory.store.spec.ts b/src/stores/__tests__/inventory.store.spec.ts deleted file mode 100644 index dad6957..0000000 --- a/src/stores/__tests__/inventory.store.spec.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { useInventoryStore } from '../inventory.store'; -import { inventoryService } from '@services/api/inventory.service'; - -jest.mock('@services/api/inventory.service'); - -const mockInventoryService = inventoryService as jest.Mocked< - typeof inventoryService ->; - -describe('Inventory Store', () => { - beforeEach(() => { - useInventoryStore.setState({ - items: [], - isLoading: false, - error: null, - currentPage: 1, - hasMore: true, - searchQuery: '', - categoryFilter: null, - }); - jest.clearAllMocks(); - }); - - describe('fetchItems', () => { - it('should load inventory items', async () => { - const mockItems = [ - { id: '1', name: 'Item 1', quantity: 10, storeId: 'store-1' }, - { id: '2', name: 'Item 2', quantity: 5, storeId: 'store-1' }, - ]; - - mockInventoryService.getItems.mockResolvedValue({ - items: mockItems, - total: 2, - page: 1, - limit: 50, - hasMore: false, - }); - - await useInventoryStore.getState().fetchItems('store-1'); - - const state = useInventoryStore.getState(); - expect(state.items).toHaveLength(2); - expect(state.hasMore).toBe(false); - expect(state.error).toBeNull(); - }); - - it('should handle fetch errors', async () => { - mockInventoryService.getItems.mockRejectedValue( - new Error('Failed to fetch') - ); - - await useInventoryStore.getState().fetchItems('store-1'); - - expect(useInventoryStore.getState().error).toBe('Failed to fetch'); - }); - - it('should set loading state during fetch', async () => { - mockInventoryService.getItems.mockImplementation( - () => - new Promise((resolve) => - setTimeout( - () => - resolve({ - items: [], - total: 0, - page: 1, - limit: 50, - hasMore: false, - }), - 100 - ) - ) - ); - - const fetchPromise = useInventoryStore.getState().fetchItems('store-1'); - expect(useInventoryStore.getState().isLoading).toBe(true); - - await fetchPromise; - expect(useInventoryStore.getState().isLoading).toBe(false); - }); - }); - - describe('loadMore', () => { - it('should load next page and append items', async () => { - useInventoryStore.setState({ - items: [{ id: '1', name: 'Item 1', quantity: 10, storeId: 'store-1' }], - currentPage: 1, - hasMore: true, - }); - - mockInventoryService.getItems.mockResolvedValue({ - items: [{ id: '2', name: 'Item 2', quantity: 5, storeId: 'store-1' }], - total: 2, - page: 2, - limit: 50, - hasMore: false, - }); - - await useInventoryStore.getState().loadMore('store-1'); - - const state = useInventoryStore.getState(); - expect(state.items).toHaveLength(2); - expect(state.currentPage).toBe(2); - }); - - it('should not load if hasMore is false', async () => { - useInventoryStore.setState({ hasMore: false }); - - await useInventoryStore.getState().loadMore('store-1'); - - expect(mockInventoryService.getItems).not.toHaveBeenCalled(); - }); - }); - - describe('updateItem', () => { - it('should update an item in the list', async () => { - useInventoryStore.setState({ - items: [ - { id: '1', name: 'Item 1', quantity: 10, storeId: 'store-1' }, - { id: '2', name: 'Item 2', quantity: 5, storeId: 'store-1' }, - ], - }); - - const updatedItem = { - id: '1', - name: 'Updated Item', - quantity: 20, - storeId: 'store-1', - }; - mockInventoryService.updateItem.mockResolvedValue(updatedItem); - - await useInventoryStore - .getState() - .updateItem('store-1', '1', { name: 'Updated Item', quantity: 20 }); - - const items = useInventoryStore.getState().items; - expect(items[0].name).toBe('Updated Item'); - expect(items[0].quantity).toBe(20); - }); - }); - - describe('deleteItem', () => { - it('should remove item from the list', async () => { - useInventoryStore.setState({ - items: [ - { id: '1', name: 'Item 1', quantity: 10, storeId: 'store-1' }, - { id: '2', name: 'Item 2', quantity: 5, storeId: 'store-1' }, - ], - }); - - mockInventoryService.deleteItem.mockResolvedValue(undefined); - - await useInventoryStore.getState().deleteItem('store-1', '1'); - - const items = useInventoryStore.getState().items; - expect(items).toHaveLength(1); - expect(items[0].id).toBe('2'); - }); - }); - - describe('setSearchQuery', () => { - it('should update search query', () => { - useInventoryStore.getState().setSearchQuery('test search'); - - expect(useInventoryStore.getState().searchQuery).toBe('test search'); - }); - }); - - describe('setCategoryFilter', () => { - it('should update category filter', () => { - useInventoryStore.getState().setCategoryFilter('Electronics'); - - expect(useInventoryStore.getState().categoryFilter).toBe('Electronics'); - }); - - it('should allow null filter', () => { - useInventoryStore.setState({ categoryFilter: 'Electronics' }); - useInventoryStore.getState().setCategoryFilter(null); - - expect(useInventoryStore.getState().categoryFilter).toBeNull(); - }); - }); - - describe('clearItems', () => { - it('should reset items and pagination', () => { - useInventoryStore.setState({ - items: [{ id: '1', name: 'Item', quantity: 10, storeId: 'store-1' }], - currentPage: 5, - hasMore: false, - }); - - useInventoryStore.getState().clearItems(); - - const state = useInventoryStore.getState(); - expect(state.items).toHaveLength(0); - expect(state.currentPage).toBe(1); - expect(state.hasMore).toBe(true); - }); - }); -}); diff --git a/src/stores/__tests__/notifications.store.spec.ts b/src/stores/__tests__/notifications.store.spec.ts deleted file mode 100644 index 1f88031..0000000 --- a/src/stores/__tests__/notifications.store.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { useNotificationsStore } from '../notifications.store'; -import { notificationsService } from '@services/api/notifications.service'; - -jest.mock('@services/api/notifications.service'); - -const mockNotificationsService = notificationsService as jest.Mocked< - typeof notificationsService ->; - -describe('Notifications Store', () => { - beforeEach(() => { - useNotificationsStore.setState({ - notifications: [], - unreadCount: 0, - isLoading: false, - error: null, - }); - jest.clearAllMocks(); - }); - - describe('fetchNotifications', () => { - it('should load notifications', async () => { - const mockNotifications = [ - { id: '1', title: 'Notification 1', read: false, createdAt: new Date().toISOString() }, - { id: '2', title: 'Notification 2', read: true, createdAt: new Date().toISOString() }, - ]; - - mockNotificationsService.getNotifications.mockResolvedValue({ - notifications: mockNotifications, - unreadCount: 1, - }); - - await useNotificationsStore.getState().fetchNotifications(); - - const state = useNotificationsStore.getState(); - expect(state.notifications).toHaveLength(2); - expect(state.unreadCount).toBe(1); - }); - }); - - describe('markAsRead', () => { - it('should mark notification as read', async () => { - useNotificationsStore.setState({ - notifications: [ - { id: '1', title: 'Notification 1', read: false, createdAt: new Date().toISOString() }, - ], - unreadCount: 1, - }); - - mockNotificationsService.markAsRead.mockResolvedValue(undefined); - - await useNotificationsStore.getState().markAsRead('1'); - - const state = useNotificationsStore.getState(); - expect(state.notifications[0].read).toBe(true); - expect(state.unreadCount).toBe(0); - }); - }); - - describe('markAllAsRead', () => { - it('should mark all notifications as read', async () => { - useNotificationsStore.setState({ - notifications: [ - { id: '1', title: 'N1', read: false, createdAt: new Date().toISOString() }, - { id: '2', title: 'N2', read: false, createdAt: new Date().toISOString() }, - ], - unreadCount: 2, - }); - - mockNotificationsService.markAllAsRead.mockResolvedValue(undefined); - - await useNotificationsStore.getState().markAllAsRead(); - - const state = useNotificationsStore.getState(); - expect(state.notifications.every((n) => n.read)).toBe(true); - expect(state.unreadCount).toBe(0); - }); - }); - - describe('deleteNotification', () => { - it('should remove notification from list', async () => { - useNotificationsStore.setState({ - notifications: [ - { id: '1', title: 'N1', read: false, createdAt: new Date().toISOString() }, - { id: '2', title: 'N2', read: true, createdAt: new Date().toISOString() }, - ], - unreadCount: 1, - }); - - mockNotificationsService.deleteNotification.mockResolvedValue(undefined); - - await useNotificationsStore.getState().deleteNotification('1'); - - const state = useNotificationsStore.getState(); - expect(state.notifications).toHaveLength(1); - expect(state.notifications[0].id).toBe('2'); - expect(state.unreadCount).toBe(0); - }); - }); -}); diff --git a/src/stores/__tests__/payments.store.spec.ts b/src/stores/__tests__/payments.store.spec.ts deleted file mode 100644 index e4f0dfa..0000000 --- a/src/stores/__tests__/payments.store.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { usePaymentsStore } from '../payments.store'; -import { paymentsService } from '@services/api/payments.service'; - -jest.mock('@services/api/payments.service'); - -const mockPaymentsService = paymentsService as jest.Mocked; - -describe('Payments Store', () => { - beforeEach(() => { - usePaymentsStore.setState({ - packages: [], - payments: [], - currentPayment: null, - total: 0, - page: 1, - hasMore: false, - isLoading: false, - isProcessing: false, - error: null, - }); - jest.clearAllMocks(); - }); - - describe('fetchPackages', () => { - it('should load available packages', async () => { - const mockPackages = [ - { id: '1', name: 'Basic', credits: 100, price: 9.99 }, - { id: '2', name: 'Pro', credits: 500, price: 39.99 }, - ]; - - mockPaymentsService.getPackages.mockResolvedValue(mockPackages); - - await usePaymentsStore.getState().fetchPackages(); - - expect(usePaymentsStore.getState().packages).toEqual(mockPackages); - expect(usePaymentsStore.getState().error).toBeNull(); - }); - - it('should handle errors', async () => { - mockPaymentsService.getPackages.mockRejectedValue(new Error('Network error')); - - await usePaymentsStore.getState().fetchPackages(); - - expect(usePaymentsStore.getState().error).toBe('Network error'); - }); - }); - - describe('fetchPayments', () => { - it('should load payment history', async () => { - const mockPayments = [ - { id: '1', amount: 9.99, status: 'COMPLETED', createdAt: new Date().toISOString() }, - { id: '2', amount: 39.99, status: 'COMPLETED', createdAt: new Date().toISOString() }, - ]; - - mockPaymentsService.getPaymentHistory.mockResolvedValue({ - payments: mockPayments, - total: 2, - page: 1, - hasMore: false, - }); - - await usePaymentsStore.getState().fetchPayments(true); - - expect(usePaymentsStore.getState().payments).toEqual(mockPayments); - expect(usePaymentsStore.getState().total).toBe(2); - }); - - it('should append payments when not refreshing', async () => { - usePaymentsStore.setState({ - payments: [{ id: '1', amount: 9.99, status: 'COMPLETED', createdAt: new Date().toISOString() }], - page: 2, - }); - - mockPaymentsService.getPaymentHistory.mockResolvedValue({ - payments: [{ id: '2', amount: 39.99, status: 'COMPLETED', createdAt: new Date().toISOString() }], - total: 2, - page: 2, - hasMore: false, - }); - - await usePaymentsStore.getState().fetchPayments(false); - - expect(usePaymentsStore.getState().payments).toHaveLength(2); - }); - }); - - describe('createPayment', () => { - it('should create payment and store response', async () => { - const mockResponse = { - paymentId: 'payment-1', - checkoutUrl: 'https://checkout.example.com', - status: 'PENDING', - }; - - mockPaymentsService.createPayment.mockResolvedValue(mockResponse); - - const result = await usePaymentsStore.getState().createPayment({ - packageId: 'package-1', - paymentMethod: 'card', - }); - - expect(result).toEqual(mockResponse); - expect(usePaymentsStore.getState().currentPayment).toEqual(mockResponse); - expect(usePaymentsStore.getState().isProcessing).toBe(false); - }); - - it('should handle payment errors', async () => { - mockPaymentsService.createPayment.mockRejectedValue(new Error('Payment failed')); - - const result = await usePaymentsStore.getState().createPayment({ - packageId: 'package-1', - paymentMethod: 'card', - }); - - expect(result).toBeNull(); - expect(usePaymentsStore.getState().error).toBe('Payment failed'); - }); - }); - - describe('getPaymentById', () => { - it('should fetch payment by ID', async () => { - const mockPayment = { id: '1', amount: 9.99, status: 'COMPLETED', createdAt: new Date().toISOString() }; - mockPaymentsService.getPaymentById.mockResolvedValue(mockPayment); - - const result = await usePaymentsStore.getState().getPaymentById('1'); - - expect(result).toEqual(mockPayment); - }); - }); - - describe('clearCurrentPayment', () => { - it('should clear current payment', () => { - usePaymentsStore.setState({ - currentPayment: { paymentId: '1', checkoutUrl: 'url', status: 'PENDING' }, - }); - - usePaymentsStore.getState().clearCurrentPayment(); - - expect(usePaymentsStore.getState().currentPayment).toBeNull(); - }); - }); - - describe('clearError', () => { - it('should clear error state', () => { - usePaymentsStore.setState({ error: 'Some error' }); - - usePaymentsStore.getState().clearError(); - - expect(usePaymentsStore.getState().error).toBeNull(); - }); - }); -}); diff --git a/src/stores/__tests__/referrals.store.spec.ts b/src/stores/__tests__/referrals.store.spec.ts deleted file mode 100644 index 312c1a1..0000000 --- a/src/stores/__tests__/referrals.store.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { useReferralsStore } from '../referrals.store'; -import { referralsService } from '@services/api/referrals.service'; - -jest.mock('@services/api/referrals.service'); - -const mockReferralsService = referralsService as jest.Mocked; - -describe('Referrals Store', () => { - beforeEach(() => { - useReferralsStore.setState({ - referralCode: null, - referrals: [], - stats: null, - isLoading: false, - error: null, - }); - jest.clearAllMocks(); - }); - - describe('fetchReferralCode', () => { - it('should load referral code', async () => { - mockReferralsService.getReferralCode.mockResolvedValue({ - code: 'REF123', - shareUrl: 'https://app.example.com/r/REF123', - }); - - await useReferralsStore.getState().fetchReferralCode(); - - expect(useReferralsStore.getState().referralCode).toBe('REF123'); - }); - - it('should handle errors', async () => { - mockReferralsService.getReferralCode.mockRejectedValue(new Error('Failed')); - - await useReferralsStore.getState().fetchReferralCode(); - - expect(useReferralsStore.getState().error).toBe('Failed'); - }); - }); - - describe('fetchReferrals', () => { - it('should load referral list', async () => { - const mockReferrals = [ - { id: '1', referredUserId: 'user-1', status: 'COMPLETED', creditsEarned: 50, createdAt: new Date().toISOString() }, - { id: '2', referredUserId: 'user-2', status: 'PENDING', creditsEarned: 0, createdAt: new Date().toISOString() }, - ]; - - mockReferralsService.getReferrals.mockResolvedValue({ referrals: mockReferrals }); - - await useReferralsStore.getState().fetchReferrals(); - - expect(useReferralsStore.getState().referrals).toEqual(mockReferrals); - }); - }); - - describe('fetchStats', () => { - it('should load referral statistics', async () => { - const mockStats = { - totalReferrals: 10, - completedReferrals: 8, - pendingReferrals: 2, - totalCreditsEarned: 400, - }; - - mockReferralsService.getReferralStats.mockResolvedValue(mockStats); - - await useReferralsStore.getState().fetchStats(); - - expect(useReferralsStore.getState().stats).toEqual(mockStats); - }); - }); - - describe('applyReferralCode', () => { - it('should apply referral code successfully', async () => { - mockReferralsService.applyReferralCode.mockResolvedValue({ - success: true, - creditsAwarded: 25, - }); - - const result = await useReferralsStore.getState().applyReferralCode('FRIEND123'); - - expect(result).toBe(true); - expect(mockReferralsService.applyReferralCode).toHaveBeenCalledWith('FRIEND123'); - }); - - it('should handle invalid referral code', async () => { - mockReferralsService.applyReferralCode.mockRejectedValue(new Error('Invalid code')); - - const result = await useReferralsStore.getState().applyReferralCode('INVALID'); - - expect(result).toBe(false); - expect(useReferralsStore.getState().error).toBe('Invalid code'); - }); - }); -}); diff --git a/src/stores/__tests__/stores.store.spec.ts b/src/stores/__tests__/stores.store.spec.ts deleted file mode 100644 index 4062e9c..0000000 --- a/src/stores/__tests__/stores.store.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { useStoresStore } from '../stores.store'; -import { storesService } from '@services/api/stores.service'; - -jest.mock('@services/api/stores.service'); - -const mockStoresService = storesService as jest.Mocked; - -describe('Stores Store', () => { - beforeEach(() => { - useStoresStore.setState({ - stores: [], - currentStore: null, - isLoading: false, - error: null, - }); - jest.clearAllMocks(); - }); - - describe('fetchStores', () => { - it('should load all stores', async () => { - const mockStores = [ - { id: '1', name: 'Store 1', ownerId: 'user-1' }, - { id: '2', name: 'Store 2', ownerId: 'user-1' }, - ]; - - mockStoresService.getStores.mockResolvedValue(mockStores); - - await useStoresStore.getState().fetchStores(); - - expect(useStoresStore.getState().stores).toEqual(mockStores); - }); - - it('should set first store as current if none selected', async () => { - const mockStores = [ - { id: '1', name: 'Store 1', ownerId: 'user-1' }, - { id: '2', name: 'Store 2', ownerId: 'user-1' }, - ]; - - mockStoresService.getStores.mockResolvedValue(mockStores); - - await useStoresStore.getState().fetchStores(); - - expect(useStoresStore.getState().currentStore).toEqual(mockStores[0]); - }); - - it('should handle errors', async () => { - mockStoresService.getStores.mockRejectedValue(new Error('Network error')); - - await useStoresStore.getState().fetchStores(); - - expect(useStoresStore.getState().error).toBe('Network error'); - }); - }); - - describe('createStore', () => { - it('should add new store to list', async () => { - const newStore = { id: '3', name: 'New Store', ownerId: 'user-1' }; - mockStoresService.createStore.mockResolvedValue(newStore); - - await useStoresStore.getState().createStore({ name: 'New Store' }); - - const stores = useStoresStore.getState().stores; - expect(stores).toContainEqual(newStore); - }); - - it('should set new store as current', async () => { - const newStore = { id: '3', name: 'New Store', ownerId: 'user-1' }; - mockStoresService.createStore.mockResolvedValue(newStore); - - await useStoresStore.getState().createStore({ name: 'New Store' }); - - expect(useStoresStore.getState().currentStore).toEqual(newStore); - }); - }); - - describe('updateStore', () => { - it('should update store in list', async () => { - useStoresStore.setState({ - stores: [{ id: '1', name: 'Store 1', ownerId: 'user-1' }], - }); - - const updatedStore = { id: '1', name: 'Updated Store', ownerId: 'user-1' }; - mockStoresService.updateStore.mockResolvedValue(updatedStore); - - await useStoresStore.getState().updateStore('1', { name: 'Updated Store' }); - - expect(useStoresStore.getState().stores[0].name).toBe('Updated Store'); - }); - - it('should update currentStore if it was updated', async () => { - const currentStore = { id: '1', name: 'Store 1', ownerId: 'user-1' }; - useStoresStore.setState({ - stores: [currentStore], - currentStore, - }); - - const updatedStore = { id: '1', name: 'Updated Store', ownerId: 'user-1' }; - mockStoresService.updateStore.mockResolvedValue(updatedStore); - - await useStoresStore.getState().updateStore('1', { name: 'Updated Store' }); - - expect(useStoresStore.getState().currentStore?.name).toBe('Updated Store'); - }); - }); - - describe('deleteStore', () => { - it('should remove store from list', async () => { - useStoresStore.setState({ - stores: [ - { id: '1', name: 'Store 1', ownerId: 'user-1' }, - { id: '2', name: 'Store 2', ownerId: 'user-1' }, - ], - }); - - mockStoresService.deleteStore.mockResolvedValue(undefined); - - await useStoresStore.getState().deleteStore('1'); - - const stores = useStoresStore.getState().stores; - expect(stores).toHaveLength(1); - expect(stores[0].id).toBe('2'); - }); - - it('should clear currentStore if deleted', async () => { - const storeToDelete = { id: '1', name: 'Store 1', ownerId: 'user-1' }; - useStoresStore.setState({ - stores: [storeToDelete], - currentStore: storeToDelete, - }); - - mockStoresService.deleteStore.mockResolvedValue(undefined); - - await useStoresStore.getState().deleteStore('1'); - - expect(useStoresStore.getState().currentStore).toBeNull(); - }); - }); - - describe('setCurrentStore', () => { - it('should set current store', () => { - const store = { id: '1', name: 'Store 1', ownerId: 'user-1' }; - useStoresStore.setState({ stores: [store] }); - - useStoresStore.getState().setCurrentStore(store); - - expect(useStoresStore.getState().currentStore).toEqual(store); - }); - }); -}); diff --git a/src/stores/__tests__/validations.store.spec.ts b/src/stores/__tests__/validations.store.spec.ts deleted file mode 100644 index 116f534..0000000 --- a/src/stores/__tests__/validations.store.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { useValidationsStore } from '../validations.store'; -import { validationsService } from '@services/api/validations.service'; - -jest.mock('@services/api/validations.service'); - -const mockValidationsService = validationsService as jest.Mocked; - -describe('Validations Store', () => { - beforeEach(() => { - useValidationsStore.setState({ - currentValidation: null, - pendingItems: [], - validatedItems: [], - progress: 0, - isLoading: false, - error: null, - }); - jest.clearAllMocks(); - }); - - describe('startValidation', () => { - it('should start a new validation session', async () => { - const mockValidation = { - id: 'validation-1', - storeId: 'store-1', - videoId: 'video-1', - status: 'IN_PROGRESS', - totalItems: 10, - validatedItems: 0, - }; - - mockValidationsService.startValidation.mockResolvedValue(mockValidation); - - await useValidationsStore.getState().startValidation('store-1', 'video-1'); - - expect(useValidationsStore.getState().currentValidation).toEqual(mockValidation); - }); - - it('should handle errors', async () => { - mockValidationsService.startValidation.mockRejectedValue(new Error('Failed to start')); - - await useValidationsStore.getState().startValidation('store-1', 'video-1'); - - expect(useValidationsStore.getState().error).toBe('Failed to start'); - }); - }); - - describe('fetchPendingItems', () => { - it('should load pending items for validation', async () => { - const mockItems = [ - { id: '1', name: 'Item 1', quantity: 10, status: 'PENDING' }, - { id: '2', name: 'Item 2', quantity: 5, status: 'PENDING' }, - ]; - - mockValidationsService.getPendingItems.mockResolvedValue({ items: mockItems }); - - await useValidationsStore.getState().fetchPendingItems('validation-1'); - - expect(useValidationsStore.getState().pendingItems).toEqual(mockItems); - }); - }); - - describe('validateItem', () => { - it('should validate an item as correct', async () => { - useValidationsStore.setState({ - pendingItems: [ - { id: '1', name: 'Item 1', quantity: 10, status: 'PENDING' }, - { id: '2', name: 'Item 2', quantity: 5, status: 'PENDING' }, - ], - validatedItems: [], - progress: 0, - }); - - mockValidationsService.validateItem.mockResolvedValue({ - success: true, - item: { id: '1', name: 'Item 1', quantity: 10, status: 'VALIDATED' }, - }); - - await useValidationsStore.getState().validateItem('validation-1', '1', true); - - const state = useValidationsStore.getState(); - expect(state.pendingItems).toHaveLength(1); - expect(state.validatedItems).toHaveLength(1); - expect(state.progress).toBe(50); - }); - - it('should validate an item with correction', async () => { - useValidationsStore.setState({ - pendingItems: [{ id: '1', name: 'Item 1', quantity: 10, status: 'PENDING' }], - validatedItems: [], - }); - - mockValidationsService.validateItem.mockResolvedValue({ - success: true, - item: { id: '1', name: 'Item 1', quantity: 15, status: 'CORRECTED' }, - }); - - await useValidationsStore.getState().validateItem('validation-1', '1', false, 15); - - expect(mockValidationsService.validateItem).toHaveBeenCalledWith( - 'validation-1', - '1', - false, - 15, - ); - }); - }); - - describe('completeValidation', () => { - it('should complete the validation session', async () => { - useValidationsStore.setState({ - currentValidation: { id: 'validation-1', status: 'IN_PROGRESS', totalItems: 10, validatedItems: 10 }, - }); - - mockValidationsService.completeValidation.mockResolvedValue({ - id: 'validation-1', - status: 'COMPLETED', - totalItems: 10, - validatedItems: 10, - }); - - await useValidationsStore.getState().completeValidation('validation-1'); - - expect(useValidationsStore.getState().currentValidation?.status).toBe('COMPLETED'); - }); - }); - - describe('clearValidation', () => { - it('should reset all validation state', () => { - useValidationsStore.setState({ - currentValidation: { id: '1', status: 'IN_PROGRESS', totalItems: 10, validatedItems: 5 }, - pendingItems: [{ id: '1', name: 'Item', quantity: 10, status: 'PENDING' }], - validatedItems: [{ id: '2', name: 'Item 2', quantity: 5, status: 'VALIDATED' }], - progress: 50, - }); - - useValidationsStore.getState().clearValidation(); - - const state = useValidationsStore.getState(); - expect(state.currentValidation).toBeNull(); - expect(state.pendingItems).toHaveLength(0); - expect(state.validatedItems).toHaveLength(0); - expect(state.progress).toBe(0); - }); - }); -}); diff --git a/src/stores/auth.store.ts b/src/stores/auth.store.ts deleted file mode 100644 index 4a1e659..0000000 --- a/src/stores/auth.store.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; -import * as SecureStore from 'expo-secure-store'; -import { authService } from '@services/api/auth.service'; - -interface User { - id: string; - phone: string; - name: string; - email?: string; -} - -interface AuthState { - user: User | null; - accessToken: string | null; - refreshToken: string | null; - isAuthenticated: boolean; - isLoading: boolean; - - // Actions - login: (phone: string, password: string) => Promise; - initiateRegistration: (phone: string, name: string) => Promise; - verifyOtp: (phone: string, otp: string, password: string) => Promise; - logout: () => Promise; - refreshTokens: () => Promise; - setUser: (user: User) => void; -} - -const secureStorage = { - getItem: async (name: string): Promise => { - return await SecureStore.getItemAsync(name); - }, - setItem: async (name: string, value: string): Promise => { - await SecureStore.setItemAsync(name, value); - }, - removeItem: async (name: string): Promise => { - await SecureStore.deleteItemAsync(name); - }, -}; - -export const useAuthStore = create()( - persist( - (set, get) => ({ - user: null, - accessToken: null, - refreshToken: null, - isAuthenticated: false, - isLoading: false, - - login: async (phone: string, password: string) => { - set({ isLoading: true }); - try { - const response = await authService.login({ phone, password }); - set({ - user: response.user, - accessToken: response.accessToken, - refreshToken: response.refreshToken, - isAuthenticated: true, - }); - } finally { - set({ isLoading: false }); - } - }, - - initiateRegistration: async (phone: string, name: string) => { - set({ isLoading: true }); - try { - await authService.initiateRegistration({ phone, name }); - } finally { - set({ isLoading: false }); - } - }, - - verifyOtp: async (phone: string, otp: string, password: string) => { - set({ isLoading: true }); - try { - const response = await authService.verifyOtp({ phone, otp, password }); - set({ - user: response.user, - accessToken: response.accessToken, - refreshToken: response.refreshToken, - isAuthenticated: true, - }); - } finally { - set({ isLoading: false }); - } - }, - - logout: async () => { - const { refreshToken } = get(); - if (refreshToken) { - try { - await authService.logout(refreshToken); - } catch { - // Ignore logout errors - } - } - set({ - user: null, - accessToken: null, - refreshToken: null, - isAuthenticated: false, - }); - }, - - refreshTokens: async () => { - const { refreshToken } = get(); - if (!refreshToken) return; - - try { - const response = await authService.refreshTokens(refreshToken); - set({ - accessToken: response.accessToken, - refreshToken: response.refreshToken, - }); - } catch { - // If refresh fails, logout - get().logout(); - } - }, - - setUser: (user: User) => { - set({ user }); - }, - }), - { - name: 'auth-storage', - storage: createJSONStorage(() => secureStorage), - partialize: (state) => ({ - user: state.user, - accessToken: state.accessToken, - refreshToken: state.refreshToken, - isAuthenticated: state.isAuthenticated, - }), - } - ) -); diff --git a/src/stores/credits.store.ts b/src/stores/credits.store.ts deleted file mode 100644 index 372defa..0000000 --- a/src/stores/credits.store.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { creditsService } from '@services/api/credits.service'; - -interface CreditBalance { - balance: number; - totalPurchased: number; - totalConsumed: number; - totalFromReferrals: number; -} - -interface Transaction { - id: string; - type: string; - amount: number; - description: string; - createdAt: string; -} - -interface CreditsState { - balance: CreditBalance | null; - transactions: Transaction[]; - transactionsTotal: number; - transactionsPage: number; - transactionsHasMore: boolean; - isLoading: boolean; - error: string | null; - lastFetched: number | null; - - // Actions - fetchBalance: () => Promise; - fetchTransactions: (refresh?: boolean) => Promise; - deductCredits: (amount: number) => void; - addCredits: (amount: number) => void; - clearError: () => void; -} - -const MAX_CACHED_TRANSACTIONS = 50; - -export const useCreditsStore = create()( - persist( - (set, get) => ({ - balance: null, - transactions: [], - transactionsTotal: 0, - transactionsPage: 1, - transactionsHasMore: false, - isLoading: false, - error: null, - lastFetched: null, - - fetchBalance: async () => { - set({ isLoading: true, error: null }); - try { - const response = await creditsService.getBalance(); - set({ - balance: { - balance: response.balance, - totalPurchased: response.totalPurchased || 0, - totalConsumed: response.totalConsumed || 0, - totalFromReferrals: response.totalFromReferrals || 0, - }, - isLoading: false, - lastFetched: Date.now(), - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al cargar balance'; - set({ error: message, isLoading: false }); - } - }, - - fetchTransactions: async (refresh = false) => { - const state = get(); - if (state.isLoading && !refresh) return; - - const page = refresh ? 1 : state.transactionsPage; - - set({ isLoading: true, error: null }); - - try { - const response = await creditsService.getTransactions(page, 20); - const newTransactions = refresh - ? response.transactions - : [...state.transactions, ...response.transactions]; - - set({ - transactions: newTransactions.slice(0, MAX_CACHED_TRANSACTIONS), - transactionsTotal: response.total, - transactionsPage: page + 1, - transactionsHasMore: page * 20 < response.total, - isLoading: false, - lastFetched: Date.now(), - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al cargar transacciones'; - set({ error: message, isLoading: false }); - } - }, - - deductCredits: (amount: number) => { - set((state) => ({ - balance: state.balance - ? { - ...state.balance, - balance: Math.max(0, state.balance.balance - amount), - totalConsumed: state.balance.totalConsumed + amount, - } - : null, - })); - }, - - addCredits: (amount: number) => { - set((state) => ({ - balance: state.balance - ? { - ...state.balance, - balance: state.balance.balance + amount, - totalPurchased: state.balance.totalPurchased + amount, - } - : null, - })); - }, - - clearError: () => set({ error: null }), - }), - { - name: 'miinventario-credits', - storage: createJSONStorage(() => AsyncStorage), - partialize: (state) => ({ - balance: state.balance, - transactions: state.transactions.slice(0, MAX_CACHED_TRANSACTIONS), - transactionsTotal: state.transactionsTotal, - lastFetched: state.lastFetched, - }), - } - ) -); diff --git a/src/stores/feedback.store.ts b/src/stores/feedback.store.ts deleted file mode 100644 index b3730ca..0000000 --- a/src/stores/feedback.store.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { create } from 'zustand'; -import feedbackService, { - CorrectionHistoryItem, - CorrectQuantityRequest, - CorrectSkuRequest, - SubmitProductRequest, - ProductSearchResult, -} from '../services/api/feedback.service'; - -interface FeedbackState { - correctionHistory: CorrectionHistoryItem[]; - searchResults: ProductSearchResult[]; - isLoading: boolean; - error: string | null; - - // Actions - correctQuantity: ( - storeId: string, - itemId: string, - data: CorrectQuantityRequest, - ) => Promise; - correctSku: ( - storeId: string, - itemId: string, - data: CorrectSkuRequest, - ) => Promise; - confirmItem: (storeId: string, itemId: string) => Promise; - fetchCorrectionHistory: (storeId: string, itemId: string) => Promise; - submitProduct: (data: SubmitProductRequest) => Promise; - searchProducts: (query: string) => Promise; - clearError: () => void; - reset: () => void; -} - -export const useFeedbackStore = create((set, get) => ({ - correctionHistory: [], - searchResults: [], - isLoading: false, - error: null, - - correctQuantity: async (storeId, itemId, data) => { - set({ isLoading: true, error: null }); - try { - await feedbackService.correctQuantity(storeId, itemId, data); - // Refresh history after correction - await get().fetchCorrectionHistory(storeId, itemId); - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error al corregir cantidad' }); - throw error; - } finally { - set({ isLoading: false }); - } - }, - - correctSku: async (storeId, itemId, data) => { - set({ isLoading: true, error: null }); - try { - await feedbackService.correctSku(storeId, itemId, data); - await get().fetchCorrectionHistory(storeId, itemId); - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error al corregir nombre' }); - throw error; - } finally { - set({ isLoading: false }); - } - }, - - confirmItem: async (storeId, itemId) => { - set({ isLoading: true, error: null }); - try { - await feedbackService.confirmItem(storeId, itemId); - await get().fetchCorrectionHistory(storeId, itemId); - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error al confirmar item' }); - throw error; - } finally { - set({ isLoading: false }); - } - }, - - fetchCorrectionHistory: async (storeId, itemId) => { - set({ isLoading: true, error: null }); - try { - const { corrections } = await feedbackService.getCorrectionHistory( - storeId, - itemId, - ); - set({ correctionHistory: corrections }); - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error al cargar historial' }); - } finally { - set({ isLoading: false }); - } - }, - - submitProduct: async (data) => { - set({ isLoading: true, error: null }); - try { - await feedbackService.submitProduct(data); - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error al enviar producto' }); - throw error; - } finally { - set({ isLoading: false }); - } - }, - - searchProducts: async (query) => { - set({ isLoading: true, error: null }); - try { - const { products } = await feedbackService.searchProducts(query); - set({ searchResults: products }); - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error en busqueda' }); - } finally { - set({ isLoading: false }); - } - }, - - clearError: () => set({ error: null }), - - reset: () => - set({ - correctionHistory: [], - searchResults: [], - isLoading: false, - error: null, - }), -})); diff --git a/src/stores/inventory.store.ts b/src/stores/inventory.store.ts deleted file mode 100644 index 51cbb86..0000000 --- a/src/stores/inventory.store.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { inventoryService, InventoryItem } from '@services/api/inventory.service'; - -interface InventoryState { - items: InventoryItem[]; - total: number; - page: number; - hasMore: boolean; - isLoading: boolean; - error: string | null; - selectedStoreId: string | null; - searchQuery: string; - selectedCategory: string | null; - lastFetched: number | null; - - // Actions - fetchItems: (storeId: string, refresh?: boolean) => Promise; - fetchInventory: (storeId: string) => Promise; - updateItem: (itemId: string, data: Partial) => Promise; - deleteItem: (itemId: string) => Promise; - setSelectedStore: (storeId: string) => void; - setSearchQuery: (query: string) => void; - setSelectedCategory: (category: string | null) => void; - clearError: () => void; -} - -const MAX_CACHED_ITEMS = 100; - -export const useInventoryStore = create()( - persist( - (set, get) => ({ - items: [], - total: 0, - page: 1, - hasMore: false, - isLoading: false, - error: null, - selectedStoreId: null, - searchQuery: '', - selectedCategory: null, - lastFetched: null, - - fetchItems: async (storeId: string, refresh = false) => { - const state = get(); - if (state.isLoading && !refresh) return; - - const page = refresh ? 1 : state.page; - - set({ isLoading: true, error: null, selectedStoreId: storeId }); - - try { - const response = await inventoryService.getInventory(storeId, page, 50); - const newItems = refresh ? response.items : [...state.items, ...response.items]; - - set({ - items: newItems.slice(0, MAX_CACHED_ITEMS), - total: response.total, - page: page + 1, - hasMore: response.hasMore, - isLoading: false, - lastFetched: Date.now(), - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al cargar inventario'; - set({ error: message, isLoading: false }); - } - }, - - fetchInventory: async (storeId: string) => { - return get().fetchItems(storeId, true); - }, - - updateItem: async (itemId: string, data: Partial) => { - const { selectedStoreId } = get(); - if (!selectedStoreId) return; - - set({ isLoading: true, error: null }); - - try { - await inventoryService.updateItem(selectedStoreId, itemId, data); - set((state) => ({ - items: state.items.map((item) => - item.id === itemId ? { ...item, ...data, isManuallyEdited: true } : item - ), - isLoading: false, - })); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al actualizar producto'; - set({ error: message, isLoading: false }); - throw error; - } - }, - - deleteItem: async (itemId: string) => { - const { selectedStoreId } = get(); - if (!selectedStoreId) return; - - set({ isLoading: true, error: null }); - - try { - await inventoryService.deleteItem(selectedStoreId, itemId); - set((state) => ({ - items: state.items.filter((item) => item.id !== itemId), - total: state.total - 1, - isLoading: false, - })); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al eliminar producto'; - set({ error: message, isLoading: false }); - throw error; - } - }, - - setSelectedStore: (storeId: string) => { - set({ selectedStoreId: storeId }); - }, - - setSearchQuery: (query: string) => { - set({ searchQuery: query }); - }, - - setSelectedCategory: (category: string | null) => { - set({ selectedCategory: category }); - }, - - clearError: () => set({ error: null }), - }), - { - name: 'miinventario-inventory', - storage: createJSONStorage(() => AsyncStorage), - partialize: (state) => ({ - items: state.items.slice(0, MAX_CACHED_ITEMS), - total: state.total, - selectedStoreId: state.selectedStoreId, - lastFetched: state.lastFetched, - }), - } - ) -); diff --git a/src/stores/notifications.store.ts b/src/stores/notifications.store.ts deleted file mode 100644 index 2746cc0..0000000 --- a/src/stores/notifications.store.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { - notificationsService, - Notification, -} from '@services/api/notifications.service'; - -interface NotificationsState { - notifications: Notification[]; - unreadCount: number; - total: number; - page: number; - hasMore: boolean; - isLoading: boolean; - error: string | null; - lastFetched: number | null; - // Actions - fetchNotifications: (refresh?: boolean) => Promise; - fetchUnreadCount: () => Promise; - markAsRead: (notificationId: string) => Promise; - markAllAsRead: () => Promise; - registerFcmToken: (token: string) => Promise; - clearError: () => void; -} - -const MAX_CACHED_NOTIFICATIONS = 50; - -export const useNotificationsStore = create()( - persist( - (set, get) => ({ - notifications: [], - unreadCount: 0, - total: 0, - page: 1, - hasMore: false, - isLoading: false, - error: null, - lastFetched: null, - - fetchNotifications: async (refresh = false) => { - const state = get(); - if (state.isLoading) return; - - const page = refresh ? 1 : state.page; - - set({ isLoading: true, error: null }); - - try { - const response = await notificationsService.getNotifications(page, 20); - const newNotifications = refresh - ? response.notifications - : [...state.notifications, ...response.notifications]; - - set({ - notifications: newNotifications.slice(0, MAX_CACHED_NOTIFICATIONS), - total: response.total, - page: response.page + 1, - hasMore: response.hasMore, - isLoading: false, - lastFetched: Date.now(), - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al cargar notificaciones'; - set({ error: message, isLoading: false }); - } - }, - - fetchUnreadCount: async () => { - try { - const response = await notificationsService.getUnreadCount(); - set({ unreadCount: response.count }); - } catch { - // Silently fail for badge count - } - }, - - markAsRead: async (notificationId: string) => { - try { - await notificationsService.markAsRead(notificationId); - const state = get(); - set({ - notifications: state.notifications.map((n) => - n.id === notificationId ? { ...n, isRead: true } : n - ), - unreadCount: Math.max(0, state.unreadCount - 1), - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al marcar notificacion'; - set({ error: message }); - } - }, - - markAllAsRead: async () => { - try { - await notificationsService.markAllAsRead(); - const state = get(); - set({ - notifications: state.notifications.map((n) => ({ ...n, isRead: true })), - unreadCount: 0, - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al marcar notificaciones'; - set({ error: message }); - } - }, - - registerFcmToken: async (token: string) => { - try { - await notificationsService.registerFcmToken(token); - } catch { - // Silently fail for token registration - } - }, - - clearError: () => set({ error: null }), - }), - { - name: 'miinventario-notifications', - storage: createJSONStorage(() => AsyncStorage), - partialize: (state) => ({ - notifications: state.notifications.slice(0, MAX_CACHED_NOTIFICATIONS), - unreadCount: state.unreadCount, - total: state.total, - lastFetched: state.lastFetched, - }), - } - ) -); diff --git a/src/stores/payments.store.ts b/src/stores/payments.store.ts deleted file mode 100644 index 89267d8..0000000 --- a/src/stores/payments.store.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { create } from 'zustand'; -import { - paymentsService, - CreditPackage, - Payment, - CreatePaymentRequest, - PaymentResponse, -} from '@services/api/payments.service'; - -interface PaymentsState { - packages: CreditPackage[]; - payments: Payment[]; - currentPayment: PaymentResponse | null; - total: number; - page: number; - hasMore: boolean; - isLoading: boolean; - isProcessing: boolean; - error: string | null; - // Actions - fetchPackages: () => Promise; - fetchPayments: (refresh?: boolean) => Promise; - createPayment: (data: CreatePaymentRequest) => Promise; - getPaymentById: (paymentId: string) => Promise; - clearCurrentPayment: () => void; - clearError: () => void; -} - -export const usePaymentsStore = create((set, get) => ({ - packages: [], - payments: [], - currentPayment: null, - total: 0, - page: 1, - hasMore: false, - isLoading: false, - isProcessing: false, - error: null, - - fetchPackages: async () => { - set({ isLoading: true, error: null }); - - try { - const packages = await paymentsService.getPackages(); - set({ packages, isLoading: false }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al cargar paquetes'; - set({ error: message, isLoading: false }); - } - }, - - fetchPayments: async (refresh = false) => { - const state = get(); - if (state.isLoading) return; - - const page = refresh ? 1 : state.page; - - set({ isLoading: true, error: null }); - - try { - const response = await paymentsService.getPaymentHistory(page, 20); - set({ - payments: refresh - ? response.payments - : [...state.payments, ...response.payments], - total: response.total, - page: response.page + 1, - hasMore: response.hasMore, - isLoading: false, - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al cargar pagos'; - set({ error: message, isLoading: false }); - } - }, - - createPayment: async (data: CreatePaymentRequest) => { - set({ isProcessing: true, error: null }); - - try { - const response = await paymentsService.createPayment(data); - set({ currentPayment: response, isProcessing: false }); - return response; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al procesar pago'; - set({ error: message, isProcessing: false }); - return null; - } - }, - - getPaymentById: async (paymentId: string) => { - try { - const payment = await paymentsService.getPaymentById(paymentId); - return payment; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al obtener pago'; - set({ error: message }); - return null; - } - }, - - clearCurrentPayment: () => set({ currentPayment: null }), - - clearError: () => set({ error: null }), -})); diff --git a/src/stores/referrals.store.ts b/src/stores/referrals.store.ts deleted file mode 100644 index 2638304..0000000 --- a/src/stores/referrals.store.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { create } from 'zustand'; -import { - referralsService, - ReferralStats, - Referral, -} from '@services/api/referrals.service'; - -interface ReferralsState { - stats: ReferralStats | null; - referrals: Referral[]; - total: number; - page: number; - hasMore: boolean; - isLoading: boolean; - isValidating: boolean; - error: string | null; - // Actions - fetchStats: () => Promise; - fetchReferrals: (refresh?: boolean) => Promise; - validateCode: (code: string) => Promise<{ valid: boolean; referrerName?: string }>; - applyCode: (code: string) => Promise<{ success: boolean; message: string }>; - clearError: () => void; -} - -export const useReferralsStore = create((set, get) => ({ - stats: null, - referrals: [], - total: 0, - page: 1, - hasMore: false, - isLoading: false, - isValidating: false, - error: null, - - fetchStats: async () => { - set({ isLoading: true, error: null }); - - try { - const stats = await referralsService.getStats(); - set({ stats, isLoading: false }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al cargar estadisticas'; - set({ error: message, isLoading: false }); - } - }, - - fetchReferrals: async (refresh = false) => { - const state = get(); - if (state.isLoading) return; - - const page = refresh ? 1 : state.page; - - set({ isLoading: true, error: null }); - - try { - const response = await referralsService.getReferrals(page, 20); - set({ - referrals: refresh - ? response.referrals - : [...state.referrals, ...response.referrals], - total: response.total, - page: response.page + 1, - hasMore: response.hasMore, - isLoading: false, - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al cargar referidos'; - set({ error: message, isLoading: false }); - } - }, - - validateCode: async (code: string) => { - set({ isValidating: true, error: null }); - - try { - const result = await referralsService.validateCode(code); - set({ isValidating: false }); - return result; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al validar codigo'; - set({ error: message, isValidating: false }); - return { valid: false }; - } - }, - - applyCode: async (code: string) => { - set({ isLoading: true, error: null }); - - try { - const result = await referralsService.applyCode(code); - set({ isLoading: false }); - return result; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al aplicar codigo'; - set({ error: message, isLoading: false }); - return { success: false, message }; - } - }, - - clearError: () => set({ error: null }), -})); diff --git a/src/stores/stores.store.ts b/src/stores/stores.store.ts deleted file mode 100644 index 6383985..0000000 --- a/src/stores/stores.store.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { - storesService, - Store, - CreateStoreRequest, - UpdateStoreRequest, -} from '@services/api/stores.service'; - -interface StoresState { - stores: Store[]; - currentStore: Store | null; - total: number; - page: number; - hasMore: boolean; - isLoading: boolean; - error: string | null; - lastFetched: number | null; - // Actions - fetchStores: (refresh?: boolean) => Promise; - selectStore: (store: Store) => void; - getStoreById: (storeId: string) => Promise; - createStore: (data: CreateStoreRequest) => Promise; - updateStore: (storeId: string, data: UpdateStoreRequest) => Promise; - deleteStore: (storeId: string) => Promise; - clearError: () => void; -} - -export const useStoresStore = create()( - persist( - (set, get) => ({ - stores: [], - currentStore: null, - total: 0, - page: 1, - hasMore: false, - isLoading: false, - error: null, - lastFetched: null, - - fetchStores: async (refresh = false) => { - const state = get(); - if (state.isLoading) return; - - const page = refresh ? 1 : state.page; - - set({ isLoading: true, error: null }); - - try { - const response = await storesService.getStores(page, 20); - const newStores = refresh - ? response.stores - : [...state.stores, ...response.stores]; - - set({ - stores: newStores, - total: response.total, - page: response.page + 1, - hasMore: response.hasMore, - isLoading: false, - lastFetched: Date.now(), - // Auto-select first store if none selected - currentStore: state.currentStore || newStores[0] || null, - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al cargar tiendas'; - set({ error: message, isLoading: false }); - } - }, - - selectStore: (store: Store) => { - set({ currentStore: store }); - }, - - getStoreById: async (storeId: string) => { - try { - const store = await storesService.getStoreById(storeId); - return store; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al obtener tienda'; - set({ error: message }); - return null; - } - }, - - createStore: async (data: CreateStoreRequest) => { - set({ isLoading: true, error: null }); - - try { - const store = await storesService.createStore(data); - const state = get(); - set({ - stores: [store, ...state.stores], - currentStore: store, - isLoading: false, - }); - return store; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al crear tienda'; - set({ error: message, isLoading: false }); - return null; - } - }, - - updateStore: async (storeId: string, data: UpdateStoreRequest) => { - set({ isLoading: true, error: null }); - - try { - const updatedStore = await storesService.updateStore(storeId, data); - const state = get(); - set({ - stores: state.stores.map((s) => - s.id === storeId ? updatedStore : s - ), - currentStore: - state.currentStore?.id === storeId ? updatedStore : state.currentStore, - isLoading: false, - }); - return updatedStore; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al actualizar tienda'; - set({ error: message, isLoading: false }); - return null; - } - }, - - deleteStore: async (storeId: string) => { - set({ isLoading: true, error: null }); - - try { - await storesService.deleteStore(storeId); - const state = get(); - const newStores = state.stores.filter((s) => s.id !== storeId); - set({ - stores: newStores, - currentStore: - state.currentStore?.id === storeId - ? newStores[0] || null - : state.currentStore, - isLoading: false, - }); - return true; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Error al eliminar tienda'; - set({ error: message, isLoading: false }); - return false; - } - }, - - clearError: () => set({ error: null }), - }), - { - name: 'miinventario-stores', - storage: createJSONStorage(() => AsyncStorage), - partialize: (state) => ({ - stores: state.stores, - currentStore: state.currentStore, - total: state.total, - lastFetched: state.lastFetched, - }), - } - ) -); diff --git a/src/stores/validations.store.ts b/src/stores/validations.store.ts deleted file mode 100644 index 8c68a80..0000000 --- a/src/stores/validations.store.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { create } from 'zustand'; -import validationsService, { - ValidationRequest, - ValidationItem, - ValidationItemResponse, -} from '../services/api/validations.service'; - -interface ValidationsState { - pendingRequest: ValidationRequest | null; - items: ValidationItem[]; - responses: ValidationItemResponse[]; - currentItemIndex: number; - isLoading: boolean; - error: string | null; - creditsRewarded: number | null; - - // Actions - checkForValidation: (videoId: string) => Promise; - fetchValidationItems: (requestId: string) => Promise; - addResponse: (response: ValidationItemResponse) => void; - nextItem: () => void; - previousItem: () => void; - submitValidation: () => Promise; - skipValidation: () => Promise; - clearError: () => void; - reset: () => void; -} - -export const useValidationsStore = create((set, get) => ({ - pendingRequest: null, - items: [], - responses: [], - currentItemIndex: 0, - isLoading: false, - error: null, - creditsRewarded: null, - - checkForValidation: async (videoId) => { - set({ isLoading: true, error: null }); - try { - const result = await validationsService.check(videoId); - if (result.validationRequired && result.requestId) { - await get().fetchValidationItems(result.requestId); - return true; - } - return false; - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error al verificar validacion' }); - return false; - } finally { - set({ isLoading: false }); - } - }, - - fetchValidationItems: async (requestId) => { - set({ isLoading: true, error: null }); - try { - const { request, items } = await validationsService.getItems(requestId); - set({ - pendingRequest: request, - items, - responses: [], - currentItemIndex: 0, - }); - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error al cargar items' }); - } finally { - set({ isLoading: false }); - } - }, - - addResponse: (response) => { - const { responses } = get(); - const existingIndex = responses.findIndex( - (r) => r.inventoryItemId === response.inventoryItemId, - ); - if (existingIndex >= 0) { - const updated = [...responses]; - updated[existingIndex] = response; - set({ responses: updated }); - } else { - set({ responses: [...responses, response] }); - } - }, - - nextItem: () => { - const { currentItemIndex, items } = get(); - if (currentItemIndex < items.length - 1) { - set({ currentItemIndex: currentItemIndex + 1 }); - } - }, - - previousItem: () => { - const { currentItemIndex } = get(); - if (currentItemIndex > 0) { - set({ currentItemIndex: currentItemIndex - 1 }); - } - }, - - submitValidation: async () => { - const { pendingRequest, responses } = get(); - if (!pendingRequest) { - set({ error: 'No hay validacion pendiente' }); - return; - } - - set({ isLoading: true, error: null }); - try { - const result = await validationsService.submit(pendingRequest.id, { - responses, - }); - set({ - creditsRewarded: result.creditsRewarded, - pendingRequest: null, - items: [], - responses: [], - currentItemIndex: 0, - }); - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error al enviar validacion' }); - } finally { - set({ isLoading: false }); - } - }, - - skipValidation: async () => { - const { pendingRequest } = get(); - if (!pendingRequest) return; - - set({ isLoading: true, error: null }); - try { - await validationsService.skip(pendingRequest.id); - set({ - pendingRequest: null, - items: [], - responses: [], - currentItemIndex: 0, - }); - } catch (error: any) { - set({ error: error.response?.data?.message || 'Error al omitir validacion' }); - } finally { - set({ isLoading: false }); - } - }, - - clearError: () => set({ error: null }), - - reset: () => - set({ - pendingRequest: null, - items: [], - responses: [], - currentItemIndex: 0, - isLoading: false, - error: null, - creditsRewarded: null, - }), -})); diff --git a/src/theme/ThemeContext.tsx b/src/theme/ThemeContext.tsx deleted file mode 100644 index 1450f99..0000000 --- a/src/theme/ThemeContext.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { createContext, useContext, useMemo } from 'react'; -import { useColorScheme } from 'react-native'; - -export interface ThemeColors { - primary: string; - primaryLight: string; - background: string; - card: string; - text: string; - textSecondary: string; - border: string; - error: string; - success: string; - warning: string; -} - -export interface Theme { - colors: ThemeColors; - isDark: boolean; -} - -const lightColors: ThemeColors = { - primary: '#2563eb', - primaryLight: '#f0f9ff', - background: '#f5f5f5', - card: '#ffffff', - text: '#1a1a1a', - textSecondary: '#666666', - border: '#e5e5e5', - error: '#ef4444', - success: '#22c55e', - warning: '#f59e0b', -}; - -const darkColors: ThemeColors = { - primary: '#3b82f6', - primaryLight: '#1e3a5f', - background: '#0f0f0f', - card: '#1a1a1a', - text: '#ffffff', - textSecondary: '#a3a3a3', - border: '#2d2d2d', - error: '#f87171', - success: '#4ade80', - warning: '#fbbf24', -}; - -const ThemeContext = createContext({ - colors: lightColors, - isDark: false, -}); - -export function ThemeProvider({ children }: { children: React.ReactNode }) { - const colorScheme = useColorScheme(); - const isDark = colorScheme === 'dark'; - - const theme = useMemo(() => ({ - colors: isDark ? darkColors : lightColors, - isDark, - }), [isDark]); - - return ( - - {children} - - ); -} - -export function useTheme(): Theme { - return useContext(ThemeContext); -} - -export function useColors(): ThemeColors { - const { colors } = useTheme(); - return colors; -} diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 4682686..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,234 +0,0 @@ -// User types -export interface User { - id: string; - phone: string; - name: string; - email?: string; - businessName?: string; - location?: string; - giro?: string; - fcmToken?: string; - createdAt: string; -} - -// Store types -export interface Store { - id: string; - name: string; - address?: string; - city?: string; - state?: string; - zipCode?: string; - giro?: string; - ownerId: string; - isActive: boolean; - createdAt: string; - updatedAt: string; -} - -// Inventory types -export interface InventoryItem { - id: string; - storeId: string; - name: string; - quantity: number; - category?: string; - barcode?: string; - price?: number; - imageUrl?: string; - detectionConfidence?: number; - isManuallyEdited: boolean; - lastDetectedAt?: string; - createdAt: string; - updatedAt: string; -} - -// Video types -export type VideoStatus = - | 'PENDING' - | 'UPLOADING' - | 'UPLOADED' - | 'PROCESSING' - | 'COMPLETED' - | 'FAILED'; - -export interface Video { - id: string; - storeId: string; - userId: string; - fileName: string; - originalFileName: string; - mimeType: string; - fileSize: number; - status: VideoStatus; - s3Key?: string; - processingStartedAt?: string; - processingCompletedAt?: string; - itemsDetected?: number; - creditsConsumed?: number; - errorMessage?: string; - createdAt: string; - updatedAt: string; -} - -// Credit types -export type TransactionType = - | 'PURCHASE' - | 'CONSUMPTION' - | 'REFERRAL_BONUS' - | 'PROMO' - | 'REFUND'; - -export interface CreditBalance { - id: string; - userId: string; - balance: number; - totalPurchased: number; - totalConsumed: number; - totalFromReferrals: number; - updatedAt: string; -} - -export interface CreditTransaction { - id: string; - userId: string; - type: TransactionType; - amount: number; - balanceAfter: number; - description: string; - referenceId?: string; - referenceType?: string; - createdAt: string; -} - -export interface CreditPackage { - id: string; - name: string; - credits: number; - priceMXN: number; - discount?: number; - isPopular: boolean; - isActive: boolean; - sortOrder: number; -} - -// Payment types -export type PaymentStatus = - | 'PENDING' - | 'PROCESSING' - | 'COMPLETED' - | 'FAILED' - | 'REFUNDED'; - -export type PaymentMethod = 'CARD' | 'OXXO' | '7ELEVEN'; - -export interface Payment { - id: string; - userId: string; - packageId: string; - amountMXN: number; - creditsGranted: number; - method: PaymentMethod; - status: PaymentStatus; - stripePaymentIntentId?: string; - stripeCustomerId?: string; - voucherCode?: string; - voucherUrl?: string; - expiresAt?: string; - completedAt?: string; - createdAt: string; - updatedAt: string; -} - -// Referral types -export type ReferralStatus = 'PENDING' | 'REGISTERED' | 'QUALIFIED' | 'REWARDED'; - -export interface Referral { - id: string; - referrerId: string; - referredId?: string; - referralCode: string; - status: ReferralStatus; - referrerBonusCredits: number; - referredBonusCredits: number; - registeredAt?: string; - qualifiedAt?: string; - rewardedAt?: string; - createdAt: string; - updatedAt: string; - referred?: { - id: string; - name: string; - createdAt: string; - }; -} - -export interface ReferralStats { - referralCode: string; - totalReferrals: number; - completedReferrals: number; - pendingReferrals: number; - totalCreditsEarned: number; -} - -// Notification types -export type NotificationType = - | 'VIDEO_PROCESSING_COMPLETE' - | 'VIDEO_PROCESSING_FAILED' - | 'LOW_CREDITS' - | 'PAYMENT_COMPLETE' - | 'PAYMENT_FAILED' - | 'REFERRAL_BONUS' - | 'SYSTEM'; - -export interface Notification { - id: string; - userId: string; - type: NotificationType; - title: string; - body: string; - data?: Record; - isRead: boolean; - isPushSent: boolean; - createdAt: string; -} - -// API response types -export interface ApiError { - message: string; - code?: string; - statusCode: number; -} - -export interface PaginatedResponse { - data: T[]; - total: number; - page: number; - limit: number; - hasMore: boolean; -} - -// Auth types -export interface AuthTokens { - accessToken: string; - refreshToken: string; - expiresIn: number; -} - -export interface LoginResponse { - user: User; - tokens: AuthTokens; -} - -export interface RegisterRequest { - phone: string; - name: string; - password: string; - referralCode?: string; -} - -export interface VerifyOtpRequest { - phone: string; - code: string; - purpose: 'REGISTRATION' | 'LOGIN' | 'PASSWORD_RESET'; -} diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts deleted file mode 100644 index 05ccb93..0000000 --- a/src/utils/formatters.ts +++ /dev/null @@ -1,58 +0,0 @@ -export function formatPhoneNumber(phone: string): string { - // Format Mexican phone number: +52 55 1234 5678 - const cleaned = phone.replace(/\D/g, ''); - - if (cleaned.length === 10) { - return `${cleaned.slice(0, 2)} ${cleaned.slice(2, 6)} ${cleaned.slice(6)}`; - } - - if (cleaned.length === 12 && cleaned.startsWith('52')) { - return `+52 ${cleaned.slice(2, 4)} ${cleaned.slice(4, 8)} ${cleaned.slice(8)}`; - } - - return phone; -} - -export function formatCurrency(amount: number, currency = 'MXN'): string { - return new Intl.NumberFormat('es-MX', { - style: 'currency', - currency, - minimumFractionDigits: 0, - maximumFractionDigits: 2, - }).format(amount); -} - -export function formatCredits(credits: number): string { - if (credits >= 1000000) { - return `${(credits / 1000000).toFixed(1)}M`; - } - if (credits >= 1000) { - return `${(credits / 1000).toFixed(1)}K`; - } - return credits.toString(); -} - -export function formatDate(date: string | Date): string { - const d = typeof date === 'string' ? new Date(date) : date; - return new Intl.DateTimeFormat('es-MX', { - day: 'numeric', - month: 'short', - year: 'numeric', - }).format(d); -} - -export function formatRelativeTime(date: string | Date): string { - const d = typeof date === 'string' ? new Date(date) : date; - const now = new Date(); - const diffMs = now.getTime() - d.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMins < 1) return 'Ahora'; - if (diffMins < 60) return `Hace ${diffMins} min`; - if (diffHours < 24) return `Hace ${diffHours}h`; - if (diffDays < 7) return `Hace ${diffDays}d`; - - return formatDate(d); -} diff --git a/src/utils/validators.ts b/src/utils/validators.ts deleted file mode 100644 index 20a059f..0000000 --- a/src/utils/validators.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { z } from 'zod'; - -export const phoneSchema = z - .string() - .min(10, 'El telefono debe tener al menos 10 digitos') - .max(15, 'El telefono es muy largo') - .regex(/^[0-9+]+$/, 'Solo numeros permitidos'); - -export const passwordSchema = z - .string() - .min(6, 'La contrasena debe tener al menos 6 caracteres') - .max(50, 'La contrasena es muy larga'); - -export const nameSchema = z - .string() - .min(2, 'El nombre debe tener al menos 2 caracteres') - .max(100, 'El nombre es muy largo'); - -export const otpSchema = z - .string() - .length(6, 'El codigo debe tener 6 digitos') - .regex(/^[0-9]+$/, 'Solo numeros permitidos'); - -export const loginSchema = z.object({ - phone: phoneSchema, - password: passwordSchema, -}); - -export const registerSchema = z.object({ - phone: phoneSchema, - name: nameSchema, -}); - -export const verifyOtpSchema = z.object({ - phone: phoneSchema, - otp: otpSchema, - password: passwordSchema, -}); - -export type LoginFormData = z.infer; -export type RegisterFormData = z.infer; -export type VerifyOtpFormData = z.infer; diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 2c51631..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "expo/tsconfig.base", - "compilerOptions": { - "strict": true, - "paths": { - "@/*": ["./src/*"], - "@screens/*": ["./src/screens/*"], - "@components/*": ["./src/components/*"], - "@hooks/*": ["./src/hooks/*"], - "@stores/*": ["./src/stores/*"], - "@services/*": ["./src/services/*"], - "@utils/*": ["./src/utils/*"], - "@types/*": ["./src/types/*"] - } - }, - "include": [ - "**/*.ts", - "**/*.tsx", - ".expo/types/**/*.ts", - "expo-env.d.ts" - ] -}