From 62a6a96bb8e61cee883f6b2537594f58280427a8 Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Tue, 20 Jan 2026 04:38:34 -0600 Subject: [PATCH] [MIINVENTARIO-MOBILE] chore: Cleanup config files Co-Authored-By: Claude Opus 4.5 --- .env.example | 5 - .eslintrc.js | 47 -- .prettierrc | 11 - app.json | 55 -- babel.config.js | 25 - jest.config.js | 37 -- jest.setup.js | 71 --- package.json | 63 -- src/__mocks__/apiClient.mock.ts | 49 -- src/app/(auth)/_layout.tsx | 16 - src/app/(auth)/login.tsx | 134 ---- src/app/(auth)/register.tsx | 136 ---- src/app/(auth)/verify-otp.tsx | 129 ---- src/app/(tabs)/_layout.tsx | 48 -- src/app/(tabs)/index.tsx | 542 ---------------- src/app/(tabs)/inventory.tsx | 554 ---------------- src/app/(tabs)/profile.tsx | 531 --------------- src/app/(tabs)/scan.tsx | 467 -------------- src/app/_layout.tsx | 50 -- src/app/credits/_layout.tsx | 23 - src/app/credits/buy.tsx | 434 ------------- src/app/credits/history.tsx | 221 ------- src/app/help/index.tsx | 292 --------- src/app/index.tsx | 12 - src/app/inventory/[id].tsx | 603 ------------------ src/app/inventory/_layout.tsx | 23 - src/app/inventory/export.tsx | 492 -------------- src/app/legal/privacy.tsx | 197 ------ src/app/legal/terms.tsx | 164 ----- src/app/notifications/_layout.tsx | 19 - src/app/notifications/index.tsx | 312 --------- src/app/payments/methods.tsx | 288 --------- src/app/profile/edit.tsx | 316 --------- src/app/referrals/_layout.tsx | 19 - src/app/referrals/index.tsx | 460 ------------- src/app/reports/_layout.tsx | 31 - src/app/reports/categories.tsx | 479 -------------- src/app/reports/index.tsx | 150 ----- src/app/reports/movements.tsx | 371 ----------- src/app/reports/valuation.tsx | 381 ----------- src/app/stores/[id].tsx | 264 -------- src/app/stores/_layout.tsx | 27 - src/app/stores/index.tsx | 301 --------- src/app/stores/new.tsx | 221 ------- src/app/support/index.tsx | 317 --------- src/app/validation/_layout.tsx | 27 - src/app/validation/complete.tsx | 165 ----- src/app/validation/items.tsx | 301 --------- src/components/feedback/ConfirmItemButton.tsx | 57 -- .../feedback/CorrectQuantityModal.tsx | 203 ------ src/components/feedback/CorrectSkuModal.tsx | 220 ------- .../feedback/CorrectionHistoryCard.tsx | 147 ----- .../skeletons/CreditCardSkeleton.tsx | 137 ---- .../skeletons/InventoryItemSkeleton.tsx | 86 --- .../skeletons/NotificationSkeleton.tsx | 75 --- .../skeletons/StoreCardSkeleton.tsx | 80 --- src/components/ui/AnimatedList.tsx | 154 ----- src/components/ui/OfflineBanner.tsx | 114 ---- src/components/ui/Skeleton.tsx | 215 ------- .../validation/ValidationItemCard.tsx | 317 --------- .../validation/ValidationProgressBar.tsx | 63 -- .../validation/ValidationPromptModal.tsx | 184 ------ src/hooks/useAnimations.ts | 186 ------ src/hooks/useNetworkStatus.ts | 73 --- .../api/__tests__/auth.service.spec.ts | 112 ---- .../api/__tests__/inventory.service.spec.ts | 119 ---- .../api/__tests__/reports.service.spec.ts | 175 ----- src/services/api/auth.service.ts | 62 -- src/services/api/client.ts | 58 -- src/services/api/credits.service.ts | 61 -- src/services/api/exports.service.ts | 143 ----- src/services/api/feedback.service.ts | 115 ---- src/services/api/inventory.service.ts | 61 -- src/services/api/notifications.service.ts | 66 -- src/services/api/payments.service.ts | 76 --- src/services/api/referrals.service.ts | 75 --- src/services/api/reports.service.ts | 171 ----- src/services/api/stores.service.ts | 70 -- src/services/api/users.service.ts | 35 - src/services/api/validations.service.ts | 70 -- src/services/api/videos.service.ts | 87 --- src/stores/__tests__/auth.store.spec.ts | 198 ------ src/stores/__tests__/credits.store.spec.ts | 98 --- src/stores/__tests__/feedback.store.spec.ts | 162 ----- src/stores/__tests__/inventory.store.spec.ts | 200 ------ .../__tests__/notifications.store.spec.ts | 100 --- src/stores/__tests__/payments.store.spec.ts | 152 ----- src/stores/__tests__/referrals.store.spec.ts | 95 --- src/stores/__tests__/stores.store.spec.ts | 149 ----- .../__tests__/validations.store.spec.ts | 146 ----- src/stores/auth.store.ts | 137 ---- src/stores/credits.store.ts | 138 ---- src/stores/feedback.store.ts | 129 ---- src/stores/inventory.store.ts | 141 ---- src/stores/notifications.store.ts | 129 ---- src/stores/payments.store.ts | 105 --- src/stores/referrals.store.ts | 101 --- src/stores/stores.store.ts | 164 ----- src/stores/validations.store.ts | 158 ----- src/theme/ThemeContext.tsx | 76 --- src/types/index.ts | 234 ------- src/utils/formatters.ts | 58 -- src/utils/validators.ts | 42 -- tsconfig.json | 22 - 104 files changed, 16681 deletions(-) delete mode 100644 .env.example delete mode 100644 .eslintrc.js delete mode 100644 .prettierrc delete mode 100644 app.json delete mode 100644 babel.config.js delete mode 100644 jest.config.js delete mode 100644 jest.setup.js delete mode 100644 package.json delete mode 100644 src/__mocks__/apiClient.mock.ts delete mode 100644 src/app/(auth)/_layout.tsx delete mode 100644 src/app/(auth)/login.tsx delete mode 100644 src/app/(auth)/register.tsx delete mode 100644 src/app/(auth)/verify-otp.tsx delete mode 100644 src/app/(tabs)/_layout.tsx delete mode 100644 src/app/(tabs)/index.tsx delete mode 100644 src/app/(tabs)/inventory.tsx delete mode 100644 src/app/(tabs)/profile.tsx delete mode 100644 src/app/(tabs)/scan.tsx delete mode 100644 src/app/_layout.tsx delete mode 100644 src/app/credits/_layout.tsx delete mode 100644 src/app/credits/buy.tsx delete mode 100644 src/app/credits/history.tsx delete mode 100644 src/app/help/index.tsx delete mode 100644 src/app/index.tsx delete mode 100644 src/app/inventory/[id].tsx delete mode 100644 src/app/inventory/_layout.tsx delete mode 100644 src/app/inventory/export.tsx delete mode 100644 src/app/legal/privacy.tsx delete mode 100644 src/app/legal/terms.tsx delete mode 100644 src/app/notifications/_layout.tsx delete mode 100644 src/app/notifications/index.tsx delete mode 100644 src/app/payments/methods.tsx delete mode 100644 src/app/profile/edit.tsx delete mode 100644 src/app/referrals/_layout.tsx delete mode 100644 src/app/referrals/index.tsx delete mode 100644 src/app/reports/_layout.tsx delete mode 100644 src/app/reports/categories.tsx delete mode 100644 src/app/reports/index.tsx delete mode 100644 src/app/reports/movements.tsx delete mode 100644 src/app/reports/valuation.tsx delete mode 100644 src/app/stores/[id].tsx delete mode 100644 src/app/stores/_layout.tsx delete mode 100644 src/app/stores/index.tsx delete mode 100644 src/app/stores/new.tsx delete mode 100644 src/app/support/index.tsx delete mode 100644 src/app/validation/_layout.tsx delete mode 100644 src/app/validation/complete.tsx delete mode 100644 src/app/validation/items.tsx delete mode 100644 src/components/feedback/ConfirmItemButton.tsx delete mode 100644 src/components/feedback/CorrectQuantityModal.tsx delete mode 100644 src/components/feedback/CorrectSkuModal.tsx delete mode 100644 src/components/feedback/CorrectionHistoryCard.tsx delete mode 100644 src/components/skeletons/CreditCardSkeleton.tsx delete mode 100644 src/components/skeletons/InventoryItemSkeleton.tsx delete mode 100644 src/components/skeletons/NotificationSkeleton.tsx delete mode 100644 src/components/skeletons/StoreCardSkeleton.tsx delete mode 100644 src/components/ui/AnimatedList.tsx delete mode 100644 src/components/ui/OfflineBanner.tsx delete mode 100644 src/components/ui/Skeleton.tsx delete mode 100644 src/components/validation/ValidationItemCard.tsx delete mode 100644 src/components/validation/ValidationProgressBar.tsx delete mode 100644 src/components/validation/ValidationPromptModal.tsx delete mode 100644 src/hooks/useAnimations.ts delete mode 100644 src/hooks/useNetworkStatus.ts delete mode 100644 src/services/api/__tests__/auth.service.spec.ts delete mode 100644 src/services/api/__tests__/inventory.service.spec.ts delete mode 100644 src/services/api/__tests__/reports.service.spec.ts delete mode 100644 src/services/api/auth.service.ts delete mode 100644 src/services/api/client.ts delete mode 100644 src/services/api/credits.service.ts delete mode 100644 src/services/api/exports.service.ts delete mode 100644 src/services/api/feedback.service.ts delete mode 100644 src/services/api/inventory.service.ts delete mode 100644 src/services/api/notifications.service.ts delete mode 100644 src/services/api/payments.service.ts delete mode 100644 src/services/api/referrals.service.ts delete mode 100644 src/services/api/reports.service.ts delete mode 100644 src/services/api/stores.service.ts delete mode 100644 src/services/api/users.service.ts delete mode 100644 src/services/api/validations.service.ts delete mode 100644 src/services/api/videos.service.ts delete mode 100644 src/stores/__tests__/auth.store.spec.ts delete mode 100644 src/stores/__tests__/credits.store.spec.ts delete mode 100644 src/stores/__tests__/feedback.store.spec.ts delete mode 100644 src/stores/__tests__/inventory.store.spec.ts delete mode 100644 src/stores/__tests__/notifications.store.spec.ts delete mode 100644 src/stores/__tests__/payments.store.spec.ts delete mode 100644 src/stores/__tests__/referrals.store.spec.ts delete mode 100644 src/stores/__tests__/stores.store.spec.ts delete mode 100644 src/stores/__tests__/validations.store.spec.ts delete mode 100644 src/stores/auth.store.ts delete mode 100644 src/stores/credits.store.ts delete mode 100644 src/stores/feedback.store.ts delete mode 100644 src/stores/inventory.store.ts delete mode 100644 src/stores/notifications.store.ts delete mode 100644 src/stores/payments.store.ts delete mode 100644 src/stores/referrals.store.ts delete mode 100644 src/stores/stores.store.ts delete mode 100644 src/stores/validations.store.ts delete mode 100644 src/theme/ThemeContext.tsx delete mode 100644 src/types/index.ts delete mode 100644 src/utils/formatters.ts delete mode 100644 src/utils/validators.ts delete mode 100644 tsconfig.json 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" - ] -}