diff --git a/.gitignore b/.gitignore
index c252908..6996578 100644
--- a/.gitignore
+++ b/.gitignore
@@ -129,4 +129,6 @@ docs/pages/versions/*/react-native/*.diff
!crsqlite.xcframework
# iOS
-**/ios/.xcode.env.local
\ No newline at end of file
+**/ios/.xcode.env.local
+
+eas.json
\ No newline at end of file
diff --git a/app.json b/app.json
index 5add738..44e6854 100644
--- a/app.json
+++ b/app.json
@@ -5,27 +5,36 @@
"scheme": "pharmatech",
"version": "0.2.0",
"orientation": "portrait",
- "icon": "./src/assets/images/icon.png",
+ "icon": "./src/assets/images/PharmaTech-icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
- "image": "src/assets/images/splash-icon.png",
+ "image": "src/assets/images/PharmaTech-icon.png",
"resizeMode": "contain",
- "backgroundColor": "#ffffff"
+ "backgroundColor": "#1C2143"
},
"ios": {
+ "config": {
+ "googleMapsApiKey": "process.env.GOOGLE_MAPS_API_KEY"
+ },
"supportsTablet": true
},
"android": {
+ "permissions": ["ACCESS_FINE_LOCATION", "ACCESS_COARSE_LOCATION"],
"adaptiveIcon": {
- "foregroundImage": "./src/assets/images/icon.png",
- "backgroundColor": "#ffffff"
+ "foregroundImage": "./src/assets/images/PharmaTech-icon.png",
+ "backgroundColor": "#1C2143"
},
- "package": "com.pharmatech.pharmatech"
+ "package": "com.pharmatech.pharmatech",
+ "config": {
+ "googleMaps": {
+ "apiKey": "AIzaSyAoesWK-4-JCtlF5TkHPLn3iInC8tiGNg4"
+ }
+ }
},
"web": {
"bundler": "metro",
- "favicon": "./src/assets/images/icon.png"
+ "favicon": "./src/assets/images/PharmaTech-icon.png"
},
"extra": {
"router": {
@@ -42,6 +51,16 @@
"updates": {
"url": "https://u.expo.dev/3b8bad5d-7426-4881-aebd-cfb1ccf0795d"
},
- "plugins": ["expo-font", "expo-router", "expo-secure-store"]
+ "plugins": [
+ "expo-font",
+ "expo-router",
+ "expo-secure-store",
+ [
+ "expo-image-picker",
+ {
+ "photosPermission": "Allow $(PRODUCT_NAME) to access your camera."
+ }
+ ]
+ ]
}
}
diff --git a/declarations.d.ts b/declarations.d.ts
index 894d908..933d520 100644
--- a/declarations.d.ts
+++ b/declarations.d.ts
@@ -2,3 +2,8 @@ declare module '*.png' {
const value: number;
export default value;
}
+
+declare module '*.jpg' {
+ const value: string;
+ export default value;
+}
diff --git a/eas.json b/eas.json
deleted file mode 100644
index bbf20f3..0000000
--- a/eas.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "build": {
- "preview": {
- "android": {
- "buildType": "apk"
- },
- "env": {
- "PHARMATECH_DEV_MODE": "true"
- }
- },
- "production": {
- "env": {
- "PHARMATECH_DEV_MODE": "false"
- }
- }
- }
-}
diff --git a/package-lock.json b/package-lock.json
index 4479c82..64e45d6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,20 +1,26 @@
{
"name": "app",
- "version": "0.1.0",
+ "version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "app",
- "version": "0.1.0",
+ "version": "0.2.0",
"dependencies": {
"@expo-google-fonts/poppins": "^0.2.3",
- "@pharmatech/sdk": "^0.3.1",
+ "@pharmatech/sdk": "^0.4.21",
+ "@ptomasroos/react-native-multi-slider": "^2.2.2",
+ "@react-native-community/slider": "^4.5.6",
+ "@reduxjs/toolkit": "^2.6.1",
"date-fns": "^4.1.0",
"expo": "~52.0.37",
"expo-constants": "~17.0.7",
+ "expo-dev-client": "~5.0.20",
"expo-font": "~13.0.4",
+ "expo-image-picker": "~16.0.6",
"expo-linking": "~7.0.5",
+ "expo-location": "^18.0.10",
"expo-modules-core": "^2.2.2",
"expo-router": "~4.0.17",
"expo-secure-store": "~14.0.1",
@@ -28,11 +34,15 @@
"react-native": "0.76.7",
"react-native-calendars": "^1.1310.0",
"react-native-heroicons": "^4.0.0",
+ "react-native-maps": "1.18.0",
"react-native-pager-view": "^6.7.0",
"react-native-reanimated": "^3.16.2",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
+ "react-native-sse": "^1.2.1",
"react-native-svg": "^15.11.2",
+ "react-redux": "^9.2.0",
+ "socket.io-client": "^4.8.1",
"tailwindcss": "^3.4.17"
},
"devDependencies": {
@@ -379,12 +389,12 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.26.9",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
- "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
+ "version": "7.26.10",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
+ "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
"dependencies": {
"@babel/template": "^7.26.9",
- "@babel/types": "^7.26.9"
+ "@babel/types": "^7.26.10"
},
"engines": {
"node": ">=6.9.0"
@@ -2000,9 +2010,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.26.9",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
- "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
+ "version": "7.26.10",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
+ "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -2075,9 +2085,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.26.9",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
- "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
+ "version": "7.26.10",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
+ "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
@@ -2410,13 +2420,14 @@
}
},
"node_modules/@expo/config": {
- "version": "10.0.10",
- "resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.10.tgz",
- "integrity": "sha512-wI9/iam3Irk99ADGM/FyD7YrrEibIZXR4huSZiU5zt9o3dASOKhqepiNJex4YPiktLfKhYrpSEJtwno1g0SrgA==",
+ "version": "10.0.11",
+ "resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.11.tgz",
+ "integrity": "sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww==",
+ "license": "MIT",
"dependencies": {
"@babel/code-frame": "~7.10.4",
- "@expo/config-plugins": "~9.0.15",
- "@expo/config-types": "^52.0.4",
+ "@expo/config-plugins": "~9.0.17",
+ "@expo/config-types": "^52.0.5",
"@expo/json-file": "^9.0.2",
"deepmerge": "^4.3.1",
"getenv": "^1.0.0",
@@ -2430,9 +2441,9 @@
}
},
"node_modules/@expo/config-plugins": {
- "version": "9.0.16",
- "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.16.tgz",
- "integrity": "sha512-AnJzmFB7ztM0JZBn+Ut6BQYC2WeGDzfIhBZVOIPMQbdBqvwJ7TmFEsGTGSxdwU/VqJaJK2sWxyt1zbWkpIYCEA==",
+ "version": "9.0.17",
+ "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.17.tgz",
+ "integrity": "sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==",
"license": "MIT",
"dependencies": {
"@expo/config-types": "^52.0.5",
@@ -2843,15 +2854,14 @@
"integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ=="
},
"node_modules/@expo/server": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/@expo/server/-/server-0.5.1.tgz",
- "integrity": "sha512-lk8pKKw0eVP6rqkDR46vQB3vLA46z4KNGrqHpjD/SvMu1cGaRmQG2cQdX44mQtG8WyO9EYau+fBMHQQS2OTFKg==",
- "license": "MIT",
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@expo/server/-/server-0.5.3.tgz",
+ "integrity": "sha512-WXsWzeBs5v/h0PUfHyNLLz07rwwO5myQ1A5DGYewyyGLmsyl61yVCe8AgAlp1wkiMsqhj2hZqI2u3K10QnCMrQ==",
"dependencies": {
- "@remix-run/node": "^2.12.0",
"abort-controller": "^3.0.0",
"debug": "^4.3.4",
- "source-map-support": "~0.5.21"
+ "source-map-support": "~0.5.21",
+ "undici": "^6.18.2"
}
},
"node_modules/@expo/spawn-async": {
@@ -3583,9 +3593,9 @@
}
},
"node_modules/@pharmatech/sdk": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.3.1.tgz",
- "integrity": "sha512-kEfGpYXrnFi75GAZYTkAIKWEwi67bDQBc/rZJ9ZMcgWnJkET9eLCbNWwhQmd0tZOlPOAUOOtuVpI5rSAlCuqkA==",
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.21.tgz",
+ "integrity": "sha512-fxyXlgKN3qLxuGVg6bmRS5DhqfLOLsCYazffax5x0iYFFBIQHQBVRLltm5h84jvOEM0oA7ipD08VVXWMyBj/vQ==",
"license": "MIT",
"dependencies": {
"axios": "^1.8.1"
@@ -3600,6 +3610,16 @@
"node": ">=14"
}
},
+ "node_modules/@ptomasroos/react-native-multi-slider": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@ptomasroos/react-native-multi-slider/-/react-native-multi-slider-2.2.2.tgz",
+ "integrity": "sha512-HWyCnRD3Z3SbHK2FLWYmBBqd1B4iXipeKv1+AK0FoY/CElEDTEixHE8hN60TsqxalPrznn798LE2Q4tHuCiyaA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz",
@@ -3625,6 +3645,12 @@
"react": "^16.8 || ^17.0 || ^18.0"
}
},
+ "node_modules/@react-native-community/slider": {
+ "version": "4.5.6",
+ "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.6.tgz",
+ "integrity": "sha512-UhLPFeqx0YfPLrEz8ffT3uqAyXWu6iqFjohNsbp4cOU7hnJwg2RXtDnYHoHMr7MOkZDVdlLMdrSrAuzY6KGqrg==",
+ "license": "MIT"
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.76.7",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.7.tgz",
@@ -4075,131 +4101,35 @@
"nanoid": "3.3.8"
}
},
- "node_modules/@remix-run/node": {
- "version": "2.16.0",
- "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.16.0.tgz",
- "integrity": "sha512-9yYBYCHYO1+bIScGAtOy5/r4BoTS8E5lpQmjWP99UxSCSiKHPEO76V9Z8mmmarTNis/FPN+sUwfmbQWNHLA2vw==",
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.1.tgz",
+ "integrity": "sha512-SSlIqZNYhqm/oMkXbtofwZSt9lrncblzo6YcZ9zoX+zLngRBrCOjK4lNLdkNucJF58RHOWrD9txT3bT3piH7Zw==",
"license": "MIT",
"dependencies": {
- "@remix-run/server-runtime": "2.16.0",
- "@remix-run/web-fetch": "^4.4.2",
- "@web3-storage/multipart-parser": "^1.0.0",
- "cookie-signature": "^1.1.0",
- "source-map-support": "^0.5.21",
- "stream-slice": "^0.1.2",
- "undici": "^6.11.1"
- },
- "engines": {
- "node": ">=18.0.0"
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
},
"peerDependencies": {
- "typescript": "^5.1.0"
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
- "typescript": {
+ "react": {
"optional": true
- }
- }
- },
- "node_modules/@remix-run/router": {
- "version": "1.23.0",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
- "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@remix-run/server-runtime": {
- "version": "2.16.0",
- "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.16.0.tgz",
- "integrity": "sha512-gbuc4slxPi+pT47MrUYprX/wCuDlYL6H3LHZSvimWO1kDCBt8oefHzdHDPjLi4B1xzqXZomswTbuJzpZ7xRRTg==",
- "license": "MIT",
- "dependencies": {
- "@remix-run/router": "1.23.0",
- "@types/cookie": "^0.6.0",
- "@web3-storage/multipart-parser": "^1.0.0",
- "cookie": "^0.6.0",
- "set-cookie-parser": "^2.4.8",
- "source-map": "^0.7.3",
- "turbo-stream": "2.4.0"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "peerDependencies": {
- "typescript": "^5.1.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
+ },
+ "react-redux": {
"optional": true
}
}
},
- "node_modules/@remix-run/server-runtime/node_modules/source-map": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
- "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@remix-run/web-blob": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@remix-run/web-blob/-/web-blob-3.1.0.tgz",
- "integrity": "sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==",
- "license": "MIT",
- "dependencies": {
- "@remix-run/web-stream": "^1.1.0",
- "web-encoding": "1.1.5"
- }
- },
- "node_modules/@remix-run/web-fetch": {
- "version": "4.4.2",
- "resolved": "https://registry.npmjs.org/@remix-run/web-fetch/-/web-fetch-4.4.2.tgz",
- "integrity": "sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==",
- "license": "MIT",
- "dependencies": {
- "@remix-run/web-blob": "^3.1.0",
- "@remix-run/web-file": "^3.1.0",
- "@remix-run/web-form-data": "^3.1.0",
- "@remix-run/web-stream": "^1.1.0",
- "@web3-storage/multipart-parser": "^1.0.0",
- "abort-controller": "^3.0.0",
- "data-uri-to-buffer": "^3.0.1",
- "mrmime": "^1.0.0"
- },
- "engines": {
- "node": "^10.17 || >=12.3"
- }
- },
- "node_modules/@remix-run/web-file": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@remix-run/web-file/-/web-file-3.1.0.tgz",
- "integrity": "sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==",
- "license": "MIT",
- "dependencies": {
- "@remix-run/web-blob": "^3.1.0"
- }
- },
- "node_modules/@remix-run/web-form-data": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@remix-run/web-form-data/-/web-form-data-3.1.0.tgz",
- "integrity": "sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==",
- "license": "MIT",
- "dependencies": {
- "web-encoding": "1.1.5"
- }
- },
- "node_modules/@remix-run/web-stream": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@remix-run/web-stream/-/web-stream-1.1.0.tgz",
- "integrity": "sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==",
- "license": "MIT",
- "dependencies": {
- "web-streams-polyfill": "^3.1.1"
- }
+ "node_modules/@reduxjs/toolkit/node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
},
"node_modules/@segment/loosely-validate-event": {
"version": "2.0.0",
@@ -4231,6 +4161,11 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
+ },
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
@@ -4760,12 +4695,6 @@
"@babel/types": "^7.20.7"
}
},
- "node_modules/@types/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
- "license": "MIT"
- },
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@@ -4791,6 +4720,12 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -4894,6 +4829,12 @@
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -5136,12 +5077,6 @@
"@urql/core": "^5.0.0"
}
},
- "node_modules/@web3-storage/multipart-parser": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz",
- "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==",
- "license": "(Apache-2.0 AND MIT)"
- },
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@@ -5309,13 +5244,6 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"peer": true
},
- "node_modules/@zxing/text-encoding": {
- "version": "0.9.0",
- "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
- "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
- "license": "(Unlicense OR Apache-2.0)",
- "optional": true
- },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -5742,6 +5670,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
@@ -5753,10 +5682,9 @@
}
},
"node_modules/axios": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
- "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
- "license": "MIT",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
+ "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -5767,7 +5695,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
- "license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -6313,6 +6240,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
@@ -6342,6 +6270,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
+ "dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"get-intrinsic": "^1.2.6"
@@ -6824,24 +6753,6 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
- "node_modules/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
- "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
- "license": "MIT",
- "engines": {
- "node": ">=6.6.0"
- }
- },
"node_modules/core-js": {
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
@@ -6935,6 +6846,16 @@
"node": ">=8"
}
},
+ "node_modules/css-in-js-utils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
+ "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "hyphenate-style-name": "^1.0.3"
+ }
+ },
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -7055,15 +6976,6 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true
},
- "node_modules/data-uri-to-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
- "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==",
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@@ -7261,6 +7173,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
@@ -7571,6 +7484,62 @@
"once": "^1.4.0"
}
},
+ "node_modules/engine.io-client": {
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
+ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1",
+ "xmlhttprequest-ssl": "~2.1.1"
+ }
+ },
+ "node_modules/engine.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io-client/node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -8336,6 +8305,80 @@
"react-native": "*"
}
},
+ "node_modules/expo-dev-client": {
+ "version": "5.0.20",
+ "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.0.20.tgz",
+ "integrity": "sha512-bLNkHdU7V3I4UefgJbJnIDUBUL0LxIal/xYEx9BbgDd3B7wgQKY//+BpPIxBOKCQ22lkyiHY8y9tLhO903sAgg==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-dev-launcher": "5.0.35",
+ "expo-dev-menu": "6.0.25",
+ "expo-dev-menu-interface": "1.9.3",
+ "expo-manifests": "~0.15.8",
+ "expo-updates-interface": "~1.0.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-dev-launcher": {
+ "version": "5.0.35",
+ "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-5.0.35.tgz",
+ "integrity": "sha512-hEQr0ZREnUMxZ6wtQgfK1lzYnbb0zar3HqYZhmANzXmE6UEPbQ4GByLzhpfz/d+xxdBVQZsrHdtiV28KPG2sog==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "8.11.0",
+ "expo-dev-menu": "6.0.25",
+ "expo-manifests": "~0.15.8",
+ "resolve-from": "^5.0.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-dev-launcher/node_modules/ajv": {
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
+ "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/expo-dev-launcher/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/expo-dev-menu": {
+ "version": "6.0.25",
+ "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-6.0.25.tgz",
+ "integrity": "sha512-K2m4z/I+CPWbMtHlDzU68lHaQs52De0v5gbsjAmA5ig8FrYh4MKZvPxSVANaiKENzgmtglu8qaFh7ua9Gt2TfA==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-dev-menu-interface": "1.9.3"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-dev-menu-interface": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.9.3.tgz",
+ "integrity": "sha512-KY/dWTBE1l47i9V366JN5rC6YIdOc9hz8yAmZzkl5DrPia5l3M2WIjtnpHC9zUkNjiSiG2urYoOAq4H/uLdmyg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-eas-client": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-0.13.3.tgz",
@@ -8366,6 +8409,25 @@
"react": "*"
}
},
+ "node_modules/expo-image-loader": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.0.0.tgz",
+ "integrity": "sha512-Eg+5FHtyzv3Jjw9dHwu2pWy4xjf8fu3V0Asyy42kO+t/FbvW/vjUixpTjPtgKQLQh+2/9Nk4JjFDV6FwCnF2ZA==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-image-picker": {
+ "version": "16.0.6",
+ "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-16.0.6.tgz",
+ "integrity": "sha512-HN4xZirFjsFDIsWFb12AZh19fRzuvZjj2ll17cGr19VNRP06S/VPQU3Tdccn5vwUzQhOBlLu704CnNm278boiQ==",
+ "dependencies": {
+ "expo-image-loader": "~5.0.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-json-utils": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.14.0.tgz",
@@ -8395,13 +8457,22 @@
"react-native": "*"
}
},
+ "node_modules/expo-location": {
+ "version": "18.0.10",
+ "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-18.0.10.tgz",
+ "integrity": "sha512-R0Iioz0UZ9Ts8TACPngh8uDFbajJhVa5/igLqWB8Pq/gp8UHuwj7PC8XbZV7avsFoShYjaxrOhf4U7IONeKLgg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-manifests": {
- "version": "0.15.7",
- "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.15.7.tgz",
- "integrity": "sha512-IVzLcPamzUi4Br96xw6JPHaa1vjYupUnMqYyV1Mtd9VQojS5hJsf5VcVzbAMZE/cFGzWLZ1oJa6QXxYjN39Uww==",
+ "version": "0.15.8",
+ "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.15.8.tgz",
+ "integrity": "sha512-VuIyaMfRfLZeETNsRohqhy1l7iZ7I+HKMPfZXVL2Yn17TT0WkOhZoq1DzYwPbOHPgp1Uk6phNa86EyaHrD2DLw==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~10.0.10",
+ "@expo/config": "~10.0.11",
"expo-json-utils": "~0.14.0"
},
"peerDependencies": {
@@ -8637,6 +8708,13 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
+ "node_modules/fast-loops": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.4.tgz",
+ "integrity": "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==",
+ "optional": true,
+ "peer": true
+ },
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@@ -8875,6 +8953,7 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
"dependencies": {
"is-callable": "^1.2.7"
},
@@ -9251,6 +9330,16 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/graphql": {
+ "version": "15.8.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz",
+ "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -9275,6 +9364,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
@@ -9464,6 +9554,13 @@
"url": "https://github.com/sponsors/typicode"
}
},
+ "node_modules/hyphenate-style-name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
+ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
+ "optional": true,
+ "peer": true
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -9516,6 +9613,16 @@
"node": ">=16.x"
}
},
+ "node_modules/immer": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
@@ -9649,6 +9756,17 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
+ "node_modules/inline-style-prefixer": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz",
+ "integrity": "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "css-in-js-utils": "^3.1.0",
+ "fast-loops": "^1.1.3"
+ }
+ },
"node_modules/internal-ip": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz",
@@ -9699,22 +9817,6 @@
"node": ">= 0.10"
}
},
- "node_modules/is-arguments": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
- "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -9828,6 +9930,7 @@
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -9947,6 +10050,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
"integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+ "dev": true,
"dependencies": {
"call-bound": "^1.0.3",
"get-proto": "^1.0.0",
@@ -10043,6 +10147,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
@@ -10128,6 +10233,7 @@
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
"dependencies": {
"which-typed-array": "^1.1.16"
},
@@ -12570,15 +12676,6 @@
"node": "*"
}
},
- "node_modules/mrmime": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
- "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -13516,6 +13613,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -13769,8 +13867,7 @@
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/psl": {
"version": "1.15.0",
@@ -14374,6 +14471,28 @@
"react-native": ">=0.73.0"
}
},
+ "node_modules/react-native-maps": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.18.0.tgz",
+ "integrity": "sha512-S17nYUqeMptgIPaAZuVRo+eRelPreBBYQWw6jsxU7qQ12p+THSfFaqabcNn7fBmsXhT3T27iIl8ek8v1H8BaGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "^7946.0.13"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": ">= 17.0.1",
+ "react-native": ">= 0.64.3",
+ "react-native-web": ">= 0.11"
+ },
+ "peerDependenciesMeta": {
+ "react-native-web": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-native-pager-view": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.7.0.tgz",
@@ -14430,6 +14549,11 @@
"react-native": "*"
}
},
+ "node_modules/react-native-sse": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/react-native-sse/-/react-native-sse-1.2.1.tgz",
+ "integrity": "sha512-zejanlScF+IB9tYnbdry0MT34qjBXbiV/E72qGz33W/tX1bx8MXsbB4lxiuPETc9v/008vYZ60yjIstW22VlVg=="
+ },
"node_modules/react-native-svg": {
"version": "15.11.2",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz",
@@ -14468,6 +14592,41 @@
"integrity": "sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==",
"license": "MIT"
},
+ "node_modules/react-native-web": {
+ "version": "0.19.13",
+ "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
+ "integrity": "sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A==",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.18.6",
+ "@react-native/normalize-colors": "^0.74.1",
+ "fbjs": "^3.0.4",
+ "inline-style-prefixer": "^6.0.1",
+ "memoize-one": "^6.0.0",
+ "nullthrows": "^1.1.1",
+ "postcss-value-parser": "^4.2.0",
+ "styleq": "^0.1.3"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/react-native-web/node_modules/@react-native/normalize-colors": {
+ "version": "0.74.89",
+ "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz",
+ "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/react-native-web/node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "optional": true,
+ "peer": true
+ },
"node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz",
@@ -14536,6 +14695,29 @@
"async-limiter": "~1.0.0"
}
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -14670,6 +14852,21 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -15039,6 +15236,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -15319,16 +15517,11 @@
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="
},
- "node_modules/set-cookie-parser": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
- "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
- "license": "MIT"
- },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
@@ -15586,6 +15779,64 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/socket.io-client": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.6.1",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -15755,12 +16006,6 @@
"node": ">= 0.10.0"
}
},
- "node_modules/stream-slice": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz",
- "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==",
- "license": "MIT"
- },
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
@@ -16017,6 +16262,13 @@
"resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz",
"integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg=="
},
+ "node_modules/styleq": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz",
+ "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==",
+ "optional": true,
+ "peer": true
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -16633,12 +16885,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
- "node_modules/turbo-stream": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
- "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
- "license": "ISC"
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -16748,7 +16994,7 @@
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
- "devOptional": true,
+ "dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16953,7 +17199,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
"dependencies": {
"punycode": "^2.1.0"
}
@@ -16985,19 +17230,6 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
- "node_modules/util": {
- "version": "0.12.5",
- "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
- "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "^2.0.3",
- "is-arguments": "^1.0.4",
- "is-generator-function": "^1.0.7",
- "is-typed-array": "^1.1.3",
- "which-typed-array": "^1.1.2"
- }
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -17099,18 +17331,6 @@
"defaults": "^1.0.3"
}
},
- "node_modules/web-encoding": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
- "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
- "license": "MIT",
- "dependencies": {
- "util": "^0.12.3"
- },
- "optionalDependencies": {
- "@zxing/text-encoding": "0.9.0"
- }
- },
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
@@ -17337,6 +17557,7 @@
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz",
"integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==",
+ "dev": true,
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
@@ -17564,6 +17785,14 @@
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
+ "node_modules/xmlhttprequest-ssl": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index bc59c48..0fe1cfc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "app",
- "version": "0.2.0",
+ "version": "1.0.0",
"main": "index.ts",
"scripts": {
"start": "expo start",
@@ -19,12 +19,18 @@
},
"dependencies": {
"@expo-google-fonts/poppins": "^0.2.3",
- "@pharmatech/sdk": "^0.3.1",
+ "@pharmatech/sdk": "^0.4.21",
+ "@ptomasroos/react-native-multi-slider": "^2.2.2",
+ "@react-native-community/slider": "^4.5.6",
+ "@reduxjs/toolkit": "^2.6.1",
"date-fns": "^4.1.0",
"expo": "~52.0.37",
"expo-constants": "~17.0.7",
+ "expo-dev-client": "~5.0.20",
"expo-font": "~13.0.4",
+ "expo-image-picker": "~16.0.6",
"expo-linking": "~7.0.5",
+ "expo-location": "^18.0.10",
"expo-modules-core": "^2.2.2",
"expo-router": "~4.0.17",
"expo-secure-store": "~14.0.1",
@@ -38,11 +44,15 @@
"react-native": "0.76.7",
"react-native-calendars": "^1.1310.0",
"react-native-heroicons": "^4.0.0",
+ "react-native-maps": "1.18.0",
"react-native-pager-view": "^6.7.0",
"react-native-reanimated": "^3.16.2",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
+ "react-native-sse": "^1.2.1",
"react-native-svg": "^15.11.2",
+ "react-redux": "^9.2.0",
+ "socket.io-client": "^4.8.1",
"tailwindcss": "^3.4.17"
},
"devDependencies": {
diff --git a/src/app/(delivery-tabs)/_layout.tsx b/src/app/(delivery-tabs)/_layout.tsx
new file mode 100644
index 0000000..14b1f43
--- /dev/null
+++ b/src/app/(delivery-tabs)/_layout.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { Tabs } from 'expo-router';
+import { View, StyleSheet } from 'react-native';
+import { Colors } from '../../styles/theme';
+import PoppinsText from '../../components/PoppinsText';
+import { HomeIcon, ClockIcon } from 'react-native-heroicons/outline';
+import TopBarDelivery from '../../components/TopBarDelivery';
+import { AlertProvider } from '../../components/AlertProvider'; // Importar el AlertProvider
+import { TabBar } from '../../components/TabBar';
+
+export default function DeliveryTabLayout() {
+ return (
+
+
+
+ }>
+ (
+
+ Home
+
+ ),
+ headerShown: false,
+ title: 'Home',
+ tabBarIcon: () => (
+
+ ),
+ }}
+ />
+ (
+
+ Historial
+
+ ),
+ headerShown: false,
+ title: 'Historial',
+ tabBarIcon: () => (
+
+ ),
+ }}
+ />
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+});
diff --git a/src/app/(delivery-tabs)/deliveryHistory.tsx b/src/app/(delivery-tabs)/deliveryHistory.tsx
new file mode 100644
index 0000000..ff6b5b9
--- /dev/null
+++ b/src/app/(delivery-tabs)/deliveryHistory.tsx
@@ -0,0 +1,6 @@
+import React from 'react';
+import DeliveryHistoryScreen from '../../screens/delivery/DeliveryHistoryScreen';
+
+export default function DeliveryHistory() {
+ return ;
+}
diff --git a/src/app/(delivery-tabs)/index.tsx b/src/app/(delivery-tabs)/index.tsx
new file mode 100644
index 0000000..8255937
--- /dev/null
+++ b/src/app/(delivery-tabs)/index.tsx
@@ -0,0 +1,6 @@
+import React from 'react';
+import DeliveryHomeScreen from '../../screens/delivery/DeliveryHomeScreen';
+
+export default function DeliveryHome() {
+ return ;
+}
diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx
index c403537..27b9f64 100644
--- a/src/app/(tabs)/_layout.tsx
+++ b/src/app/(tabs)/_layout.tsx
@@ -3,22 +3,32 @@ import {
ListBulletIcon,
MapPinIcon,
TagIcon,
- LifebuoyIcon,
} from 'react-native-heroicons/outline';
import { Tabs } from 'expo-router';
import { Colors } from '../../styles/theme';
import PoppinsText from '../../components/PoppinsText';
import { View, StyleSheet } from 'react-native';
import TopBar from '../../components/TopBar';
+import { TabBar } from '../../components/TabBar';
export default function TabLayout() {
return (
} // Siempre renderiza la TabBar
screenOptions={{
- tabBarActiveTintColor: Colors.primary,
- tabBarStyle: { backgroundColor: Colors.secondaryWhite },
+ tabBarHideOnKeyboard: true,
+ tabBarVisibilityAnimationConfig: {
+ show: {
+ animation: 'timing',
+ config: { duration: 0 },
+ },
+ hide: {
+ animation: 'timing',
+ config: { duration: 0 },
+ },
+ },
}}
>
- (
-
- Soporte
-
- ),
- title: 'Soporte',
- headerShown: false,
- tabBarIcon: () => (
-
- ),
- }}
- />
);
diff --git a/src/app/(tabs)/support.tsx b/src/app/(tabs)/support.tsx
deleted file mode 100644
index 600e4de..0000000
--- a/src/app/(tabs)/support.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-// app/(tabs)/support.tsx
-import React from 'react';
-import SupportScreen from '../../screens/tab/SupportScreen';
-
-export default function TabsHome() {
- return ;
-}
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index bf3dc4e..0440b97 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import {
View,
ActivityIndicator,
@@ -13,7 +13,11 @@ import {
Poppins_600SemiBold,
} from '@expo-google-fonts/poppins';
import { Stack } from 'expo-router/stack';
-import { ChevronLeftIcon } from 'react-native-heroicons/outline';
+import { ChevronLeftIcon, XMarkIcon } from 'react-native-heroicons/outline';
+import { Provider } from 'react-redux';
+import { store } from '../redux/store';
+import { Colors } from '../styles/theme';
+import { NotificationsProvider } from '../hooks/useNotifications';
SplashScreen.preventAutoHideAsync();
@@ -24,51 +28,172 @@ export default function RootLayout() {
Poppins_600SemiBold,
});
+ const [isAppReady, setIsAppReady] = useState(false);
+
useEffect(() => {
- if (fontsLoaded) {
- SplashScreen.hideAsync();
- }
+ const prepareApp = async () => {
+ if (fontsLoaded) {
+ SplashScreen.hideAsync();
+ setIsAppReady(true); // Marca la app como lista
+ }
+ };
+
+ prepareApp();
}, [fontsLoaded]);
- if (!fontsLoaded) {
+ if (!isAppReady) {
return (
-
+
);
}
return (
- ({
- headerBackVisible: false,
- headerLeft: () =>
- navigation.canGoBack() ? (
-
-
-
- ) : null,
- headerBackTitleVisible: false,
- })}
- >
-
-
-
+
+
+ ({
+ headerBackVisible: false,
+ headerStyle: {
+ backgroundColor: Colors.bgColor, // Color específico para esta pantalla
+ },
+ headerShadowVisible: false,
+ headerLeft: () =>
+ navigation.canGoBack() ? (
+
+
+
+ ) : null,
+ headerBackTitleVisible: false,
+ })}
+ >
+ {/* Cambia la pantalla inicial al SplashScreen personalizado */}
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ({
+ headerTitle: '',
+ headerLeft: undefined,
+ headerRight: () => (
+ navigation.goBack()}>
+
+
+ ),
+ })}
+ />
+
+
+
+
+
+
+ ({
+ headerTitle: '',
+ headerTransparent: true,
+ headerShown: true,
+ headerLeft: () => (
+ navigation.goBack()}
+ style={{
+ padding: 12,
+ marginLeft: -12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ }}
+ >
+
+
+ ),
+ })}
+ />
+
+
+
+
+
);
}
diff --git a/src/app/active-orders.tsx b/src/app/active-orders.tsx
new file mode 100644
index 0000000..96416d0
--- /dev/null
+++ b/src/app/active-orders.tsx
@@ -0,0 +1,3 @@
+import ActiveOrdersScreen from '../screens/ActiveOrdersScreen';
+
+export default ActiveOrdersScreen;
diff --git a/src/app/branchDetail/[id].tsx b/src/app/branchDetail/[id].tsx
new file mode 100644
index 0000000..b37d3f7
--- /dev/null
+++ b/src/app/branchDetail/[id].tsx
@@ -0,0 +1,2 @@
+import BranchDetailScreen from '../../screens/BranchDetailScreen';
+export default BranchDetailScreen;
diff --git a/src/app/branchDetail/_layout.tsx b/src/app/branchDetail/_layout.tsx
new file mode 100644
index 0000000..c4d9d43
--- /dev/null
+++ b/src/app/branchDetail/_layout.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { TouchableOpacity, View, StyleSheet } from 'react-native';
+import { Stack } from 'expo-router';
+import { ChevronLeftIcon } from 'react-native-heroicons/outline';
+
+export default function BranchDetailLayout() {
+ return (
+ ({
+ headerShown: true,
+ headerTransparent: true,
+ headerTitle: '',
+ headerLeft: () =>
+ navigation.canGoBack() ? (
+
+
+
+
+
+ ) : null,
+ })}
+ />
+ );
+}
+
+const styles = StyleSheet.create({
+ iconContainer: {
+ marginLeft: -8,
+ },
+});
diff --git a/src/app/cart.tsx b/src/app/cart.tsx
new file mode 100644
index 0000000..3c79068
--- /dev/null
+++ b/src/app/cart.tsx
@@ -0,0 +1,3 @@
+import CartListScreen from '../../src/screens/CartListScreen';
+
+export default CartListScreen;
diff --git a/src/app/categoryDetail/[id].tsx b/src/app/categoryDetail/[id].tsx
new file mode 100644
index 0000000..a220fef
--- /dev/null
+++ b/src/app/categoryDetail/[id].tsx
@@ -0,0 +1,2 @@
+import CategoryDetailScreen from '../../screens/CategoryDetailScreen';
+export default CategoryDetailScreen;
diff --git a/src/app/categoryDetail/_layout.tsx b/src/app/categoryDetail/_layout.tsx
new file mode 100644
index 0000000..cb44643
--- /dev/null
+++ b/src/app/categoryDetail/_layout.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { TouchableOpacity, View, StyleSheet } from 'react-native';
+import { Stack } from 'expo-router';
+import { ChevronLeftIcon } from 'react-native-heroicons/outline';
+
+export default function CategoryDetailLayout() {
+ return (
+ ({
+ headerShown: true,
+ headerTransparent: true,
+ headerTitle: '',
+ headerLeft: () =>
+ navigation.canGoBack() ? (
+
+
+
+
+
+ ) : null,
+ })}
+ />
+ );
+}
+
+const styles = StyleSheet.create({
+ iconContainer: {
+ marginLeft: -8,
+ },
+});
diff --git a/src/app/change-direction/[id].tsx b/src/app/change-direction/[id].tsx
new file mode 100644
index 0000000..c098574
--- /dev/null
+++ b/src/app/change-direction/[id].tsx
@@ -0,0 +1,2 @@
+import ChangeDirectionScreen from '../../screens/DirectionChangeScreen';
+export default ChangeDirectionScreen;
diff --git a/src/app/change-direction/_layout.tsx b/src/app/change-direction/_layout.tsx
new file mode 100644
index 0000000..12e0b44
--- /dev/null
+++ b/src/app/change-direction/_layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+
+export default function DirectionLayout() {
+ return (
+
+ );
+}
diff --git a/src/app/change-password.tsx b/src/app/change-password.tsx
new file mode 100644
index 0000000..e14c711
--- /dev/null
+++ b/src/app/change-password.tsx
@@ -0,0 +1,2 @@
+import ChangePasswordScreen from '../screens/PasswordChangeScreen';
+export default ChangePasswordScreen;
diff --git a/src/app/checkout.tsx b/src/app/checkout.tsx
new file mode 100644
index 0000000..741cfb9
--- /dev/null
+++ b/src/app/checkout.tsx
@@ -0,0 +1,3 @@
+import CheckoutScreen from '../screens/CheckoutScreen';
+
+export default CheckoutScreen;
diff --git a/src/app/createDirection.tsx b/src/app/createDirection.tsx
new file mode 100644
index 0000000..1ae1ce1
--- /dev/null
+++ b/src/app/createDirection.tsx
@@ -0,0 +1,2 @@
+import DirectionCreateScreen from '../screens/DirectionCreateScreen';
+export default DirectionCreateScreen;
diff --git a/src/app/deliveryDetail/[id].tsx b/src/app/deliveryDetail/[id].tsx
new file mode 100644
index 0000000..6f8a55f
--- /dev/null
+++ b/src/app/deliveryDetail/[id].tsx
@@ -0,0 +1,2 @@
+import DeliveryDetailScreen from '../../screens/delivery/DeliveryDetailScreen';
+export default DeliveryDetailScreen;
diff --git a/src/app/deliveryDetail/_layout.tsx b/src/app/deliveryDetail/_layout.tsx
new file mode 100644
index 0000000..2fe1a7e
--- /dev/null
+++ b/src/app/deliveryDetail/_layout.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { TouchableOpacity, View, StyleSheet } from 'react-native';
+import { Stack } from 'expo-router';
+import { ChevronLeftIcon } from 'react-native-heroicons/outline';
+
+export default function DeliveryDetailLayout() {
+ return (
+ ({
+ headerShown: true,
+ headerTransparent: true,
+ headerTitle: '',
+ headerLeft: () =>
+ navigation.canGoBack() ? (
+
+
+
+
+
+ ) : null,
+ })}
+ />
+ );
+}
+
+const styles = StyleSheet.create({
+ iconContainer: {
+ marginLeft: -8,
+ },
+});
diff --git a/src/app/deliveryHistoryDetail/[id].tsx b/src/app/deliveryHistoryDetail/[id].tsx
new file mode 100644
index 0000000..c7fb885
--- /dev/null
+++ b/src/app/deliveryHistoryDetail/[id].tsx
@@ -0,0 +1,2 @@
+import DeliveryHistoryDetailScreen from '../../screens/delivery/DeliveryHistoryDetailScreen';
+export default DeliveryHistoryDetailScreen;
diff --git a/src/app/deliveryHistoryDetail/_layout.tsx b/src/app/deliveryHistoryDetail/_layout.tsx
new file mode 100644
index 0000000..2fe1a7e
--- /dev/null
+++ b/src/app/deliveryHistoryDetail/_layout.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { TouchableOpacity, View, StyleSheet } from 'react-native';
+import { Stack } from 'expo-router';
+import { ChevronLeftIcon } from 'react-native-heroicons/outline';
+
+export default function DeliveryDetailLayout() {
+ return (
+ ({
+ headerShown: true,
+ headerTransparent: true,
+ headerTitle: '',
+ headerLeft: () =>
+ navigation.canGoBack() ? (
+
+
+
+
+
+ ) : null,
+ })}
+ />
+ );
+}
+
+const styles = StyleSheet.create({
+ iconContainer: {
+ marginLeft: -8,
+ },
+});
diff --git a/src/app/direction.tsx b/src/app/direction.tsx
new file mode 100644
index 0000000..4dbdacc
--- /dev/null
+++ b/src/app/direction.tsx
@@ -0,0 +1,2 @@
+import DirectionScreen from '../screens/DirectionScreen';
+export default DirectionScreen;
diff --git a/src/app/in-progress-order.tsx b/src/app/in-progress-order.tsx
new file mode 100644
index 0000000..746011c
--- /dev/null
+++ b/src/app/in-progress-order.tsx
@@ -0,0 +1,3 @@
+import InProgressOrderScreen from '../screens/InProgressOrderScreen';
+
+export default InProgressOrderScreen;
diff --git a/src/app/index.tsx b/src/app/index.tsx
index ef45edd..c644d4f 100644
--- a/src/app/index.tsx
+++ b/src/app/index.tsx
@@ -1,5 +1,5 @@
import { Redirect } from 'expo-router';
export default function Index() {
- return ;
+ return ;
}
diff --git a/src/app/loggedInPasswordRecovery.tsx b/src/app/loggedInPasswordRecovery.tsx
new file mode 100644
index 0000000..49cfc60
--- /dev/null
+++ b/src/app/loggedInPasswordRecovery.tsx
@@ -0,0 +1,2 @@
+import LoggedInPasswordRecoveryScreen from '../screens/LoggedInPasswordRecoveryScreen';
+export default LoggedInPasswordRecoveryScreen;
diff --git a/src/app/menu.tsx b/src/app/menu.tsx
new file mode 100644
index 0000000..fb8d623
--- /dev/null
+++ b/src/app/menu.tsx
@@ -0,0 +1,2 @@
+import MenuScreen from '../screens/MenuScreen';
+export default MenuScreen;
diff --git a/src/app/notifications.tsx b/src/app/notifications.tsx
new file mode 100644
index 0000000..58b88a6
--- /dev/null
+++ b/src/app/notifications.tsx
@@ -0,0 +1,2 @@
+import NotificationsScreen from '../screens/NotificationsScreen';
+export default NotificationsScreen;
diff --git a/src/app/order/[id].tsx b/src/app/order/[id].tsx
new file mode 100644
index 0000000..0645eb6
--- /dev/null
+++ b/src/app/order/[id].tsx
@@ -0,0 +1,2 @@
+import OrderDetailScreen from '../../screens/OrderDetailScreen';
+export default OrderDetailScreen;
diff --git a/src/app/order/_layout.tsx b/src/app/order/_layout.tsx
new file mode 100644
index 0000000..365a7bd
--- /dev/null
+++ b/src/app/order/_layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+
+export default function OrdersLayout() {
+ return (
+
+ );
+}
diff --git a/src/app/order/tracking/[id].tsx b/src/app/order/tracking/[id].tsx
new file mode 100644
index 0000000..6d95284
--- /dev/null
+++ b/src/app/order/tracking/[id].tsx
@@ -0,0 +1,2 @@
+import OrderTrackingScreen from '../../../screens/OrderTrackingScreen';
+export default OrderTrackingScreen;
diff --git a/src/app/order/tracking/_layout.tsx b/src/app/order/tracking/_layout.tsx
new file mode 100644
index 0000000..c0f43ee
--- /dev/null
+++ b/src/app/order/tracking/_layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+
+export default function OrdersTrackingLayout() {
+ return (
+
+ );
+}
diff --git a/src/app/orders.tsx b/src/app/orders.tsx
new file mode 100644
index 0000000..929cce9
--- /dev/null
+++ b/src/app/orders.tsx
@@ -0,0 +1,2 @@
+import OrdersScreen from '../screens/OrdersScreen';
+export default OrdersScreen;
diff --git a/src/app/products/[productId]/_layout.tsx b/src/app/products/[productId]/_layout.tsx
new file mode 100644
index 0000000..c87c606
--- /dev/null
+++ b/src/app/products/[productId]/_layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+
+export default function ProductIdLayout() {
+ return (
+
+ );
+}
diff --git a/src/app/products/[productId]/presentation/[id].tsx b/src/app/products/[productId]/presentation/[id].tsx
new file mode 100644
index 0000000..9e295fd
--- /dev/null
+++ b/src/app/products/[productId]/presentation/[id].tsx
@@ -0,0 +1,2 @@
+import ProductDetailScreen from '../../../../screens/ProductDetailScreen';
+export default ProductDetailScreen;
diff --git a/src/app/products/[productId]/presentation/_layout.tsx b/src/app/products/[productId]/presentation/_layout.tsx
new file mode 100644
index 0000000..1bbfe4d
--- /dev/null
+++ b/src/app/products/[productId]/presentation/_layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+
+export default function PresentationLayout() {
+ return (
+
+ );
+}
diff --git a/src/app/products/_layout.tsx b/src/app/products/_layout.tsx
new file mode 100644
index 0000000..04160e7
--- /dev/null
+++ b/src/app/products/_layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+
+export default function ProductsLayout() {
+ return (
+
+ );
+}
diff --git a/src/app/profile.tsx b/src/app/profile.tsx
new file mode 100644
index 0000000..f48c96f
--- /dev/null
+++ b/src/app/profile.tsx
@@ -0,0 +1,2 @@
+import ProfileScreen from '../screens/ProfileScreen';
+export default ProfileScreen;
diff --git a/src/app/search/[query].tsx b/src/app/search/[query].tsx
new file mode 100644
index 0000000..8864333
--- /dev/null
+++ b/src/app/search/[query].tsx
@@ -0,0 +1,2 @@
+import SearchProductScreen from '../../screens/SearchProductScreen';
+export default SearchProductScreen;
diff --git a/src/app/search/_layout.tsx b/src/app/search/_layout.tsx
new file mode 100644
index 0000000..7f2e24a
--- /dev/null
+++ b/src/app/search/_layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+
+export default function SearchLayout() {
+ return (
+
+ );
+}
diff --git a/src/app/selectLocation.tsx b/src/app/selectLocation.tsx
new file mode 100644
index 0000000..4dfa35d
--- /dev/null
+++ b/src/app/selectLocation.tsx
@@ -0,0 +1,2 @@
+import SelectLocationScreen from '../screens/SelectLocationScreen';
+export default SelectLocationScreen;
diff --git a/src/app/splash.tsx b/src/app/splash.tsx
new file mode 100644
index 0000000..d5011e5
--- /dev/null
+++ b/src/app/splash.tsx
@@ -0,0 +1,2 @@
+import SplashScreen from '../screens/SplashScreen';
+export default SplashScreen;
diff --git a/src/assets/images/PharmaTech-icon.png b/src/assets/images/PharmaTech-icon.png
new file mode 100644
index 0000000..2afdb37
Binary files /dev/null and b/src/assets/images/PharmaTech-icon.png differ
diff --git a/src/assets/images/defaults/category.png b/src/assets/images/defaults/category.png
new file mode 100644
index 0000000..ddecb47
Binary files /dev/null and b/src/assets/images/defaults/category.png differ
diff --git a/src/assets/images/notifications/e.svg b/src/assets/images/notifications/e.svg
new file mode 100644
index 0000000..56e4734
--- /dev/null
+++ b/src/assets/images/notifications/e.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/notifications/f.svg b/src/assets/images/notifications/f.svg
new file mode 100644
index 0000000..aa3a765
--- /dev/null
+++ b/src/assets/images/notifications/f.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/notifications/image.svg b/src/assets/images/notifications/image.svg
new file mode 100644
index 0000000..2b74403
--- /dev/null
+++ b/src/assets/images/notifications/image.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/notifications/m.svg b/src/assets/images/notifications/m.svg
new file mode 100644
index 0000000..a5b7463
--- /dev/null
+++ b/src/assets/images/notifications/m.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/notifications/r.svg b/src/assets/images/notifications/r.svg
new file mode 100644
index 0000000..abb5ee3
--- /dev/null
+++ b/src/assets/images/notifications/r.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/notifications/w.svg b/src/assets/images/notifications/w.svg
new file mode 100644
index 0000000..85e6f67
--- /dev/null
+++ b/src/assets/images/notifications/w.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/components/AlertProvider.tsx b/src/components/AlertProvider.tsx
new file mode 100644
index 0000000..9dda4fc
--- /dev/null
+++ b/src/components/AlertProvider.tsx
@@ -0,0 +1,74 @@
+import React, { createContext, useContext, useState } from 'react';
+import { View, StyleSheet } from 'react-native';
+import Alert from './Alerts';
+
+type AlertContextType = {
+ showAlert: (
+ type: 'success' | 'error' | 'info' | 'warning',
+ title: string,
+ message: string,
+ ) => void;
+};
+
+const AlertContext = createContext(undefined);
+
+export const AlertProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const [visible, setVisible] = useState(false);
+ const [type, setType] = useState<'success' | 'error' | 'info' | 'warning'>(
+ 'info',
+ );
+ const [title, setTitle] = useState('');
+ const [message, setMessage] = useState('');
+
+ const showAlert = (
+ alertType: 'success' | 'error' | 'info' | 'warning',
+ alertTitle: string,
+ alertMessage: string,
+ ) => {
+ setType(alertType);
+ setTitle(alertTitle);
+ setMessage(alertMessage);
+ setVisible(true);
+
+ // Ocultar la alerta automáticamente después de 3 segundos
+ setTimeout(() => setVisible(false), 3000);
+ };
+
+ return (
+
+ {children}
+ {visible && (
+
+ setVisible(false)}
+ />
+
+ )}
+
+ );
+};
+
+export const useAlert = (): AlertContextType => {
+ const context = useContext(AlertContext);
+ if (!context) {
+ throw new Error('useAlert must be used within an AlertProvider');
+ }
+ return context;
+};
+
+const styles = StyleSheet.create({
+ alertContainer: {
+ position: 'absolute',
+ top: 20,
+ left: '50%',
+ transform: [{ translateX: -163 }], // Centrar horizontalmente
+ zIndex: 1000,
+ width: 326,
+ },
+});
diff --git a/src/components/Alerts.tsx b/src/components/Alerts.tsx
index 758bf0a..440cad0 100644
--- a/src/components/Alerts.tsx
+++ b/src/components/Alerts.tsx
@@ -22,6 +22,8 @@ interface AlertProps {
onClose?: () => void;
}
+let activeAlertClose: (() => void) | null = null;
+
const Alert: React.FC = ({
title,
message,
@@ -62,6 +64,12 @@ const Alert: React.FC = ({
const opacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
+ // Close any active alert before showing a new one
+ if (activeAlertClose) {
+ activeAlertClose();
+ }
+ activeAlertClose = handleClose;
+
Animated.parallel([
Animated.timing(translateY, {
toValue: 0,
@@ -73,6 +81,12 @@ const Alert: React.FC = ({
useNativeDriver: true,
}),
]).start();
+
+ return () => {
+ if (activeAlertClose === handleClose) {
+ activeAlertClose = null;
+ }
+ };
}, []);
const handleClose = () => {
@@ -88,7 +102,7 @@ const Alert: React.FC = ({
useNativeDriver: true,
}),
]).start(() => {
- onClose?.();
+ if (onClose) onClose();
});
};
diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx
new file mode 100644
index 0000000..facb744
--- /dev/null
+++ b/src/components/Avatar.tsx
@@ -0,0 +1,151 @@
+import React, { useEffect, useState } from 'react';
+import { Image, View, StyleSheet, ActivityIndicator } from 'react-native';
+import { Colors } from '../styles/theme';
+import { UserService } from '../services/user';
+import PoppinsText from './PoppinsText';
+import * as SecureStore from 'expo-secure-store';
+import { usePathname } from 'expo-router';
+
+type AvatarProps = {
+ scale?: number; // Propiedad para definir el tamaño del avatar
+};
+
+const Avatar: React.FC = ({ scale = 32 }) => {
+ const [profile, setProfile] = useState<{ uri?: string; name?: string }>({});
+ const [loading, setLoading] = useState(true);
+ const pathname = usePathname();
+
+ useEffect(() => {
+ const fetchProfile = async () => {
+ setLoading(true);
+
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ console.warn(
+ 'No se encontró el token de autenticación. Usando datos locales.',
+ );
+
+ const userData = await SecureStore.getItemAsync('user_data');
+ if (userData) {
+ const { firstName, lastName } = JSON.parse(userData);
+ setProfile({
+ name: `${firstName} ${lastName}`,
+ });
+ } else {
+ console.error('No se encontraron datos del usuario en SecureStore.');
+ }
+
+ setLoading(false);
+ return;
+ }
+
+ const response = await UserService.getProfile();
+ if (response.success) {
+ const { firstName, lastName } = response.data!;
+ const profilePicture = response.data.profile.profilePicture;
+ setProfile({
+ uri:
+ profilePicture !== 'https://via.placeholder.com/150'
+ ? profilePicture
+ : '',
+ name: `${firstName} ${lastName}`,
+ });
+ } else {
+ console.error('Error fetching profile:', response.error);
+ }
+ setLoading(false);
+ };
+
+ fetchProfile();
+ }, [pathname]);
+
+ const getInitials = (fullName: string): string => {
+ const words = fullName.trim().split(' ');
+ if (words.length === 1) return words[0].charAt(0).toUpperCase();
+ return (words[0].charAt(0) + words[1].charAt(0)).toUpperCase();
+ };
+
+ // Cálculo dinámico de tamaño y fuentes
+ const avatarSize = scale;
+ const fontSize = avatarSize / 2.5;
+ const lineHeight = fontSize + 1;
+ const marginTop = avatarSize / 10;
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {profile.uri ? (
+
+ ) : (
+
+ {profile.name ? getInitials(profile.name) : ''}
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ avatar: {
+ backgroundColor: Colors.primary,
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ },
+ avatarImage: {
+ resizeMode: 'cover',
+ },
+ initials: {
+ color: Colors.textWhite,
+ textAlign: 'center',
+ },
+ avatarContainer: {
+ backgroundColor: Colors.primary,
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ },
+ loadingContainer: {
+ backgroundColor: Colors.stroke,
+ },
+});
+
+export default Avatar;
diff --git a/src/components/AvatarWithMenu.tsx b/src/components/AvatarWithMenu.tsx
new file mode 100644
index 0000000..3432caa
--- /dev/null
+++ b/src/components/AvatarWithMenu.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { View, TouchableOpacity, StyleSheet } from 'react-native';
+import Avatar from './Avatar';
+import { useRouter } from 'expo-router';
+
+const AvatarWithMenu: React.FC = () => {
+ const router = useRouter();
+
+ return (
+
+ router.push('menu')}>
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ top: -15,
+ left: 0,
+ zIndex: 100,
+ },
+});
+
+export default AvatarWithMenu;
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
new file mode 100644
index 0000000..e308669
--- /dev/null
+++ b/src/components/Badge.tsx
@@ -0,0 +1,173 @@
+import React from 'react';
+import {
+ View,
+ StyleSheet,
+ StyleProp,
+ ViewStyle,
+ TextStyle,
+} from 'react-native';
+import { Colors } from '../styles/theme';
+import PoppinsText from './PoppinsText';
+
+export interface BadgeProps {
+ variant: 'filled' | 'outlined' | 'text';
+ color:
+ | 'primary'
+ | 'warning'
+ | 'danger'
+ | 'success'
+ | 'info'
+ | 'secondary_300'
+ | 'primary_300';
+ size: 'tiny' | 'small' | 'medium' | 'large';
+ borderRadius?: 'rounded' | 'square';
+ textColor?: string;
+ children: React.ReactNode;
+}
+
+const Badge: React.FC = ({
+ variant,
+ color,
+ size,
+ borderRadius = 'rounded',
+ textColor,
+ children,
+}) => {
+ const sizeStyles: Record> = {
+ tiny: { height: 17, fontSize: 10, minWidth: 17 },
+ small: { height: 24, fontSize: 12, minWidth: 24 },
+ medium: { height: 32, fontSize: 14, minWidth: 32 },
+ large: { height: 40, fontSize: 16, minWidth: 40 },
+ };
+
+ const borderRadiusStyles: Record> = {
+ rounded: { borderRadius: 9999 },
+ square: { borderRadius: 4 },
+ };
+
+ const colorStyles: Record<
+ string,
+ Record>
+ > = {
+ filled: {
+ primary: { backgroundColor: Colors.primary, color: Colors.textWhite },
+ warning: {
+ backgroundColor: Colors.semanticWarning,
+ color: Colors.textWhite,
+ },
+ danger: {
+ backgroundColor: Colors.semanticDanger,
+ color: Colors.textWhite,
+ },
+ success: {
+ backgroundColor: Colors.semanticSuccess,
+ color: Colors.textWhite,
+ },
+ info: { backgroundColor: Colors.semanticInfo, color: Colors.textWhite },
+
+ secondary_300: {
+ backgroundColor: Colors.secondary_300,
+ color: Colors.textWhite,
+ },
+
+ primary_300: {
+ backgroundColor: Colors.primary_300,
+ color: Colors.textWhite,
+ },
+ },
+ outlined: {
+ primary: {
+ borderColor: Colors.primary,
+ borderWidth: 1,
+ color: Colors.primary,
+ },
+ warning: {
+ borderColor: Colors.semanticWarning,
+ borderWidth: 1,
+ color: Colors.semanticWarning,
+ },
+ danger: {
+ borderColor: Colors.semanticDanger,
+ borderWidth: 1,
+ color: Colors.semanticDanger,
+ },
+ success: {
+ borderColor: Colors.semanticSuccess,
+ borderWidth: 1,
+ color: Colors.semanticSuccess,
+ },
+ info: {
+ borderColor: Colors.semanticInfo,
+ borderWidth: 1,
+ color: Colors.semanticInfo,
+ },
+ secondary_300: {
+ borderColor: Colors.secondary_300,
+ borderWidth: 1,
+ color: Colors.secondary_300,
+ },
+ primary_300: {
+ borderColor: Colors.primary_300,
+ borderWidth: 1,
+ color: Colors.primary_300,
+ },
+ },
+ text: {
+ primary: { color: Colors.primary },
+ warning: { color: Colors.semanticWarning },
+ danger: { color: Colors.semanticDanger },
+ success: { color: Colors.semanticSuccess },
+ info: { color: Colors.semanticInfo },
+ secondary_300: { color: Colors.secondary_300 },
+ primary_300: { color: Colors.primary_300 },
+ },
+ };
+
+ const variantStyle = colorStyles[variant][color];
+ const sizeStyle = sizeStyles[size];
+ const borderRadiusStyle = borderRadiusStyles[borderRadius];
+
+ return (
+ 1 ? 6 : 4 },
+ ]}
+ >
+
+ {children}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ base: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 4,
+ },
+ text: {
+ textAlign: 'center',
+ marginTop: 1.5,
+ lineHeight: 16,
+ fontSize: 10,
+ },
+});
+
+export default Badge;
diff --git a/src/components/BranchCard.tsx b/src/components/BranchCard.tsx
new file mode 100644
index 0000000..e6dd46a
--- /dev/null
+++ b/src/components/BranchCard.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { StyleSheet, TouchableOpacity } from 'react-native';
+import { BranchResponse } from '@pharmatech/sdk';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from './PoppinsText';
+
+type BranchCardProps = {
+ branch: BranchResponse;
+ onPress: () => void;
+};
+
+const BranchCard: React.FC = ({ branch, onPress }) => {
+ return (
+
+ {branch.name}
+ {branch.address}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 8,
+ padding: 16,
+ marginBottom: 10,
+ shadowColor: '#000',
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ name: {
+ fontSize: FontSizes.s2.size,
+ lineHeight: FontSizes.s2.lineHeight,
+ color: Colors.primary,
+ marginBottom: 4,
+ },
+ address: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.textLowContrast,
+ },
+});
+
+export default BranchCard;
diff --git a/src/components/BranchMap.tsx b/src/components/BranchMap.tsx
new file mode 100644
index 0000000..e6556e7
--- /dev/null
+++ b/src/components/BranchMap.tsx
@@ -0,0 +1,117 @@
+import React, { useMemo } from 'react';
+import { StyleSheet, View } from 'react-native';
+import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
+import { Colors } from '../styles/theme';
+import { MapPinIcon } from 'react-native-heroicons/solid';
+import PoppinsText from './PoppinsText';
+
+type Branch = {
+ id: string;
+ name: string;
+ address: string;
+ latitude: number | null | undefined;
+ longitude: number | null | undefined;
+ stockQuantity: number;
+};
+
+type BranchMapProps = {
+ branches: Branch[];
+};
+
+// Componente Marker personalizado con React.memo
+const CustomMarker = React.memo(({ branch }: { branch: Branch }) => (
+
+
+
+
+
+));
+
+CustomMarker.displayName = 'CustomMarker';
+
+const BranchMap: React.FC = ({ branches }) => {
+ // Memorizar las sucursales válidas
+ const validBranches = useMemo(
+ () =>
+ branches.filter(
+ (branch) =>
+ branch.latitude !== null &&
+ branch.latitude !== undefined &&
+ branch.longitude !== null &&
+ branch.longitude !== undefined,
+ ),
+ [branches],
+ );
+
+ const initialRegion = useMemo(
+ () =>
+ validBranches.length > 0
+ ? {
+ latitude: validBranches[0].latitude!,
+ longitude: validBranches[0].longitude!,
+ latitudeDelta: 0.1,
+ longitudeDelta: 0.1,
+ }
+ : {
+ latitude: 10.0678, // Coordenadas de Barquisimeto, estado Lara, Venezuela
+ longitude: -69.3467,
+ latitudeDelta: 0.1,
+ longitudeDelta: 0.1,
+ },
+ [validBranches],
+ );
+
+ return (
+
+ {validBranches.length > 0 ? (
+
+ {validBranches.map((branch) => (
+
+ ))}
+
+ ) : (
+
+
+ No hay sucursales disponibles para mostrar en el mapa.
+
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1, // Asegura que el mapa ocupe todo el espacio disponible
+ },
+ map: {
+ ...StyleSheet.absoluteFillObject, // Hace que el mapa ocupe todo el espacio del contenedor
+ },
+ noDataContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ noDataText: {
+ textAlign: 'center',
+ color: 'gray',
+ },
+ customMarker: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
+export default BranchMap;
diff --git a/src/components/BranchMapModal.tsx b/src/components/BranchMapModal.tsx
new file mode 100644
index 0000000..9d15c5d
--- /dev/null
+++ b/src/components/BranchMapModal.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+import { Modal, View, StyleSheet, TouchableOpacity } from 'react-native';
+import { XMarkIcon, MapPinIcon } from 'react-native-heroicons/solid';
+import PoppinsText from './PoppinsText';
+import BranchMap from './BranchMap'; // Import BranchMap
+import { Colors } from '../styles/theme';
+
+const BranchMapModal = ({
+ visible,
+ onClose,
+ branchName,
+ branchCoordinates,
+}: {
+ visible: boolean;
+ onClose: () => void;
+ branchName: string | null;
+ branchCoordinates: { latitude: number; longitude: number } | null; // Add branchCoordinates prop
+}) => {
+ return (
+
+
+
+
+
+
+
+ Dirección de Sucursal
+
+
+ {branchCoordinates ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {branchName}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0,0,0,0.4)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ modal: {
+ width: 352,
+ height: 448,
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 20,
+ alignItems: 'center',
+ position: 'relative',
+ elevation: 8,
+ },
+ closeButton: {
+ position: 'absolute',
+ top: 20,
+ right: 20,
+ zIndex: 10,
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: Colors.textMain,
+ textAlign: 'center',
+ marginTop: 20,
+ marginBottom: 16,
+ },
+ mapContainer: {
+ width: 285,
+ height: 285,
+ borderRadius: 12,
+ overflow: 'hidden',
+ backgroundColor: Colors.gray_100,
+ marginBottom: 20,
+ },
+ fakeMap: {
+ flex: 1,
+ backgroundColor: '#ccc',
+ },
+ branchInfo: {
+ flexDirection: 'row',
+ alignSelf: 'flex-start',
+ alignItems: 'center',
+ marginLeft: 5,
+ },
+ branchName: {
+ fontSize: 14,
+ color: Colors.textMain,
+ marginLeft: 6,
+ textAlign: 'left',
+ },
+});
+
+export default BranchMapModal;
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index d123ef0..fac699e 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -17,7 +17,8 @@ export type ButtonVariant =
| 'secondaryLight'
| 'secondaryWhite'
| 'secondaryGray'
- | 'disabled';
+ | 'disabled'
+ | 'iconCancel';
export type ButtonMode = 'filled' | 'outline';
@@ -67,6 +68,10 @@ const getButtonStyles = (
backgroundColor = Colors.stroke;
borderColor = Colors.stroke;
break;
+ case 'iconCancel': // Nuevo color para el botón "Descartar"
+ backgroundColor = Colors.iconCancel;
+ borderColor = Colors.iconCancel;
+ break;
default:
backgroundColor = Colors.primary;
borderColor = Colors.primary;
@@ -98,19 +103,23 @@ const getTextColor = (variant: ButtonVariant, mode: ButtonMode): string => {
return Colors.secondaryGray;
case 'disabled':
return Colors.disableText;
+ case 'iconCancel': // Color para el texto del botón "Descartar"
+ return Colors.iconCancel;
default:
return Colors.primary;
}
}
return variant === 'secondaryWhite'
? Colors.textHighContrast
- : variant === 'secondary'
- ? Colors.textHighContrast
- : variant === 'secondaryLight'
+ : variant === 'iconCancel'
+ ? Colors.textWhite
+ : variant === 'disabled'
? Colors.textHighContrast
- : variant === 'disabled'
- ? Colors.disableText
- : Colors.textWhite;
+ : variant === 'secondary'
+ ? Colors.textHighContrast
+ : variant === 'secondaryLight'
+ ? Colors.disableText
+ : Colors.textWhite;
};
const getFontSizeStyle = (size: ButtonSize): TextStyle => {
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
index d494007..8e492cb 100644
--- a/src/components/Card.tsx
+++ b/src/components/Card.tsx
@@ -1,21 +1,18 @@
import React from 'react';
-import { View, StyleSheet, Image } from 'react-native';
+import { View, StyleSheet, Image, TouchableOpacity } from 'react-native';
import { Colors, FontSizes } from '../styles/theme';
import PoppinsText from './PoppinsText';
import CardButton from './CardButton';
import { truncateString } from '../utils/commons';
+import { useCart } from '../hooks/useCart';
+import { Product } from '../types/Product';
+import { useRouter } from 'expo-router';
+import { formatPrice } from '../utils/formatPrice';
-interface ProductCardProps {
- imageUrl?: string;
- name: string;
- category?: string;
- originalPrice: string;
- discount?: string;
- finalPrice?: string;
- getQuantity?: (count: number) => void;
-}
-
-const ProductCard: React.FC = ({
+const ProductCard: React.FC = ({
+ id,
+ presentationId,
+ productId,
imageUrl,
name,
category,
@@ -24,107 +21,130 @@ const ProductCard: React.FC = ({
finalPrice,
getQuantity,
}) => {
+ const { getItemQuantity, addToCart } = useCart();
+ const router = useRouter();
+ const computedFinalPrice = discount
+ ? (finalPrice * (100 - discount)) / 100
+ : finalPrice;
+
return (
-
-
+
+ router.push(
+ '/products/' + productId + '/presentation/' + presentationId,
+ )
+ }
+ >
+
- {category}
-
-
-
-
-
+
+ {category}
-
-
-
- {truncateString(name)}
- {discount && (
-
-
- ${originalPrice}
-
- {discount}%
+
+
+
+ {
+ if (getQuantity) getQuantity(quantity);
+ // Asegura que price siempre sea number
+ addToCart({
+ id,
+ name,
+ price: originalPrice ?? 0,
+ quantity,
+ image: imageUrl,
+ discount: discount ?? 0,
+ });
+ }}
+ initialValue={getItemQuantity(id)}
+ />
+
- )}
-
- ${finalPrice}
-
+
+
+ {truncateString(name)}
+ {discount && (
+
+
+ ${formatPrice(originalPrice ?? 0)}
+
+ {discount}%
+
+ )}
+
+ ${formatPrice(computedFinalPrice)}
+
+
-
+
);
};
const styles = StyleSheet.create({
card: {
margin: 10,
- backgroundColor: '#fff',
+ backgroundColor: Colors.menuWhite,
borderWidth: 1,
- borderColor: '#ccc',
+ borderColor: Colors.gray_100,
borderRadius: 15,
- padding: 16,
+ padding: 10,
paddingBottom: 5,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.1,
- shadowRadius: 4,
- elevation: 3,
marginBottom: 16,
- maxWidth: 170,
- minWidth: 170,
- minHeight: 330,
- maxHeight: 330,
+ maxWidth: 140,
+ minWidth: 140,
+ minHeight: 315,
+ maxHeight: 315,
},
tag: {
backgroundColor: Colors.semanticInfo,
borderRadius: 50,
paddingHorizontal: 9,
color: Colors.textWhite,
- fontSize: FontSizes.c2.size,
+ fontSize: FontSizes.c3.size,
maxWidth: '100%',
},
imageContainer: {
- backgroundColor: Colors.secondaryGray,
- minHeight: 140,
- maxHeight: 140,
+ minHeight: 118,
+ maxHeight: 118,
+ maxWidth: 114,
borderRadius: 15,
marginVertical: 8,
},
cardButtonContainer: {
position: 'relative',
top: -30,
- left: 63,
+ left: 58,
maxWidth: '65%',
alignItems: 'flex-end',
zIndex: 999,
},
description: {
- marginHorizontal: 4,
+ marginHorizontal: 2,
+ justifyContent: 'center',
},
name: {
fontSize: FontSizes.s2.size,
- marginBottom: 8,
+ marginBottom: 6,
+ minHeight: 70,
+ maxHeight: 70,
},
priceContainer: {
flexDirection: 'row',
@@ -134,16 +154,16 @@ const styles = StyleSheet.create({
fontSize: FontSizes.b1.size,
color: Colors.disableText,
textDecorationLine: 'line-through',
- marginRight: 18,
+ marginRight: 14,
},
discount: {
fontSize: FontSizes.c1.size,
backgroundColor: Colors.semanticInfo,
borderRadius: 5,
- padding: 5,
+ padding: 4,
},
finalPrice: {
- fontSize: 20,
+ fontSize: FontSizes.s1.size,
},
});
diff --git a/src/components/CardButton.tsx b/src/components/CardButton.tsx
index 256e198..1ab08d0 100644
--- a/src/components/CardButton.tsx
+++ b/src/components/CardButton.tsx
@@ -6,33 +6,63 @@ import { Colors, FontSizes } from '../styles/theme';
interface CardButtonProps {
getValue?: (count: number) => void;
+ initialValue: number;
+ size?: number;
+ syncQuantity?: (count: number) => void;
+ disabled?: boolean;
+ showNoStockAlert?: () => void; // <-- nuevo prop opcional
}
-const CardButton: React.FC = ({ getValue }) => {
- const [count, setCount] = useState(0);
+const CardButton: React.FC = ({
+ getValue,
+ initialValue = 0,
+ size = 4,
+ syncQuantity,
+ disabled = false, // <-- Valor por defecto
+ showNoStockAlert, // <-- Add this line
+}) => {
+ const [count, setCount] = useState(initialValue);
const [showCounter, setShowCounter] = useState(false);
+ const [hasInteracted, setHasInteracted] = useState(false);
+
+ const toggleCounter = () => setShowCounter(count > 0);
+ useEffect(toggleCounter, [count]);
useEffect(() => {
- if (count == 0) setShowCounter(false);
- else {
- setShowCounter(true);
- }
- if (getValue) {
+ if (hasInteracted && getValue) {
getValue(count);
}
+ if (hasInteracted && syncQuantity) {
+ syncQuantity(count);
+ }
}, [count]);
+ useEffect(() => {
+ setCount(initialValue);
+ }, [initialValue]);
+
const incrementCount = () => {
+ if (disabled) {
+ if (showNoStockAlert) showNoStockAlert();
+ return;
+ }
+ setHasInteracted(true);
setCount((prev) => prev + 1);
};
const decrementCount = () => {
+ if (disabled) {
+ if (showNoStockAlert) showNoStockAlert();
+ return;
+ }
if (count > 0) {
+ setHasInteracted(true);
setCount((prev) => prev - 1);
}
};
const showCounterIncrement = () => {
+ if (disabled) return; // <-- No permitir si está deshabilitado
setShowCounter(true);
incrementCount();
};
@@ -41,9 +71,14 @@ const CardButton: React.FC = ({ getValue }) => {
{!showCounter ? (
{
- showCounterIncrement();
+ if (disabled && showNoStockAlert) showNoStockAlert();
+ else showCounterIncrement();
}}
>
@@ -51,10 +86,19 @@ const CardButton: React.FC = ({ getValue }) => {
) : (
-
+
{
+ if (disabled && showNoStockAlert) showNoStockAlert();
+ else decrementCount();
+ }}
>
@@ -65,7 +109,10 @@ const CardButton: React.FC = ({ getValue }) => {
{
+ if (disabled && showNoStockAlert) showNoStockAlert();
+ else incrementCount();
+ }}
>
@@ -85,8 +132,6 @@ const styles = StyleSheet.create({
},
mainButton: {
backgroundColor: Colors.primary,
- paddingVertical: 5,
- paddingHorizontal: 5,
borderRadius: 50,
elevation: 3,
},
@@ -96,7 +141,6 @@ const styles = StyleSheet.create({
gap: 15,
backgroundColor: Colors.primary,
borderRadius: 50,
- padding: 5,
},
counterButton: {
width: 20,
diff --git a/src/components/Carousel.tsx b/src/components/Carousel.tsx
index c9c54ac..a80adb9 100644
--- a/src/components/Carousel.tsx
+++ b/src/components/Carousel.tsx
@@ -1,32 +1,24 @@
import React from 'react';
import { View, StyleSheet, Dimensions, ScrollView } from 'react-native';
import ProductCard from './Card';
-import { ProductPresentation } from '../types/api';
+import { Product } from '../types/Product';
const { width: screenWidth } = Dimensions.get('window');
-type ProductCardProps = {
- cards: ProductPresentation[];
+type CarouselProps = {
+ cards: Product[];
};
-const Carrusel: React.FC = ({ cards }) => {
+const Carousel: React.FC = ({ cards }) => {
return (
{cards.map((item, index) => (
-
+
))}
@@ -38,10 +30,10 @@ const styles = StyleSheet.create({
flex: 1,
},
card: {
- width: screenWidth - 170, // Ancho de cada card
+ width: screenWidth - 207, // Ancho de cada card
borderRadius: 10,
alignItems: 'center',
},
});
-export default Carrusel;
+export default Carousel;
diff --git a/src/components/CartTotal.tsx b/src/components/CartTotal.tsx
new file mode 100644
index 0000000..cb35ecb
--- /dev/null
+++ b/src/components/CartTotal.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { RootState } from '../redux/store';
+import { Text } from 'react-native';
+
+const CartTotal = () => {
+ const total = useSelector((state: RootState) => state.cart.total);
+
+ return Total del carrito: ${total};
+};
+
+export default CartTotal;
diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx
index 3631545..7b8233d 100644
--- a/src/components/Checkbox.tsx
+++ b/src/components/Checkbox.tsx
@@ -6,8 +6,9 @@ import PoppinsText from './PoppinsText';
interface CheckboxProps {
checked: boolean;
- onChange: (checked: boolean) => void;
+ onChange: (checked: boolean, value: string) => void;
label?: string;
+ value: string;
disabled?: boolean;
color?: string;
size?: number;
@@ -18,6 +19,7 @@ const Checkbox: React.FC = ({
checked,
onChange,
label,
+ value,
disabled = false,
color = Colors.primary,
size = 24,
@@ -25,7 +27,7 @@ const Checkbox: React.FC = ({
}) => {
const handlePress = () => {
if (!disabled) {
- onChange(!checked);
+ onChange(!checked, value);
}
};
diff --git a/src/components/Coupon.tsx b/src/components/Coupon.tsx
new file mode 100644
index 0000000..2d98bcb
--- /dev/null
+++ b/src/components/Coupon.tsx
@@ -0,0 +1,154 @@
+// src/components/Coupon.tsx
+import React, { useState } from 'react';
+import { View, StyleSheet } from 'react-native';
+import Input from './Input';
+import Button from './Button';
+import PoppinsText from './PoppinsText';
+import { Colors, FontSizes } from '../styles/theme';
+import { CouponService } from '../services/coupon';
+import { useDispatch, useSelector } from 'react-redux';
+import { RootState } from '../redux/store';
+import {
+ setCouponDiscount,
+ setCouponApplied,
+ setLastAppliedCoupon,
+} from '../redux/slices/checkoutSlice';
+
+interface CouponProps {
+ onApplyCoupon: (discount: number) => void;
+ onCouponApplied: () => void;
+ isLoading?: boolean;
+}
+
+const Coupon: React.FC = ({ isLoading = false }) => {
+ const dispatch = useDispatch();
+ const lastAppliedCoupon = useSelector(
+ (state: RootState) => state.checkout.lastAppliedCoupon,
+ );
+
+ const [couponCode, setCouponCode] = useState(lastAppliedCoupon || '');
+ const [validationMessage, setValidationMessage] = useState(
+ null,
+ );
+ const [loading, setLoading] = useState(false);
+
+ const handleApply = async () => {
+ const code = couponCode.trim();
+ if (!code) return;
+
+ setLoading(true);
+ setValidationMessage(null);
+
+ try {
+ const response = await CouponService.validateCoupon(code);
+ const { discount, expirationDate } = response;
+ if (new Date(expirationDate) < new Date()) {
+ setValidationMessage('El cupón ha expirado.');
+ } else {
+ setValidationMessage(
+ `Cupón válido. Se aplicó ${discount}% de descuento.`,
+ );
+ dispatch(setCouponDiscount(discount));
+ dispatch(setCouponApplied(true));
+ dispatch(setLastAppliedCoupon(code));
+ }
+ } catch (err: unknown) {
+ const msg = (err instanceof Error && err.message.toLowerCase?.()) ?? '';
+ if (
+ typeof msg === 'string' &&
+ (msg.includes('not found') || msg.includes('no encontrado'))
+ ) {
+ setValidationMessage('Cupón inválido.');
+ } else {
+ setValidationMessage('Error al validar el cupón.');
+ console.error('Error en validateCoupon:', err);
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleChange = (text: string) => {
+ setCouponCode(text);
+ };
+
+ return (
+
+ Ingresa el Cupón
+
+
+
+
+
+
+
+ {validationMessage && (
+
+ {validationMessage}
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ marginVertical: 8,
+ },
+ label: {
+ color: Colors.textMain,
+ fontSize: FontSizes.b4.size,
+ marginBottom: 4,
+ },
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ inputWrapper: {
+ flex: 0.8,
+ marginRight: 8,
+ borderColor: Colors.gray_100,
+ borderWidth: 1,
+ borderRadius: 8,
+ height: 44,
+ paddingHorizontal: 10,
+ justifyContent: 'center',
+ backgroundColor: Colors.menuWhite,
+ },
+ button: {
+ flex: 0.2,
+ height: 44,
+ paddingHorizontal: 16,
+ },
+ validationMessage: {
+ marginTop: 8,
+ fontSize: FontSizes.b4.size,
+ },
+});
+
+export default Coupon;
diff --git a/src/components/CustomerAvatar.tsx b/src/components/CustomerAvatar.tsx
new file mode 100644
index 0000000..5710657
--- /dev/null
+++ b/src/components/CustomerAvatar.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { View, StyleSheet, Image } from 'react-native';
+import PoppinsText from './PoppinsText';
+import { Colors } from '../styles/theme';
+
+type CustomerAvatarProps = {
+ firstName: string;
+ lastName: string;
+ profilePicture?: string; // Nueva propiedad para la imagen de perfil
+ scale?: number; // Propiedad opcional para definir el tamaño del avatar
+};
+
+const CustomerAvatar: React.FC = ({
+ firstName,
+ lastName,
+ profilePicture,
+ scale = 32,
+}) => {
+ const getInitials = (firstName: string, lastName: string): string => {
+ return `${firstName.charAt(0).toUpperCase()}${lastName.charAt(0).toUpperCase()}`;
+ };
+
+ const initials = getInitials(firstName, lastName);
+
+ // Cálculo dinámico de tamaño y fuentes
+ const avatarSize = scale;
+ const fontSize = avatarSize / 2.5;
+ const lineHeight = fontSize + 1;
+ const marginTop = avatarSize / 10;
+
+ return (
+
+ {profilePicture ? (
+
+ ) : (
+
+ {initials}
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ avatar: {
+ backgroundColor: Colors.primary,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ avatarImage: {
+ resizeMode: 'cover',
+ },
+ initials: {
+ color: Colors.textWhite,
+ textAlign: 'center',
+ },
+});
+
+export default CustomerAvatar;
diff --git a/src/components/DeliveryMap.tsx b/src/components/DeliveryMap.tsx
new file mode 100644
index 0000000..2ce4eee
--- /dev/null
+++ b/src/components/DeliveryMap.tsx
@@ -0,0 +1,255 @@
+import React, { useEffect, useState } from 'react';
+import MapView, { Marker, Polyline } from 'react-native-maps';
+import { StyleSheet, View, ActivityIndicator, ViewStyle } from 'react-native';
+import { Config } from '../config';
+import { Colors } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import { MapPinIcon, UserIcon } from 'react-native-heroicons/solid';
+import { FontAwesome5 } from '@expo/vector-icons';
+
+interface DeliveryMapProps {
+ deliveryState: number;
+ branchLocation: { latitude: number; longitude: number };
+ customerLocation: { latitude: number; longitude: number };
+ deliveryLocation: { latitude: number; longitude: number } | null; // NUEVO
+ style?: ViewStyle;
+}
+
+const DeliveryMap: React.FC = ({
+ deliveryState,
+ branchLocation,
+ customerLocation,
+ deliveryLocation,
+ style,
+}) => {
+ const [routeCoordinates, setRouteCoordinates] = useState<
+ { latitude: number; longitude: number }[]
+ >([]);
+ const [customerRouteCoordinates, setCustomerRouteCoordinates] = useState<
+ { latitude: number; longitude: number }[]
+ >([]);
+ const [, setIsLoading] = useState(true); // Estado de carga
+
+ // Obtener la ruta desde Google Maps Directions API
+ const fetchRoute = async (
+ originLat: number,
+ originLng: number,
+ destLat: number,
+ destLng: number,
+ setRoute: React.Dispatch<
+ React.SetStateAction<{ latitude: number; longitude: number }[]>
+ >,
+ ) => {
+ try {
+ const response = await fetch(
+ `https://maps.googleapis.com/maps/api/directions/json?origin=${originLat},${originLng}&destination=${destLat},${destLng}&key=${Config.googleMapsApiKey}`,
+ );
+ const data = await response.json();
+
+ if (data.routes && data.routes.length > 0) {
+ const points = decodePolyline(data.routes[0].overview_polyline.points);
+ setRoute(points);
+ } else {
+ console.error('No se pudo obtener la ruta.');
+ }
+ } catch (error) {
+ console.error('Error al obtener la ruta:', error);
+ }
+ };
+
+ // Obtener la ruta hacia la sucursal
+ useEffect(() => {
+ if (
+ deliveryLocation &&
+ branchLocation.latitude !== 0 &&
+ branchLocation.longitude !== 0
+ ) {
+ fetchRoute(
+ deliveryLocation.latitude,
+ deliveryLocation.longitude,
+ branchLocation.latitude,
+ branchLocation.longitude,
+ setRouteCoordinates,
+ );
+ }
+ }, [deliveryLocation, branchLocation]);
+
+ // Obtener la ruta hacia el cliente cuando el estado sea el correspondiente
+ useEffect(() => {
+ if (
+ deliveryState >= 3 &&
+ deliveryLocation &&
+ customerLocation.latitude !== 0 &&
+ customerLocation.longitude !== 0
+ ) {
+ fetchRoute(
+ deliveryLocation.latitude,
+ deliveryLocation.longitude,
+ customerLocation.latitude,
+ customerLocation.longitude,
+ setCustomerRouteCoordinates,
+ );
+
+ // Limpiar la ruta hacia la sucursal
+ setRouteCoordinates([]);
+ }
+ }, [deliveryState, deliveryLocation, customerLocation]);
+
+ // Verificar si todos los datos necesarios están disponibles
+ useEffect(() => {
+ if (
+ deliveryLocation &&
+ branchLocation.latitude !== 0 &&
+ branchLocation.longitude !== 0 &&
+ customerLocation.latitude !== 0 &&
+ customerLocation.longitude !== 0
+ ) {
+ setIsLoading(false); // Finalizar la carga cuando todos los datos estén disponibles
+ }
+ }, [deliveryLocation, branchLocation, customerLocation]);
+
+ // Decodificar la polyline de Google Maps
+ const decodePolyline = (encoded: string) => {
+ const points: { latitude: number; longitude: number }[] = [];
+ let index = 0;
+ const len = encoded.length;
+ let lat = 0,
+ lng = 0;
+
+ while (index < len) {
+ let b,
+ shift = 0,
+ result = 0;
+ do {
+ b = encoded.charCodeAt(index++) - 63;
+ result |= (b & 0x1f) << shift;
+ shift += 5;
+ } while (b >= 0x20);
+ const dlat = result & 1 ? ~(result >> 1) : result >> 1;
+ lat += dlat;
+
+ shift = 0;
+ result = 0;
+ do {
+ b = encoded.charCodeAt(index++) - 63;
+ result |= (b & 0x1f) << shift;
+ shift += 5;
+ } while (b >= 0x20);
+ const dlng = result & 1 ? ~(result >> 1) : result >> 1;
+ lng += dlng;
+
+ points.push({
+ latitude: lat / 1e5,
+ longitude: lng / 1e5,
+ });
+ }
+
+ return points;
+ };
+
+ if (
+ // Solo espera si faltan datos críticos
+ branchLocation.latitude === 0 ||
+ branchLocation.longitude === 0 ||
+ customerLocation.latitude === 0 ||
+ customerLocation.longitude === 0
+ ) {
+ return (
+
+
+
+ );
+ }
+
+ // Mostrar el mapa aunque deliveryLocation sea null
+ return (
+
+
+ {/* Mostrar la ubicación del delivery si existe */}
+ {deliveryLocation && (
+
+
+
+ )}
+
+ {/* Mostrar la ubicación de la sucursal */}
+ {branchLocation.latitude !== 0 && branchLocation.longitude !== 0 && (
+
+
+
+ )}
+
+ {/* Mostrar la ubicación del cliente */}
+ {customerLocation.latitude !== 0 &&
+ customerLocation.longitude !== 0 && (
+
+
+
+ )}
+
+ {/* Mostrar la ruta hacia la sucursal */}
+ {routeCoordinates.length > 0 && (
+
+ )}
+
+ {/* Mostrar la ruta hacia el cliente */}
+ {deliveryState >= 3 && customerRouteCoordinates.length > 0 && (
+
+ )}
+
+ {!deliveryLocation && (
+
+
+ Esperando ubicación del repartidor...
+
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1, // Permitir que el mapa ocupe todo el espacio disponible
+ },
+ loadingContainer: {
+ flex: 1, // Asegura que el indicador de carga ocupe todo el espacio disponible
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+});
+
+export default DeliveryMap;
diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx
index 5e554fc..054a78d 100644
--- a/src/components/Dropdown.tsx
+++ b/src/components/Dropdown.tsx
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
import {
View,
TouchableOpacity,
- TouchableHighlight,
StyleSheet,
+ TouchableWithoutFeedback, // Importa TouchableWithoutFeedback
} from 'react-native';
import { ChevronDownIcon } from 'react-native-heroicons/outline';
import PoppinsText from './PoppinsText';
@@ -16,6 +16,7 @@ interface DropdownProps {
placeholder?: string;
options: string[];
border?: borderType;
+ borderColor?: string;
onSelect: (option: string) => void;
}
@@ -24,6 +25,7 @@ const Dropdown: React.FC = ({
placeholder,
options,
border = 'default',
+ borderColor = Colors.primary,
onSelect,
}) => {
const [isOpen, setIsOpen] = useState(false);
@@ -32,7 +34,9 @@ const Dropdown: React.FC = ({
const handleSelect = (option: string) => {
setSelectedOption(option);
onSelect(option);
- setIsOpen(false);
+ setTimeout(() => {
+ setIsOpen(false);
+ }, 150); // Retraso de 200ms antes de cerrar el menú
};
const getBorderWidth = () => {
@@ -51,7 +55,10 @@ const Dropdown: React.FC = ({
{label && {label}}
setIsOpen(!isOpen)}
- style={[{ borderWidth: getBorderWidth() }, styles.Selectbtn]}
+ style={[
+ { borderWidth: getBorderWidth(), borderColor: borderColor },
+ styles.Selectbtn,
+ ]}
>
{selectedOption || placeholder}
@@ -61,28 +68,36 @@ const Dropdown: React.FC = ({
{isOpen && (
{options.map((option, index) => (
- handleSelect(option)}
- style={[
- {
- backgroundColor:
- option === selectedOption ? Colors.primary : '',
- },
- styles.option,
- ]}
- activeOpacity={1}
- underlayColor={Colors.primary}
+ onPress={() => handleSelect(option)} // Selecciona la opción al presionar
>
-
- {option}
-
-
+
+ {option}
+
+
+
))}
)}
@@ -93,7 +108,6 @@ const Dropdown: React.FC = ({
const styles = StyleSheet.create({
container: {
width: '100%',
- paddingHorizontal: 24,
paddingVertical: 12,
},
label: {
@@ -103,11 +117,11 @@ const styles = StyleSheet.create({
Selectbtn: {
flexDirection: 'row',
alignItems: 'center',
- borderColor: Colors.primary,
borderRadius: 10,
padding: 10,
paddingHorizontal: 15,
fontSize: FontSizes.label.size,
+ backgroundColor: Colors.textWhite,
},
selectedText: {
fontSize: 16,
@@ -116,9 +130,10 @@ const styles = StyleSheet.create({
},
optionsContainer: {
marginTop: 15,
- borderColor: '#ccc',
+ borderColor: Colors.gray_100,
borderRadius: 5,
borderWidth: 1,
+ backgroundColor: Colors.textWhite,
},
option: {
padding: 10,
diff --git a/src/components/FilterOptions.tsx b/src/components/FilterOptions.tsx
new file mode 100644
index 0000000..05136e1
--- /dev/null
+++ b/src/components/FilterOptions.tsx
@@ -0,0 +1,257 @@
+import React, { useRef, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ PanResponder,
+ Animated,
+ Dimensions,
+ TouchableWithoutFeedback,
+ Modal,
+ ScrollView,
+} from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import { AdjustmentsHorizontalIcon } from 'react-native-heroicons/solid';
+import PoppinsText from './PoppinsText';
+import Button from './Button';
+import MultiSlider from '@ptomasroos/react-native-multi-slider';
+
+type FilterOptionsProps = {
+ visible: boolean;
+ onClose: () => void;
+ onClearFilters: () => void;
+ onApplyFilters: (priceRange: { min: number; max: number }) => void;
+ children: React.ReactNode;
+};
+
+const FilterOptions: React.FC = ({
+ visible,
+ onClose,
+ onClearFilters,
+ onApplyFilters,
+ children,
+}) => {
+ const screenHeight = Dimensions.get('screen').height;
+ const panY = useRef(new Animated.Value(screenHeight)).current;
+ const scrollOffset = useRef(0);
+
+ const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
+
+ const resetPositionAnim = Animated.timing(panY, {
+ toValue: 0,
+ duration: 300,
+ useNativeDriver: true,
+ });
+
+ const closeAnim = Animated.timing(panY, {
+ toValue: screenHeight,
+ duration: 500,
+ useNativeDriver: true,
+ });
+
+ const panResponders = useRef(
+ PanResponder.create({
+ onStartShouldSetPanResponder: (_, gestureState) => {
+ return scrollOffset.current <= 0 && gestureState.dy > 0;
+ },
+ onMoveShouldSetPanResponder: (_, gestureState) => {
+ return scrollOffset.current <= 0 && gestureState.dy > 0;
+ },
+ onPanResponderMove: (_, gestureState) => {
+ const newY = Math.max(gestureState.dy, 0);
+ panY.setValue(newY);
+ },
+ onPanResponderRelease: (_, gestureState) => {
+ if (gestureState.dy > 200 || gestureState.vy > 1) {
+ return closeSheet();
+ }
+ return resetPositionAnim.start();
+ },
+ }),
+ ).current;
+
+ React.useEffect(() => {
+ if (visible) {
+ resetPositionAnim.start();
+ } else {
+ closeAnim.start();
+ }
+ }, [visible]);
+
+ const closeSheet = () => {
+ closeAnim.start(() => onClose());
+ };
+
+ const handleApplyFilters = () => {
+ onApplyFilters(priceRange);
+ closeSheet();
+ };
+
+ const handleClearFilters = () => {
+ setPriceRange({ min: 0, max: 1000 }); // Reiniciar el rango de precios
+ onClearFilters(); // Llamar a la función pasada como prop
+ };
+
+ return (
+
+ {}}>
+
+
+
+
+
+ {/* Cabecera fija */}
+
+
+ Filtros
+
+
+
+
+ Limpiar
+
+
+
+
+ {/* Contenido desplazable */}
+ {
+ scrollOffset.current = event.nativeEvent.contentOffset.y;
+ }}
+ scrollEventThrottle={16}
+ >
+ {children}
+
+ {/* Filtro de precios */}
+
+
+ Rango de precios
+
+
+ setPriceRange({ min: values[0], max: values[1] })
+ }
+ selectedStyle={{ backgroundColor: Colors.primary }}
+ unselectedStyle={{ backgroundColor: Colors.gray_500 }}
+ markerStyle={{
+ backgroundColor: Colors.primary,
+ height: 20,
+ width: 20,
+ }}
+ containerStyle={{ marginHorizontal: 10 }}
+ />
+
+
+ Min: ${priceRange.min}
+
+
+ Max: ${priceRange.max}
+
+
+
+
+
+ {/* Botón para aplicar filtros */}
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ overlay: {
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ flex: 1,
+ justifyContent: 'flex-end',
+ },
+ container: {
+ backgroundColor: Colors.bgColor,
+ borderTopRightRadius: 12,
+ borderTopLeftRadius: 12,
+ maxHeight: '80%',
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderBottomWidth: 1,
+ borderColor: Colors.gray_100,
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ justifyContent: 'center',
+ },
+ headerTitleContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ headerTitle: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.primary,
+ marginRight: 4,
+ },
+ clearButtonContainer: {
+ position: 'absolute',
+ right: 20,
+ },
+ clearText: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.iconMainDefault,
+ },
+ scrollContent: {
+ paddingBottom: 20,
+ paddingHorizontal: 20,
+ },
+ priceFilterContainer: {
+ marginTop: 20,
+ },
+ priceFilterTitle: {
+ fontSize: FontSizes.s1.size,
+ lineHeight: FontSizes.s1.lineHeight,
+ marginBottom: 8,
+ },
+ priceLabels: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: 8,
+ },
+ priceLabel: {
+ fontSize: 14,
+ color: Colors.textLowContrast,
+ },
+ applyButtonContainer: {
+ padding: 20,
+ borderTopWidth: 1,
+ borderColor: Colors.gray_100,
+ },
+});
+
+export default FilterOptions;
diff --git a/src/components/HistoryMap.tsx b/src/components/HistoryMap.tsx
new file mode 100644
index 0000000..0f0d229
--- /dev/null
+++ b/src/components/HistoryMap.tsx
@@ -0,0 +1,180 @@
+import React, { useEffect, useState } from 'react';
+import MapView, { Marker, Polyline } from 'react-native-maps';
+import { StyleSheet, View, Alert, ViewStyle } from 'react-native';
+import { Config } from '../config';
+import { Colors } from '../styles/theme';
+
+import { MapPinIcon, UserIcon } from 'react-native-heroicons/solid';
+import { FontAwesome5 } from '@expo/vector-icons';
+
+interface HistoryMapProps {
+ deliveryLocation: { latitude: number; longitude: number };
+ branchLocation: { latitude: number; longitude: number };
+ customerLocation: { latitude: number; longitude: number };
+ style?: ViewStyle; // Permitir estilos personalizados
+}
+
+const HistoryMap: React.FC = ({
+ deliveryLocation,
+ branchLocation,
+ customerLocation,
+ style, // Recibir el estilo como prop
+}) => {
+ const [routeToBranch, setRouteToBranch] = useState<
+ { latitude: number; longitude: number }[]
+ >([]);
+ const [routeToCustomer, setRouteToCustomer] = useState<
+ { latitude: number; longitude: number }[]
+ >([]);
+
+ // Obtener las rutas al cargar el componente
+ useEffect(() => {
+ const fetchRoutes = async () => {
+ try {
+ // Ruta desde el delivery hasta la sucursal
+ await fetchRoute(
+ deliveryLocation.latitude,
+ deliveryLocation.longitude,
+ branchLocation.latitude,
+ branchLocation.longitude,
+ setRouteToBranch,
+ );
+
+ // Ruta desde la sucursal hasta el cliente
+ await fetchRoute(
+ branchLocation.latitude,
+ branchLocation.longitude,
+ customerLocation.latitude,
+ customerLocation.longitude,
+ setRouteToCustomer,
+ );
+ } catch (error) {
+ console.error('Error al obtener las rutas:', error);
+ Alert.alert('Error', 'Hubo un problema al obtener las rutas.');
+ }
+ };
+
+ fetchRoutes();
+ }, [deliveryLocation, branchLocation, customerLocation]);
+
+ // Obtener la ruta desde Google Maps Directions API
+ const fetchRoute = async (
+ originLat: number,
+ originLng: number,
+ destLat: number,
+ destLng: number,
+ setRoute: React.Dispatch<
+ React.SetStateAction<{ latitude: number; longitude: number }[]>
+ >,
+ ) => {
+ try {
+ const response = await fetch(
+ `https://maps.googleapis.com/maps/api/directions/json?origin=${originLat},${originLng}&destination=${destLat},${destLng}&key=${Config.googleMapsApiKey}`,
+ );
+ const data = await response.json();
+
+ if (data.routes && data.routes.length > 0) {
+ const points = decodePolyline(data.routes[0].overview_polyline.points);
+ setRoute(points);
+ } else {
+ console.error('No se pudo obtener la ruta.');
+ }
+ } catch (error) {
+ console.error('Error al obtener la ruta:', error);
+ }
+ };
+
+ // Decodificar la polyline de Google Maps
+ const decodePolyline = (encoded: string) => {
+ const points: { latitude: number; longitude: number }[] = [];
+ let index = 0;
+ const len = encoded.length;
+ let lat = 0,
+ lng = 0;
+
+ while (index < len) {
+ let b,
+ shift = 0,
+ result = 0;
+ do {
+ b = encoded.charCodeAt(index++) - 63;
+ result |= (b & 0x1f) << shift;
+ shift += 5;
+ } while (b >= 0x20);
+ const dlat = result & 1 ? ~(result >> 1) : result >> 1;
+ lat += dlat;
+
+ shift = 0;
+ result = 0;
+ do {
+ b = encoded.charCodeAt(index++) - 63;
+ result |= (b & 0x1f) << shift;
+ shift += 5;
+ } while (b >= 0x20);
+ const dlng = result & 1 ? ~(result >> 1) : result >> 1;
+ lng += dlng;
+
+ points.push({
+ latitude: lat / 1e5,
+ longitude: lng / 1e5,
+ });
+ }
+
+ return points;
+ };
+
+ return (
+
+
+ {/* Mostrar la ubicación del delivery */}
+
+
+
+
+ {/* Mostrar la ubicación de la sucursal */}
+
+
+
+
+ {/* Mostrar la ubicación del cliente */}
+
+
+
+
+ {/* Mostrar la ruta hacia la sucursal */}
+ {routeToBranch.length > 0 && (
+
+ )}
+
+ {/* Mostrar la ruta hacia el cliente */}
+ {routeToCustomer.length > 0 && (
+
+ )}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1, // Permitir que el mapa ocupe todo el espacio disponible
+ },
+});
+
+export default HistoryMap;
diff --git a/src/components/HistoryOrderCard.tsx b/src/components/HistoryOrderCard.tsx
new file mode 100644
index 0000000..e70d09c
--- /dev/null
+++ b/src/components/HistoryOrderCard.tsx
@@ -0,0 +1,163 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+import {
+ MapPinIcon,
+ CalendarDaysIcon,
+ BuildingStorefrontIcon,
+} from 'react-native-heroicons/outline';
+import { CheckCircleIcon } from 'react-native-heroicons/solid';
+import Badge from './Badge';
+import CustomerAvatar from './CustomerAvatar';
+import Button from './Button';
+import PoppinsText from './PoppinsText';
+import { Colors, FontSizes } from '../styles/theme';
+
+interface HistoryOrderCardProps {
+ orderCode: string;
+ orderType: 'pedido' | 'reubicación';
+ address: string;
+ branch: string;
+ estimatedTime: string;
+ elapsedTime: string;
+ completionTime: string;
+ userName: string;
+ profilePicture?: string; // Nueva propiedad para la imagen de perfil
+ onViewDetails: () => void;
+}
+
+const HistoryOrderCard: React.FC = ({
+ orderCode,
+ orderType,
+ address,
+ branch,
+ estimatedTime,
+ elapsedTime,
+ completionTime,
+ userName,
+ profilePicture,
+ onViewDetails,
+}) => {
+ // Separar el nombre y apellido del cliente
+ const [firstName, lastName] = userName.split(' ');
+
+ return (
+
+ {/* Header */}
+
+
+ Hace {`${elapsedTime}, ${completionTime}`}
+
+
+
+ {/* Order Code */}
+
+
+
+ {orderType === 'pedido' ? 'Orden' : 'Reubicación'} #{orderCode}
+
+
+
+ {/* Details */}
+
+
+
+ {address}
+
+
+
+ {userName}
+
+
+
+
+ Buscar en: {branch}
+
+
+
+
+
+ Fecha de entrega: {estimatedTime}
+
+
+
+
+ {/* Button */}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 8,
+ padding: 16,
+ marginBottom: 16,
+ shadowColor: Colors.textLowContrast,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ width: '100%',
+ minHeight: 280,
+ flexGrow: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ orderCodeContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ orderCodeText: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.primary,
+ marginLeft: 8,
+ },
+ elapsedTime: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.gray_500,
+ },
+ details: {
+ marginBottom: 4,
+ },
+ detailRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ detailText: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.textMain,
+ marginLeft: 8,
+ flexWrap: 'wrap', // Permite que el texto se ajuste en varias líneas
+ maxWidth: '90%', // Limita el ancho del texto al 90% del contenedor
+ textAlign: 'left',
+ },
+ button: {
+ marginBottom: 8,
+ },
+});
+
+export default HistoryOrderCard;
diff --git a/src/components/Input.tsx b/src/components/Input.tsx
index 5193d76..0af223c 100644
--- a/src/components/Input.tsx
+++ b/src/components/Input.tsx
@@ -8,6 +8,7 @@ import {
} from 'react-native-heroicons/outline';
import { Colors, FontSizes } from '../styles/theme';
import PoppinsText from './PoppinsText';
+import { validateEmail, validatePassword } from '../utils/validators';
type FieldType = 'text' | 'number' | 'email' | 'password' | 'textarea';
type BorderType = 'none' | 'default' | 'parcial' | 'double';
@@ -55,17 +56,27 @@ const Input: React.FC = ({
setIvalue(value);
}, [value]);
+ const handleValidation = (input: string, type: 'email' | 'password') => {
+ if (type === 'email') {
+ return validateEmail(input);
+ }
+ if (type === 'password') {
+ return validatePassword(input);
+ }
+ return false;
+ };
+
const validateInput = (input: string) => {
if (useDefaultValidation) {
switch (fieldType) {
case 'text':
return input.length > 0;
case 'email':
- return validateEmail(input);
+ return handleValidation(input, 'email');
case 'number':
return !isNaN(Number(input));
case 'password':
- return validatePassword(input);
+ return handleValidation(input, 'password');
default:
return true;
}
@@ -113,15 +124,6 @@ const Input: React.FC = ({
}
};
- const validatePassword = (input: string) => {
- return input.length >= 8;
- };
-
- const validateEmail = (input: string) => {
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- return emailRegex.test(input);
- };
-
const showPass = () => {
setShowPassword(!showPassword);
};
@@ -144,7 +146,7 @@ const Input: React.FC = ({
backgroundColor: backgroundColor
? backgroundColor
: !isEditable
- ? Colors.disableText
+ ? Colors.gray_100
: 'transparent',
},
]}
diff --git a/src/components/LocationSelector.tsx b/src/components/LocationSelector.tsx
new file mode 100644
index 0000000..b1fade2
--- /dev/null
+++ b/src/components/LocationSelector.tsx
@@ -0,0 +1,217 @@
+import { useEffect, useState } from 'react';
+import { View, StyleSheet, TouchableOpacity } from 'react-native';
+import { useRouter } from 'expo-router';
+import Dropdown from './Dropdown';
+import PoppinsText from './PoppinsText';
+import { Colors } from '../styles/theme';
+import { BranchService } from '../services/branches';
+import { extractErrorMessage } from '../utils/errorHandler';
+import { MapPinIcon } from 'react-native-heroicons/solid';
+import BranchMapModal from './BranchMapModal';
+import { UserService } from '../services/user';
+
+type Branch = {
+ id: string;
+ name: string;
+ latitude: number;
+ longitude: number;
+ address: string;
+};
+
+const LocationSelector = ({
+ selectedOption,
+ onSelect,
+ setSelectedBranch,
+}: {
+ selectedOption: 'pickup' | 'delivery';
+ onSelect: (value: string | null) => void;
+ setSelectedBranch: (branch: Branch | null) => void;
+}) => {
+ const router = useRouter();
+ const [pickupBranches, setPickupBranches] = useState([]);
+ const [deliveryAddresses, setDeliveryAddresses] = useState<
+ { id: string; address: string }[]
+ >([]);
+ const [dropdownKey, setDropdownKey] = useState(0);
+ const [modalVisible, setModalVisible] = useState(false);
+ const [selectedBranch, setSelectedBranchState] = useState(
+ null,
+ );
+ const [noAddresses, setNoAddresses] = useState(false);
+
+ useEffect(() => {
+ const resetState = async () => {
+ setDropdownKey((prev) => prev + 1);
+ onSelect(null);
+ setSelectedBranchState(null);
+
+ if (selectedOption === 'pickup') {
+ try {
+ const response = await BranchService.findAll({});
+
+ // Mapear las sucursales al formato esperado
+ const branches = response.results.map((branch) => ({
+ id: branch.id,
+ name: branch.name,
+ latitude: branch.latitude,
+ longitude: branch.longitude,
+ address: branch.address,
+ }));
+
+ setPickupBranches(branches);
+ } catch (error) {
+ console.error(
+ 'Error al obtener sucursales:',
+ extractErrorMessage(error),
+ );
+ setPickupBranches([]); // Asegúrate de limpiar el estado en caso de error
+ }
+ } else if (selectedOption === 'delivery') {
+ try {
+ const response = await UserService.getUserDirections();
+
+ if (!response.success || response.data.length === 0) {
+ setNoAddresses(true);
+ setDeliveryAddresses([]);
+ } else {
+ setNoAddresses(false);
+ const addresses = response.data.map((address) => ({
+ id: address.id,
+ address: address.adress,
+ }));
+ setDeliveryAddresses(addresses);
+ }
+ } catch (error) {
+ console.error(
+ 'Error al obtener direcciones:',
+ extractErrorMessage(error),
+ );
+ setNoAddresses(true);
+ setDeliveryAddresses([]);
+ }
+ }
+ };
+
+ resetState();
+ }, [selectedOption]);
+
+ const options =
+ selectedOption === 'pickup'
+ ? pickupBranches.map((branch) => `${branch.name} - ${branch.address}`)
+ : deliveryAddresses.map((item) => item.address);
+
+ return (
+
+
+ {selectedOption === 'pickup'
+ ? 'Seleccione la sucursal'
+ : noAddresses
+ ? 'No se encontraron direcciones registradas'
+ : 'Seleccione la dirección de entrega'}
+
+
+ {selectedOption === 'pickup' && pickupBranches.length > 0 && (
+ {
+ const branch = pickupBranches.find(
+ (b) => `${b.name} - ${b.address}` === val,
+ );
+ setSelectedBranch(branch || null);
+ setSelectedBranchState(branch || null);
+ onSelect(branch ? branch.id : null);
+ }}
+ borderColor={Colors.gray_100}
+ />
+ )}
+
+ {!noAddresses && selectedOption === 'delivery' && (
+ {
+ const selectedAddress = deliveryAddresses.find(
+ (item) => item.address === val,
+ );
+ onSelect(selectedAddress ? selectedAddress.id : null);
+ }}
+ borderColor={Colors.gray_100}
+ />
+ )}
+
+
+ selectedOption === 'pickup'
+ ? setModalVisible(true)
+ : router.push({
+ pathname: '/selectLocation',
+ params: { fromCheckout: 'true' },
+ })
+ }
+ disabled={selectedOption === 'pickup' && !selectedBranch}
+ >
+ {selectedOption === 'pickup' && (
+
+ )}
+
+ {selectedOption === 'pickup'
+ ? 'Ver Ubicación en el Mapa'
+ : 'Agregar nueva dirección'}
+
+
+
+ setModalVisible(false)}
+ branchName={selectedBranch?.name || null}
+ branchCoordinates={
+ selectedBranch
+ ? {
+ latitude: selectedBranch.latitude,
+ longitude: selectedBranch.longitude,
+ }
+ : null
+ }
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 20,
+ width: '100%',
+ },
+ label: {
+ marginBottom: 8,
+ color: Colors.textMain,
+ },
+ button: {
+ marginTop: 12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: Colors.primary,
+ },
+ textWithIcon: {
+ marginLeft: 6,
+ },
+ disabledText: {
+ color: Colors.gray_500,
+ },
+});
+
+export default LocationSelector;
diff --git a/src/components/MapViewComponent.tsx b/src/components/MapViewComponent.tsx
new file mode 100644
index 0000000..3fbc858
--- /dev/null
+++ b/src/components/MapViewComponent.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
+
+type MapViewComponentProps = {
+ initialRegion: {
+ latitude: number;
+ longitude: number;
+ latitudeDelta: number;
+ longitudeDelta: number;
+ };
+ markers?: {
+ latitude: number;
+ longitude: number;
+ title?: string;
+ description?: string;
+ }[];
+};
+
+const MapViewComponent: React.FC = ({
+ initialRegion,
+ markers = [],
+}) => {
+ return (
+
+
+ {markers.map((marker, index) => (
+
+ ))}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ map: {
+ ...StyleSheet.absoluteFillObject,
+ },
+});
+
+export default MapViewComponent;
diff --git a/src/components/OrderBadge.tsx b/src/components/OrderBadge.tsx
new file mode 100644
index 0000000..e6caedc
--- /dev/null
+++ b/src/components/OrderBadge.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from './PoppinsText';
+
+// Define los tipos para las props del componente
+type BadgeProps = {
+ status: string;
+};
+
+// Mapeo de estados en inglés a colores y etiquetas en español
+const STATUS_COLORS: Record = {
+ requested: Colors.semanticDanger,
+ ready_for_pickup: Colors.semanticInfo,
+ completed: Colors.semanticSuccess,
+ canceled: Colors.semanticDanger,
+ in_progress: Colors.secondaryGray,
+ approved: Colors.secondary,
+};
+
+const STATUS_LABELS: Record = {
+ requested: 'Pendiente',
+ ready_for_pickup: 'A Retirar',
+ completed: 'Entregado',
+ canceled: 'Cancelado',
+ in_progress: 'En Proceso',
+ approved: 'Aprobado',
+};
+
+const OrderBadge: React.FC = ({ status }) => {
+ // Normalizamos el estado a minúsculas para evitar problemas de case sensitivity
+ const normalizedStatus = status.toLowerCase();
+
+ // Obtener el color y texto correspondiente al estado
+ const backgroundColor =
+ STATUS_COLORS[normalizedStatus] || STATUS_COLORS.default;
+ const label = STATUS_LABELS[normalizedStatus] || STATUS_LABELS.default;
+
+ return (
+
+ {label}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ badge: {
+ padding: 5,
+ borderRadius: 5,
+ width: 80,
+ },
+ badgeText: {
+ color: Colors.textWhite,
+ fontSize: FontSizes.c1.size,
+ textAlign: 'center',
+ },
+});
+
+export default OrderBadge;
diff --git a/src/components/OrderCard.tsx b/src/components/OrderCard.tsx
new file mode 100644
index 0000000..67d196c
--- /dev/null
+++ b/src/components/OrderCard.tsx
@@ -0,0 +1,156 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+import {
+ MapPinIcon,
+ ClockIcon,
+ BuildingStorefrontIcon,
+} from 'react-native-heroicons/outline';
+import Badge from './Badge';
+import Button from './Button';
+import PoppinsText from './PoppinsText';
+import { Colors, FontSizes } from '../styles/theme';
+
+interface OrderCardProps {
+ orderCode: string;
+ orderType: 'pedido' | 'reubicación';
+ address: string;
+ branch: string;
+ estimatedTime: string;
+ elapsedTime: string;
+ deliveryStatus:
+ | 'assigned'
+ | 'waiting_confirmation'
+ | 'picked_up'
+ | 'in_route'; // Nueva prop
+ onTakeOrder: () => void;
+ onDiscardOrder?: () => void;
+}
+
+const OrderCard: React.FC = ({
+ orderCode,
+ orderType,
+ address,
+ branch,
+ estimatedTime,
+ elapsedTime,
+ deliveryStatus,
+ onTakeOrder,
+ onDiscardOrder,
+}) => {
+ return (
+
+ {/* Header */}
+
+
+ {orderType === 'pedido' ? 'Pedido' : 'Reubicación de productos'}
+
+ Hace {elapsedTime}
+
+
+ {/* Order Code */}
+
+ {orderType === 'pedido' ? 'Orden' : 'Reubicación'} #{orderCode}
+
+
+ {/* Details */}
+
+
+
+ {address}
+
+
+
+
+ Buscar en: {branch}
+
+
+
+
+
+ Tiempo estimado: {estimatedTime}
+
+
+
+
+ {/* Buttons */}
+
+
+ {deliveryStatus === 'assigned' && onDiscardOrder && (
+
+ )}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 8,
+ padding: 16,
+ marginBottom: 16,
+ shadowColor: Colors.textLowContrast,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ width: '100%',
+ minHeight: 250,
+ flexGrow: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ elapsedTime: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.gray_500,
+ },
+ orderCode: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.primary,
+ marginBottom: 8,
+ },
+ detailRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 10,
+ },
+ detailText: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.textMain,
+ marginLeft: 8,
+ flexWrap: 'wrap',
+ maxWidth: '90%',
+ textAlign: 'left',
+ },
+ button: {
+ marginBottom: 8,
+ },
+});
+
+export default OrderCard;
diff --git a/src/components/OrderSummary.tsx b/src/components/OrderSummary.tsx
new file mode 100644
index 0000000..5b441a0
--- /dev/null
+++ b/src/components/OrderSummary.tsx
@@ -0,0 +1,254 @@
+import React, { useState } from 'react';
+import { View, StyleSheet, Image, TouchableOpacity } from 'react-native';
+import { useCart } from '../hooks/useCart';
+import PoppinsText from './PoppinsText';
+import { Colors, FontSizes } from '../styles/theme';
+import type { CartItem } from '../redux/slices/cartSlice';
+import { ChevronDownIcon, ChevronUpIcon } from 'react-native-heroicons/outline';
+import { formatPrice } from '../utils/formatPrice';
+
+const OrderSummary = () => {
+ const { cartItems } = useCart();
+ const [isOpen, setIsOpen] = useState(false);
+
+ const subtotal = cartItems.reduce(
+ (sum, item) => sum + item.price * item.quantity,
+ 0,
+ );
+ const totalDiscount = cartItems.reduce(
+ (sum, item) =>
+ sum + item.price * item.quantity * ((item.discount ?? 0) / 100),
+ 0,
+ );
+ const total = subtotal - totalDiscount;
+
+ const renderItem = ({ item }: { item: CartItem }) => {
+ const discount = item.discount ?? 0;
+ const discountedPrice = item.price * (1 - discount / 100);
+ const totalDiscountedPrice = discountedPrice * item.quantity;
+ const totalOriginalPrice = item.price * item.quantity;
+
+ return (
+
+
+ {discount > 0 && (
+
+
+ -{discount}%
+
+
+ )}
+
+
+
+
+ {item.name}
+
+ ${formatPrice(totalDiscountedPrice)}
+
+
+
+ ${formatPrice(totalOriginalPrice)}
+
+
+ (${formatPrice(discountedPrice)} c/u)
+
+
+ Cantidad: {item.quantity}
+
+
+
+ );
+ };
+
+ return (
+
+ setIsOpen(!isOpen)}
+ >
+
+ Resumen del pedido
+
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+
+ {isOpen && (
+ <>
+
+ {cartItems
+ .filter((item) => item.quantity > 0)
+ .map((item) => (
+
+ {renderItem({ item })}
+
+ ))}
+
+
+
+
+ Subtotal
+
+ ${formatPrice(subtotal)}
+
+
+
+ Descuentos
+
+ -${formatPrice(totalDiscount)}
+
+
+
+ Total
+
+ ${formatPrice(total)}
+
+
+
+ >
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ dropdownHeaderContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ width: '100%',
+ height: 46,
+ borderWidth: 1,
+ borderColor: Colors.gray_100,
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 8,
+ paddingHorizontal: 12,
+ },
+ dropdownHeader: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ },
+ dropdownIcon: {
+ marginLeft: 8,
+ },
+ listContainer: {
+ paddingBottom: 16,
+ },
+ card: {
+ flexDirection: 'row',
+ borderRadius: 8,
+ marginBottom: 24,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.gray_100,
+ paddingBottom: 18,
+ },
+ imageContainer: {
+ width: 100,
+ height: 100,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 16,
+ },
+ detailsContainer: {
+ flex: 1,
+ },
+ productName: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ width: '50%',
+ },
+ productPrice: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.gray_500,
+ marginBottom: 2,
+ },
+ productTotalPrice: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 8,
+ },
+ productOriginalPrice: {
+ position: 'absolute',
+ right: 0,
+ top: 24,
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.gray_500,
+ textDecorationLine: 'line-through',
+ marginBottom: 8,
+ },
+ discountBadge: {
+ zIndex: 999,
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ backgroundColor: Colors.semanticWarning,
+ borderRadius: 50,
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ },
+ discountBadgeText: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.primary,
+ },
+ footer: {
+ paddingTop: 16,
+ },
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 8,
+ },
+ subtotalText: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ },
+ discountText: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.secondary,
+ },
+ ivaText: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ },
+ productImage: {
+ width: '100%',
+ height: '100%',
+ marginTop: 16,
+ },
+ productQuantity: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.textMain,
+ marginTop: 4,
+ },
+});
+
+export default OrderSummary;
diff --git a/src/components/PaymentInfoForm.tsx b/src/components/PaymentInfoForm.tsx
new file mode 100644
index 0000000..e8b9b73
--- /dev/null
+++ b/src/components/PaymentInfoForm.tsx
@@ -0,0 +1,284 @@
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
+import { View, StyleSheet } from 'react-native';
+import PoppinsText from './PoppinsText';
+import Input from './Input';
+import { FontSizes, Colors } from '../styles/theme';
+import Dropdown from './Dropdown';
+import { PharmaTech } from '@pharmatech/sdk';
+
+interface Props {
+ paymentMethod: 'CARD' | 'CASH' | 'BANK_TRANSFER' | 'MOBILE_PAYMENT' | null;
+ total: string;
+ onValidationChange: (isValid: boolean) => void;
+ onBankChange: (value: string) => void;
+ onReferenceChange: (value: string) => void;
+ onDocumentNumberChange: (value: string) => void;
+ onPhoneChange: (value: string) => void;
+}
+
+const PaymentInfoForm: React.FC = ({
+ paymentMethod,
+ total,
+ onValidationChange,
+ onBankChange,
+ onReferenceChange,
+ onDocumentNumberChange,
+ onPhoneChange,
+}) => {
+ const [bank, setBank] = useState('');
+ const [hasBankTouched, setHasBankTouched] = useState(false);
+ const [reference, setReference] = useState('');
+ const [documentNumber, setDocumentNumber] = useState('');
+ const [phone, setPhone] = useState('');
+ const [bankOptions, setBankOptions] = useState([]);
+
+ // Fetch bank options from SDK
+ useEffect(() => {
+ const fetchBanks = async () => {
+ try {
+ const sdk = PharmaTech.getInstance();
+ const banks = sdk.bank.findAll();
+ setBankOptions(banks);
+ } catch (error) {
+ console.error('Error fetching banks:', error);
+ }
+ };
+ fetchBanks();
+ }, []);
+
+ // Memoized handlers
+ const handleBankSelect = useCallback((val: string) => {
+ setBank(val);
+ setHasBankTouched(true);
+ }, []);
+
+ const handleReferenceChange = useCallback((val: string) => {
+ setReference(val.replace(/\D/g, ''));
+ }, []);
+
+ const handleDocumentNumberChange = useCallback((val: string) => {
+ setDocumentNumber(val.replace(/\D/g, '').slice(0, 8));
+ }, []);
+
+ const handlePhoneChange = useCallback((val: string) => {
+ setPhone(val.replace(/\D/g, '').slice(0, 11));
+ }, []);
+
+ // Sync up with parent callbacks
+ useEffect(() => {
+ onBankChange(bank);
+ }, [bank, onBankChange]);
+
+ useEffect(() => {
+ onReferenceChange(reference);
+ }, [reference, onReferenceChange]);
+
+ useEffect(() => {
+ onDocumentNumberChange(documentNumber);
+ }, [documentNumber, onDocumentNumberChange]);
+
+ useEffect(() => {
+ onPhoneChange(phone);
+ }, [phone, onPhoneChange]);
+
+ // Validation effect
+ useEffect(() => {
+ const isValid =
+ paymentMethod !== null &&
+ (paymentMethod === 'MOBILE_PAYMENT' || paymentMethod === 'BANK_TRANSFER')
+ ? bank.trim() !== '' &&
+ /^\d{4,}$/.test(reference) &&
+ reference.trim() !== '' &&
+ !/^0+$/.test(reference) &&
+ /^\d{7,8}$/.test(documentNumber) &&
+ !/^0+$/.test(documentNumber) &&
+ !/^0/.test(documentNumber) &&
+ /^\d{11}$/.test(phone)
+ : true;
+
+ onValidationChange(isValid);
+ }, [
+ bank,
+ reference,
+ documentNumber,
+ phone,
+ paymentMethod,
+ onValidationChange,
+ ]);
+
+ if (!paymentMethod) {
+ return null;
+ }
+
+ // Memoized props to avoid recreating objects
+ const staticInputProps = useMemo(
+ () => ({
+ isEditable: false,
+ backgroundColor: Colors.gray_100,
+ border: 'default' as const,
+ }),
+ [],
+ );
+
+ const editableInputProps = useMemo(
+ () => ({ backgroundColor: Colors.iconWhite }),
+ [],
+ );
+
+ return (
+
+ {(paymentMethod === 'MOBILE_PAYMENT' ||
+ paymentMethod === 'BANK_TRANSFER') && (
+ <>
+
+ Realiza el pago en la siguiente cuenta de Pharmatech
+
+
+ Debes hacer el pago del monto exacto, de lo contrario no se creará
+ la orden
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {paymentMethod === 'MOBILE_PAYMENT' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ Ingrese los datos para validar el pago
+
+
+
+ {hasBankTouched && bank === '' && (
+
+ Debe seleccionar un banco
+
+ )}
+
+
+ /^\d{4,}$/.test(val) && val.trim() !== '' && !/^0+$/.test(val)
+ }
+ showIcon={reference.length > 0}
+ useDefaultValidation={false}
+ {...editableInputProps}
+ />
+
+ /^\d{7,8}$/.test(val) &&
+ val.trim() !== '' &&
+ !/^0+$/.test(val) &&
+ !/^0/.test(val)
+ }
+ showIcon={documentNumber.length > 0}
+ useDefaultValidation={false}
+ {...editableInputProps}
+ />
+ /^\d{11}$/.test(val) && !/^0+$/.test(val)}
+ showIcon={phone.length > 0}
+ useDefaultValidation={false}
+ {...editableInputProps}
+ />
+ >
+ )}
+ {/* Si se requiere mostrar algo para CASH o CARD, agregar aquí */}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ marginTop: 20,
+ },
+ label: {
+ fontSize: FontSizes.b2.size,
+ color: Colors.textMain,
+ marginBottom: 10,
+ },
+ label1: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.gray_500,
+ marginBottom: 10,
+ },
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ gap: 12,
+ marginBottom: 18,
+ },
+ inputWrapper: {
+ flex: 1,
+ },
+ largeInput: {
+ flex: 1.5,
+ },
+ smallInput: {
+ flex: 1,
+ },
+});
+
+export default React.memo(PaymentInfoForm);
diff --git a/src/components/PaymentMethods.tsx b/src/components/PaymentMethods.tsx
new file mode 100644
index 0000000..b5c90cc
--- /dev/null
+++ b/src/components/PaymentMethods.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+import RadioButton from './RadioButton';
+import PoppinsText from './PoppinsText';
+import { Colors } from '../styles/theme';
+import { PaymentMethod } from '@pharmatech/sdk';
+import { useDispatch } from 'react-redux';
+import { setPaymentInfoValid } from '../redux/slices/checkoutSlice';
+
+type PaymentOptionValue =
+ | 'punto_de_venta'
+ | 'efectivo'
+ | 'transferencia'
+ | 'pago_movil';
+
+interface PaymentMethodsProps {
+ selectedOption: 'pickup' | 'delivery' | null;
+ selectedPayment: PaymentOptionValue | null;
+ setSelectedPayment: (value: PaymentOptionValue) => void;
+}
+
+const labelMap: Record = {
+ [PaymentMethod.CARD]: 'Punto de Venta',
+ [PaymentMethod.CASH]: 'Efectivo',
+ [PaymentMethod.BANK_TRANSFER]: 'Transferencia Bancaria',
+ [PaymentMethod.MOBILE_PAYMENT]: 'Pago Móvil',
+};
+
+const valueMap: Record = {
+ [PaymentMethod.CARD]: 'punto_de_venta',
+ [PaymentMethod.CASH]: 'efectivo',
+ [PaymentMethod.BANK_TRANSFER]: 'transferencia',
+ [PaymentMethod.MOBILE_PAYMENT]: 'pago_movil',
+};
+
+const ALL_METHODS: PaymentMethod[] = [
+ PaymentMethod.CARD,
+ PaymentMethod.CASH,
+ PaymentMethod.BANK_TRANSFER,
+ PaymentMethod.MOBILE_PAYMENT,
+];
+
+const PaymentMethods: React.FC = ({
+ selectedOption,
+ selectedPayment,
+ setSelectedPayment,
+}) => {
+ const dispatch = useDispatch();
+
+ // Filtramos según pickup vs delivery
+ const filtered = ALL_METHODS.filter((m) => {
+ if (selectedOption === 'pickup' && m === PaymentMethod.CASH) return false;
+ if (selectedOption === 'delivery' && m === PaymentMethod.CARD) return false;
+ return true;
+ });
+
+ // Cuando el usuario elige un método:
+ const onPaymentChange = (value: string) => {
+ const v = value as PaymentOptionValue;
+ setSelectedPayment(v);
+ // Marcamos el formulario de pago como válido para avanzar
+ dispatch(setPaymentInfoValid(true));
+ };
+
+ return (
+
+
+ Seleccione el método de pago
+
+
+
+ {filtered.map((m) => {
+ const uiValue = valueMap[m];
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: { width: '100%' },
+ title: { color: Colors.gray_500 },
+ divider: {
+ height: 1,
+ backgroundColor: Colors.gray_100,
+ marginVertical: 20,
+ },
+ optionSpacing: { marginBottom: 10, paddingLeft: 20 },
+});
+
+export default PaymentMethods;
diff --git a/src/components/PaymentStatusMessage.tsx b/src/components/PaymentStatusMessage.tsx
new file mode 100644
index 0000000..51748ba
--- /dev/null
+++ b/src/components/PaymentStatusMessage.tsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import {
+ CheckCircleIcon,
+ XCircleIcon,
+ ClockIcon,
+} from 'react-native-heroicons/outline';
+import theme from '../styles/theme';
+import { Text, View, StyleSheet } from 'react-native';
+
+type OrderStatus =
+ | 'requested'
+ | 'approved'
+ | 'ready_for_pickup'
+ | 'in_progress'
+ | 'canceled'
+ | 'completed';
+
+interface PaymentStatusMessageProps {
+ orderStatus: OrderStatus;
+ orderNumber: string;
+ userName: string;
+}
+
+const statusConfig: Record<
+ OrderStatus,
+ {
+ icon: React.ElementType;
+ color: string;
+ title: string;
+ message: (orderNumber: string, userName: string) => string;
+ }
+> = {
+ requested: {
+ icon: ClockIcon,
+ color: theme.Colors.semanticWarning,
+ title: 'Orden en espera',
+ message: () =>
+ `Estamos procesando tu orden. En un momento actualizaremos el estado de tu orden. Si tienes alguna duda, por favor contacta a nuestro equipo de soporte.`,
+ },
+ approved: {
+ icon: CheckCircleIcon,
+ color: theme.Colors.semanticSuccess,
+ title: 'Orden aprobada',
+ message: (_orderNumber, userName) =>
+ `¡Gracias por tu compra, ${userName}! Tu orden ha sido aprobada.`,
+ },
+ ready_for_pickup: {
+ icon: CheckCircleIcon,
+ color: theme.Colors.semanticSuccess,
+ title: 'Lista para recoger',
+ message: () =>
+ 'Tu pedido está listo para ser recogido en la sucursal seleccionada.',
+ },
+ in_progress: {
+ icon: ClockIcon,
+ color: theme.Colors.semanticWarning,
+ title: 'Orden en progreso',
+ message: () =>
+ 'Tu pedido está en progreso. Pronto comenzaremos a procesarlo.',
+ },
+ canceled: {
+ icon: XCircleIcon,
+ color: theme.Colors.semanticDanger,
+ title: 'Orden rechazada',
+ message: () =>
+ 'No pudimos procesar tu orden. Lamentamos informarte que hubo un problema al generar tu pedido.',
+ },
+ completed: {
+ icon: CheckCircleIcon,
+ color: theme.Colors.semanticSuccess,
+ title: 'Orden completada',
+ message: (_orderNumber, userName) =>
+ `¡Gracias por tu compra, ${userName}! Tu orden ha sido completada exitosamente.`,
+ },
+};
+
+const PaymentStatusMessage: React.FC = ({
+ orderStatus,
+ orderNumber,
+ userName,
+}) => {
+ const config = statusConfig[orderStatus] || statusConfig['requested'];
+ const Icon = config.icon;
+
+ return (
+
+
+
+
+
+
+ {config.title} #{orderNumber}
+
+
+ {config.message(orderNumber, userName)}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ alignSelf: 'center',
+ width: '90%',
+ padding: 16,
+ },
+ iconContainer: {
+ marginRight: 10,
+ },
+ textContainer: {
+ flex: 1,
+ },
+ title: {
+ fontSize: 20,
+ fontFamily: theme.Fonts.poppinsSemibold,
+ },
+ message: {
+ fontSize: 14,
+ fontFamily: theme.Fonts.poppinsRegular,
+ },
+});
+
+export default PaymentStatusMessage;
diff --git a/src/components/RadioCard.tsx b/src/components/RadioCard.tsx
new file mode 100644
index 0000000..d17f417
--- /dev/null
+++ b/src/components/RadioCard.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+
+interface RadioCardProps {
+ label: string;
+ icon: React.ReactNode;
+ selected: boolean;
+ onPress: () => void;
+}
+
+const RadioCard: React.FC = ({
+ label,
+ icon,
+ selected,
+ onPress,
+}) => {
+ return (
+
+
+ {selected && }
+
+
+ {label}
+
+ {icon}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ height: 70, // Altura estándar
+ borderRadius: 8,
+ paddingHorizontal: 12,
+ borderWidth: 1,
+ width: '100%', // Asegura que ocupe todo el ancho disponible
+ },
+ selectedContainer: {
+ borderColor: Colors.primary_600,
+ backgroundColor: '#ECEFFA', // Color primario con opacidad (20%)
+ },
+ unselectedContainer: {
+ borderColor: Colors.gray_100,
+ backgroundColor: Colors.secondaryWhite,
+ },
+ radioCircle: {
+ width: 16,
+ height: 16,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: Colors.gray_100,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ radioCircleInner: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: Colors.secondaryWhite, // Círculo interno blanco
+ },
+ label: {
+ flex: 1,
+ fontSize: FontSizes.b1.size,
+ fontFamily: 'Poppins_500Medium',
+ },
+ selectedLabel: {
+ color: Colors.textMain,
+ },
+ unselectedLabel: {
+ color: Colors.textLowContrast,
+ },
+ icon: {
+ marginLeft: 12,
+ },
+});
+
+export default RadioCard;
diff --git a/src/components/Return.tsx b/src/components/Return.tsx
new file mode 100644
index 0000000..df6d80e
--- /dev/null
+++ b/src/components/Return.tsx
@@ -0,0 +1,41 @@
+import { TouchableOpacity } from 'react-native';
+import { ChevronLeftIcon } from 'react-native-heroicons/outline';
+import PoppinsText from './PoppinsText';
+import { Colors, FontSizes } from '../styles/theme';
+import React from 'react';
+
+interface ReturnProps {
+ onClose: () => void;
+}
+
+const Return: React.FC = ({ onClose }) => {
+ return (
+
+
+
+ Volver
+
+
+ );
+};
+
+export default Return;
diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx
index 4da163b..20fc96c 100644
--- a/src/components/SearchInput.tsx
+++ b/src/components/SearchInput.tsx
@@ -1,7 +1,23 @@
-import React from 'react';
-import { View, TextInput, StyleSheet, TouchableOpacity } from 'react-native';
-import { MagnifyingGlassIcon, XMarkIcon } from 'react-native-heroicons/outline';
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ TextInput,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ Image,
+} from 'react-native';
+import {
+ ArrowRightIcon,
+ MagnifyingGlassIcon,
+ XMarkIcon,
+} from 'react-native-heroicons/outline';
import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from './PoppinsText';
+import { router } from 'expo-router';
+import { ProductService } from '../services/products';
+import { ProductPresentation } from '@pharmatech/sdk';
+import { formatPrice } from '../utils/formatPrice';
interface SearchInputProps {
placeholder?: string;
@@ -20,37 +36,198 @@ const SearchInput: React.FC = ({
style,
showClearButton = true,
}) => {
- return (
-
-
-
-
+ const [search, setSearch] = useState('');
+ const [result, setResult] = useState();
+
+ const handleSearch = () => {
+ if (search.trim()) {
+ router.push(`/search/${encodeURIComponent(search)}`); // Redirige a la misma ruta para ambas acciones
+ }
+ };
-
+ useEffect(() => {
+ const fetchQuery = async () => {
+ const resultData = await ProductService.getProducts(1, 20, { q: search });
- {showClearButton && value.length > 0 && (
+ if (resultData.success) setResult(resultData.data.results);
+ };
+
+ fetchQuery();
+ }, [search]);
+ return (
+ <>
+
onChangeText('')}
- style={styles.clearButton}
- testID="clear-button"
- accessibilityLabel="Limpiar búsqueda"
+ onPress={onSearchPress}
+ style={styles.iconContainer}
+ testID="search-icon"
>
-
+
+
+ {
+ setSearch(text); // Actualizar el estado local
+ onChangeText(text); // Notificar al componente padre
+ }}
+ returnKeyType="search" // Cambiar el botón del teclado a "Buscar"
+ onSubmitEditing={handleSearch}
+ />
+
+ {showClearButton && value.length > 0 && (
+ {
+ setSearch('');
+ onChangeText('');
+ }}
+ style={styles.clearButton}
+ testID="clear-button"
+ accessibilityLabel="Limpiar búsqueda"
+ >
+
+
+ )}
+
+ {search && (
+
+
+
+
+ Sugerencias
+
+
+ Productos que coinciden con "{search}"
+
+
+
+ {result?.map((p, index) => (
+
+ router.push(
+ `/products/${p.product.id}/presentation/${p.presentation.id}`,
+ )
+ }
+ >
+ {/* Imagen del producto */}
+
+
+
+
+ {/* Contenido del producto */}
+
+
+ {p?.product?.name}
+
+
+ {p?.presentation?.quantity}{' '}
+ {p?.presentation?.measurementUnit} -{' '}
+ {p?.product?.categories[0]?.name}
+
+
+ $ {formatPrice(p?.price)}
+
+
+
+ ))}
+
+
+
+
+ Buscar "{search}"
+
+
+
+
+
+
)}
-
+ >
);
};
diff --git a/src/components/SlideMenu.tsx b/src/components/SlideMenu.tsx
new file mode 100644
index 0000000..7852f2c
--- /dev/null
+++ b/src/components/SlideMenu.tsx
@@ -0,0 +1,263 @@
+import React from 'react';
+import {
+ Animated,
+ StyleSheet,
+ TouchableOpacity,
+ View,
+ Dimensions,
+ SafeAreaView,
+ Easing,
+} from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from './PoppinsText';
+import { ChevronLeftIcon } from 'react-native-heroicons/solid';
+import Avatar from './Avatar';
+import { UserService } from '../services/user';
+import { ArrowUpTrayIcon } from 'react-native-heroicons/outline';
+import { AuthService } from '../services/auth';
+import { useRouter } from 'expo-router';
+import Popup from './Popup';
+
+const { width, height } = Dimensions.get('window');
+
+type MenuItem = {
+ id: string;
+ title: string;
+ icon?: React.ReactElement;
+ onPress: () => void;
+};
+
+type FullScreenSlideMenuProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ items: MenuItem[];
+};
+
+const FullScreenSlideMenu: React.FC = ({
+ isOpen,
+ onClose,
+ items,
+}) => {
+ const [userName, setUserName] = React.useState('');
+ const menuPosition = React.useRef(new Animated.Value(-width)).current;
+ const [isActive, setIsActive] = React.useState('');
+ const [isLogoutConfirmationVisible, setIsLogoutConfirmationVisible] =
+ React.useState(false);
+ const router = useRouter();
+
+ const handleLogout = async () => {
+ try {
+ await AuthService.logout(); // Llama al método logout del servicio de autenticación
+ setIsLogoutConfirmationVisible(false); // Cierra el popup de confirmación
+ router.replace('/login'); // Redirige a la pantalla de inicio de sesión
+ } catch (error) {
+ console.error('Error al cerrar sesión:', error);
+ }
+ };
+
+ React.useEffect(() => {
+ Animated.timing(menuPosition, {
+ toValue: isOpen ? 0 : -width,
+ duration: 250,
+ easing: Easing.out(Easing.ease),
+ useNativeDriver: true,
+ }).start();
+
+ const fetchProfile = async () => {
+ try {
+ const profile = await UserService.getProfile();
+
+ if (profile.success)
+ setUserName(profile.data.firstName + ' ' + profile.data.lastName);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ fetchProfile();
+ setIsActive('');
+ }, [isOpen, menuPosition]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Volver
+
+
+
+
+
+
+ {userName}
+
+
+ Cuenta Personal
+
+
+
+ {items.map((item) => (
+ {
+ item.onPress();
+ onClose();
+ }}
+ >
+ {item.icon && (
+ {item.icon}
+ )}
+
+ {item.title}
+
+
+ ))}
+
+
+ setIsLogoutConfirmationVisible(true)}
+ >
+
+
+
+
+ Cerrar Sesión
+
+
+
+
+
+
+
+ {/* Popup for Logout Confirmation */}
+ setIsLogoutConfirmationVisible(false),
+ }}
+ onClose={() => setIsLogoutConfirmationVisible(false)}
+ />
+
+ {isOpen && (
+
+ )}
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ menu: {
+ position: 'absolute',
+ top: -25,
+ left: -15,
+ width: width,
+ height: height + 5,
+ backgroundColor: Colors.bgColor,
+ zIndex: 100,
+ elevation: 10,
+ },
+ safeArea: {
+ flex: 1,
+ },
+ menuContent: {
+ flex: 1,
+ paddingTop: 20,
+ paddingHorizontal: 15,
+ justifyContent: 'space-between',
+ },
+ menuItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginVertical: 8,
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ borderColor: Colors.primary,
+ borderRadius: 5,
+ },
+ menuIcon: {
+ marginRight: 20,
+ fontSize: 24,
+ width: 30,
+ textAlign: 'center',
+ },
+ menuText: {
+ fontSize: 18,
+ color: '#333',
+ },
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ zIndex: 99,
+ },
+});
+
+export default FullScreenSlideMenu;
diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx
new file mode 100644
index 0000000..c34528f
--- /dev/null
+++ b/src/components/TabBar.tsx
@@ -0,0 +1,140 @@
+import { View, StyleSheet, LayoutChangeEvent } from 'react-native';
+import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
+import { Colors } from '../styles/theme';
+import { useState } from 'react';
+import TabBarButton from './TabBarButton';
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
+
+export function TabBar({ state, descriptors, navigation }: BottomTabBarProps) {
+ const currentRoute = state.routes[state.index].name;
+
+ // Oculta la TabBar en pantallas específicas
+ const hiddenRoutes = ['menu', 'cart']; // Agrega aquí las rutas donde no quieres mostrar la TabBar
+ if (hiddenRoutes.includes(currentRoute)) {
+ return null; // No renderiza la TabBar
+ }
+
+ const [dimensions, setDimensions] = useState({ width: 10, height: 20 });
+
+ const buttonWidth = dimensions.width / state.routes.length;
+
+ const onTabBarLayout = (e: LayoutChangeEvent) => {
+ setDimensions({
+ width: e.nativeEvent.layout.width,
+ height: e.nativeEvent.layout.height,
+ });
+ };
+
+ const tabPositionX = useSharedValue(0);
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [
+ {
+ translateX: tabPositionX.value + 16,
+ },
+ ],
+ }));
+
+ return (
+
+
+ {state.routes.map((route, index) => {
+ const { options } = descriptors[route.key];
+ const label =
+ options.tabBarLabel !== undefined
+ ? options.tabBarLabel
+ : options.title !== undefined
+ ? options.title
+ : route.name;
+
+ const isFocused = state.index === index;
+
+ const onPress = () => {
+ tabPositionX.value = withTiming(buttonWidth * index, {
+ duration: 350,
+ });
+
+ const event = navigation.emit({
+ type: 'tabPress',
+ target: route.key,
+ canPreventDefault: true,
+ });
+
+ if (!isFocused && !event.defaultPrevented) {
+ navigation.navigate(route.name, route.params);
+ }
+ };
+
+ const onLongPress = () => {
+ navigation.emit({
+ type: 'tabLongPress',
+ target: route.key,
+ });
+ };
+
+ return (
+
+ label({
+ ...props,
+ position: props.position as 'beside-icon' | 'below-icon',
+ })
+ : label
+ }
+ />
+ );
+ })}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ tabBar: {
+ position: 'absolute',
+ flexDirection: 'row',
+ bottom: 0,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ borderColor: Colors.gray_100,
+ borderTopWidth: 1,
+ backgroundColor: Colors.menuWhite,
+ paddingVertical: 6,
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 10,
+ },
+ shadowOpacity: 0.1,
+ shadowRadius: 10,
+ },
+ // tabBarItem: {
+ // flex: 1,
+ // justifyContent: 'center',
+ // alignItems: 'center',
+ // },
+});
diff --git a/src/components/TabBarButton.tsx b/src/components/TabBarButton.tsx
new file mode 100644
index 0000000..d11c631
--- /dev/null
+++ b/src/components/TabBarButton.tsx
@@ -0,0 +1,94 @@
+import { Pressable, StyleSheet, GestureResponderEvent } from 'react-native';
+import Animated from 'react-native-reanimated';
+import React, { useEffect } from 'react';
+import { icon } from '../constants/icon';
+import {
+ interpolate,
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+} from 'react-native-reanimated';
+import { Colors } from '../styles/theme';
+
+const TabBarButton = ({
+ onPress,
+ onLongPress,
+ isFocused,
+ routeName,
+ label,
+}: {
+ onPress: (event: GestureResponderEvent) => void;
+ onLongPress: (event: GestureResponderEvent) => void;
+ isFocused: boolean;
+ routeName: keyof typeof icon;
+ color: string;
+ label:
+ | string
+ | ((args: {
+ focused: boolean;
+ color: string;
+ position: string;
+ children: string;
+ }) => React.ReactNode);
+}) => {
+ const scale = useSharedValue(0);
+
+ useEffect(() => {
+ scale.value = withSpring(
+ typeof isFocused === 'boolean' ? (isFocused ? 1 : 0) : isFocused,
+ { duration: 350 },
+ );
+ }, [scale, isFocused]);
+
+ const animatedIconStyle = useAnimatedStyle(() => {
+ const scaleValue = interpolate(scale.value, [0, 1], [1, 1.2]);
+
+ const top = interpolate(scale.value, [0, 1], [0, 9]);
+ return {
+ transform: [{ scale: scaleValue }],
+ top,
+ };
+ });
+
+ const animatedTextStyle = useAnimatedStyle(() => {
+ const opacity = interpolate(scale.value, [0, 1], [1, 0]);
+ return {
+ opacity,
+ };
+ });
+ return (
+
+
+ {icon[routeName]({
+ color: isFocused ? Colors.menuWhite : Colors.primary,
+ })}
+
+
+
+ {typeof label === 'function'
+ ? label({
+ focused: isFocused,
+ color: isFocused ? Colors.primary : Colors.primary,
+ position: 'beside-icon',
+ children: '',
+ })
+ : label}
+
+
+ );
+};
+
+export default TabBarButton;
+
+const styles = StyleSheet.create({
+ tabBarItem: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: 6,
+ },
+});
diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx
new file mode 100644
index 0000000..2d8eaf9
--- /dev/null
+++ b/src/components/Toggle.tsx
@@ -0,0 +1,93 @@
+import React, { useState, useEffect } from 'react';
+import { Pressable, Animated, StyleSheet } from 'react-native';
+import { Colors, ToggleSizes } from '../styles/theme';
+
+interface ToggleProps {
+ value?: boolean;
+ onChange?: (newValue: boolean) => void;
+ disabled?: boolean;
+ size?: 'small' | 'medium' | 'large';
+}
+
+const Toggle: React.FC = ({
+ value = false,
+ onChange,
+ disabled = false,
+ size = 'medium',
+}) => {
+ const [toggleValue, setToggleValue] = useState(value);
+ const animatedValue = new Animated.Value(value ? 1 : 0);
+ const config = ToggleSizes[size];
+
+ useEffect(() => {
+ setToggleValue(value);
+ Animated.timing(animatedValue, {
+ toValue: value ? 1 : 0,
+ duration: 200,
+ useNativeDriver: false,
+ }).start();
+ }, [value]);
+
+ const handleToggle = () => {
+ if (disabled) return;
+ const newValue = !toggleValue;
+ setToggleValue(newValue);
+ onChange?.(newValue);
+
+ Animated.timing(animatedValue, {
+ toValue: newValue ? 1 : 0,
+ duration: 200,
+ useNativeDriver: false,
+ }).start();
+ };
+
+ const translateX = animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [2, config.width - config.circleSize - 2],
+ });
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ toggleBase: {
+ justifyContent: 'center',
+ padding: 2,
+ },
+ circle: {
+ backgroundColor: Colors.textWhite,
+ borderRadius: 50,
+ position: 'absolute',
+ },
+ disabled: {
+ opacity: 0.5,
+ backgroundColor: Colors.stroke,
+ },
+});
+
+export default Toggle;
diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx
index f5f62e5..e4a4ae0 100644
--- a/src/components/TopBar.tsx
+++ b/src/components/TopBar.tsx
@@ -1,28 +1,72 @@
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import {
- UserCircleIcon,
+ BellIcon,
ShoppingCartIcon,
+ ClipboardIcon,
} from 'react-native-heroicons/outline';
import Logo from '../assets/images/logos/PharmaTech_Logo.svg';
import SearchInput from './SearchInput';
import { Colors } from '../styles/theme';
+import { useRouter } from 'expo-router';
+import Popup from './Popup';
+import { AuthService } from '../services/auth';
+import Badge from './Badge';
+import { useCart } from '../hooks/useCart';
+import AvatarWithMenu from './AvatarWithMenu';
+import { useNotifications } from '../hooks/useNotifications';
const TopBar = () => {
const [searchText, setSearchText] = useState('');
+ const [isPopupVisible, setIsPopupVisible] = useState(false);
+ const [isLogoutConfirmationVisible, setIsLogoutConfirmationVisible] =
+ useState(false);
+ const router = useRouter();
+ const { cartItems } = useCart();
+ const { unreadCount } = useNotifications();
+
+ // Calculate the total quantity of items in the cart
+ const totalCartQuantity = cartItems.reduce(
+ (sum, item) => sum + item.quantity,
+ 0,
+ );
const handleSearch = () => {
- // Search logic
console.log('Texto de búsqueda:', searchText);
};
+ const handleLogout = async () => {
+ try {
+ await AuthService.logout(); // Llama al método logout del servicio de autenticación
+ setIsLogoutConfirmationVisible(false); // Cierra el popup de confirmación
+
+ // Verificar si hay pantallas en la pila antes de llamar a dismissAll
+ if (router.canGoBack()) {
+ router.dismissAll();
+ }
+
+ router.replace('/login'); // Redirige a la pantalla de inicio de sesión
+ } catch (error) {
+ console.error('Error al cerrar sesión:', error);
+ }
+ };
+
return (
{/* Upper section */}
{/* Left user icon */}
-
-
+
+
+
+
+ {
+ router.push('/active-orders');
+ }}
+ >
+
{/* Logo */}
@@ -30,9 +74,37 @@ const TopBar = () => {
- {/* Right cart icon */}
-
+ {
+ router.push('/notifications');
+ }}
+ >
+
+ {unreadCount > 0 && (
+
+
+ {unreadCount}
+
+
+ )}
+
+
+ {/* Right cart icon with badge */}
+ {
+ router.push('/cart');
+ }}
+ >
+ {totalCartQuantity > 0 && (
+
+
+ {totalCartQuantity}
+
+
+ )}
@@ -43,6 +115,46 @@ const TopBar = () => {
onSearchPress={handleSearch}
style={styles.searchInput}
/>
+
+ {/* Popup for Avatar Options */}
+ {
+ setIsPopupVisible(false);
+ router.push('/change-password');
+ },
+ }}
+ secondaryButton={{
+ text: 'Cerrar sesión',
+ onPress: () => {
+ setIsPopupVisible(false);
+ setIsLogoutConfirmationVisible(true); // Muestra el popup de confirmación
+ },
+ }}
+ onClose={() => setIsPopupVisible(false)}
+ />
+
+ {/* Popup for Logout Confirmation */}
+ setIsLogoutConfirmationVisible(false),
+ }}
+ onClose={() => setIsLogoutConfirmationVisible(false)}
+ />
);
};
@@ -67,6 +179,17 @@ const styles = StyleSheet.create({
},
iconButton: {
paddingRight: 4,
+ position: 'relative',
+ },
+ badgeContainerBell: {
+ position: 'absolute',
+ top: -5,
+ right: 4,
+ },
+ badgeContainer: {
+ position: 'absolute',
+ top: -5,
+ right: -2,
},
searchInput: {
backgroundColor: Colors.menuWhite,
diff --git a/src/components/TopBarDelivery.tsx b/src/components/TopBarDelivery.tsx
new file mode 100644
index 0000000..2b4fa54
--- /dev/null
+++ b/src/components/TopBarDelivery.tsx
@@ -0,0 +1,145 @@
+import React, { useState } from 'react';
+import { View, StyleSheet, TouchableOpacity } from 'react-native';
+import { BellIcon } from 'react-native-heroicons/outline';
+import Logo from '../assets/images/logos/PharmaTech_Logo.svg';
+import SearchInput from './SearchInput';
+import { Colors } from '../styles/theme';
+import { useRouter } from 'expo-router';
+import Avatar from './Avatar';
+import Popup from './Popup';
+import { AuthService } from '../services/auth';
+
+const TopBarDelivery = () => {
+ const [searchText, setSearchText] = useState('');
+ const [isPopupVisible, setIsPopupVisible] = useState(false);
+ const [isLogoutConfirmationVisible, setIsLogoutConfirmationVisible] =
+ useState(false);
+ const router = useRouter();
+
+ const handleSearch = () => {
+ console.log('Texto de búsqueda:', searchText);
+ };
+
+ const handleLogout = async () => {
+ try {
+ await AuthService.logout(); // Llama al método logout del servicio de autenticación
+ setIsLogoutConfirmationVisible(false); // Cierra el popup de confirmación
+
+ // Verificar si hay pantallas en la pila antes de llamar a dismissAll
+ if (router.canGoBack()) {
+ router.dismissAll();
+ }
+
+ router.replace('/login'); // Redirige a la pantalla de inicio de sesión
+ } catch (error) {
+ console.error('Error al cerrar sesión:', error);
+ }
+ };
+
+ return (
+
+ {/* Upper section */}
+
+ {/* Left user icon */}
+
+ router.push('/menu?context=topBarDelivery')}
+ >
+
+
+
+
+ {/* Logo */}
+
+
+
+
+ {/* Right notification icon */}
+ {}}>
+
+
+
+
+ {/* Search Input */}
+
+
+ {/* Popup for Avatar Options */}
+ {
+ setIsPopupVisible(false);
+ router.push('/change-password');
+ },
+ }}
+ secondaryButton={{
+ text: 'Cerrar sesión',
+ onPress: () => {
+ setIsPopupVisible(false);
+ setIsLogoutConfirmationVisible(true); // Muestra el popup de confirmación
+ },
+ }}
+ onClose={() => setIsPopupVisible(false)}
+ />
+
+ {/* Popup for Logout Confirmation */}
+ setIsLogoutConfirmationVisible(false),
+ }}
+ onClose={() => setIsLogoutConfirmationVisible(false)}
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: Colors.bgColor,
+ paddingHorizontal: 16,
+ paddingTop: 12,
+ paddingBottom: 12,
+ },
+ topSection: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ logoContainer: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ iconButton: {
+ paddingRight: 4,
+ position: 'relative',
+ },
+ searchInput: {
+ backgroundColor: Colors.menuWhite,
+ borderWidth: 1,
+ borderColor: Colors.stroke,
+ height: 32,
+ borderRadius: 50,
+ },
+});
+
+export default TopBarDelivery;
diff --git a/src/components/VerticalStepper.tsx b/src/components/VerticalStepper.tsx
new file mode 100644
index 0000000..b770eb7
--- /dev/null
+++ b/src/components/VerticalStepper.tsx
@@ -0,0 +1,134 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from './PoppinsText';
+
+interface VerticalStepperProps {
+ steps: {
+ title: string;
+ description: string;
+ }[];
+ currentStep: number;
+}
+
+const VerticalStepper: React.FC = ({
+ steps,
+ currentStep,
+}) => {
+ return (
+
+ {steps.map((step, index) => {
+ const isCompleted = index < currentStep - 1;
+ const isActive = index === currentStep - 1;
+
+ return (
+
+
+ {/* Línea superior (excepto para el primer paso) */}
+ {index > 0 && (
+
+ )}
+
+ {/* Círculo del paso */}
+
+
+ {/* Línea inferior (excepto para el último paso) */}
+ {index < steps.length - 1 && (
+
+ )}
+
+
+
+ {step.title}
+
+ {step.description && (
+
+ {step.description}
+
+ )}
+
+
+ );
+ })}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 0,
+ },
+ stepContainer: {
+ flexDirection: 'row',
+ },
+ stepIndicatorContainer: {
+ width: 24,
+ alignItems: 'center',
+ marginRight: 8,
+ },
+ stepIndicator: {
+ width: 13,
+ height: 13,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.gray_500,
+ zIndex: 1,
+ },
+ completedStep: {
+ backgroundColor: Colors.semanticSuccess,
+ borderColor: Colors.semanticSuccess,
+ },
+ activeStep: {
+ backgroundColor: Colors.semanticSuccess,
+ },
+ connector: {
+ position: 'absolute',
+ width: 2,
+ },
+ completedConnector: {
+ backgroundColor: Colors.semanticSuccess,
+ },
+ pendingConnector: {
+ backgroundColor: Colors.gray_100,
+ },
+ stepContent: {
+ flex: 1,
+ marginBottom: 13,
+ },
+ activeStepTitle: {
+ color: Colors.semanticSuccess,
+ },
+ stepDescription: {
+ color: Colors.secondaryGray,
+ fontSize: FontSizes.c3.size,
+ },
+});
+
+export default VerticalStepper;
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..26c8d4d
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,6 @@
+export const Config = {
+ isDevelopment: process.env.PHARMATECH_DEV_MODE === 'true',
+ googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY || '',
+};
+
+console.log('Variables de entorno disponibles:', process.env);
diff --git a/src/constants/icon.tsx b/src/constants/icon.tsx
new file mode 100644
index 0000000..45386e5
--- /dev/null
+++ b/src/constants/icon.tsx
@@ -0,0 +1,22 @@
+import { Feather } from '@expo/vector-icons';
+import type { ComponentProps } from 'react';
+
+type FeatherIconProps = Omit, 'name'>;
+
+export const icon = {
+ index: (props: FeatherIconProps) => (
+
+ ),
+ categories: (props: FeatherIconProps) => (
+
+ ),
+ branches: (props: FeatherIconProps) => (
+
+ ),
+ offers: (props: FeatherIconProps) => (
+
+ ),
+ deliveryHistory: (props: FeatherIconProps) => (
+
+ ),
+};
diff --git a/src/helper/jwtHelper.ts b/src/helper/jwtHelper.ts
new file mode 100644
index 0000000..c2ca4af
--- /dev/null
+++ b/src/helper/jwtHelper.ts
@@ -0,0 +1,33 @@
+import * as SecureStore from 'expo-secure-store';
+
+export const decodeJWT = (token: string): { userId: string } | null => {
+ if (!token) {
+ console.error('Token vacío o nulo');
+ return null;
+ }
+
+ try {
+ const payload = token.split('.')[1];
+ const decodedPayload = JSON.parse(atob(payload));
+ console.log('Decoded JWT Payload:', decodedPayload); // Checking JWT content
+ return { userId: decodedPayload.sub }; // `sub` is usually the user ID in JWTs
+ } catch (error) {
+ console.error('Error decoding JWT:', error);
+ return null;
+ }
+};
+
+export const getUserIdFromSecureStore = async (): Promise => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ console.log('Retrieved Token:', token); // Checking if the token is retrieved correctly
+ if (!token) return null;
+
+ const decoded = decodeJWT(token);
+ console.log('Decoded User ID:', decoded?.userId); // Checking the decoded user ID
+ return decoded?.userId || null;
+ } catch (error) {
+ console.error('Error retrieving user ID:', error);
+ return null;
+ }
+};
diff --git a/src/hooks/useCart.ts b/src/hooks/useCart.ts
new file mode 100644
index 0000000..6fd2a91
--- /dev/null
+++ b/src/hooks/useCart.ts
@@ -0,0 +1,64 @@
+import { useDispatch, useSelector } from 'react-redux';
+import { RootState } from '../redux/store';
+import {
+ addItem,
+ removeItem,
+ updateQuantity,
+ CartItem,
+ setUserId,
+} from '../redux/slices/cartSlice';
+
+export const useCart = () => {
+ const dispatch = useDispatch();
+ const cartItems = useSelector((state: RootState) => state.cart.items);
+ const total = useSelector((state: RootState) => state.cart.total);
+ const userId = useSelector((state: RootState) => state.cart.userId);
+
+ const addToCart = (item: CartItem) => {
+ if (item.quantity <= 0) return; // <-- NO agregar si cantidad es 0
+ console.log('[addToCart] Item añadido al carrito:', item); // <-- LOG
+ dispatch(addItem(item));
+ };
+
+ const removeFromCart = (id: string) => {
+ dispatch(removeItem(id));
+ };
+
+ const updateCartQuantity = (
+ id: string,
+ quantity: number,
+ discount?: number,
+ price?: number,
+ ) => {
+ // Busca el item actual en el carrito
+ const item = cartItems.find((item) => item.id === id);
+ dispatch(
+ updateQuantity({
+ id,
+ quantity,
+ discount: typeof discount === 'number' ? discount : item?.discount,
+ price: typeof price === 'number' ? price : item?.price,
+ }),
+ );
+ };
+
+ const getItemQuantity = (id: string) => {
+ const item = cartItems.find((item) => item.id === id);
+ return item ? item.quantity : 0;
+ };
+
+ const setCartUserId = (id: string | null) => {
+ dispatch(setUserId(id));
+ };
+
+ return {
+ cartItems,
+ total,
+ userId,
+ addToCart,
+ removeFromCart,
+ updateCartQuantity,
+ getItemQuantity,
+ setCartUserId,
+ };
+};
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
new file mode 100644
index 0000000..9e93ba5
--- /dev/null
+++ b/src/hooks/useNotifications.ts
@@ -0,0 +1,158 @@
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ ReactNode,
+ useCallback,
+ useMemo,
+} from 'react';
+import { NotificationService } from '../services/notifications';
+import { NotificationResponse } from '@pharmatech/sdk';
+import { ServiceResponse } from '../types/api';
+import EventSource from 'react-native-sse';
+import * as SecureStore from 'expo-secure-store';
+import { api } from '../lib/sdkConfig';
+
+export type Notification = NotificationResponse & { isRead: boolean };
+
+export type NotificationsContextType = {
+ notifications: Notification[];
+ totalCount: number;
+ unreadCount: number;
+ markAsRead: (notificationId: string) => Promise;
+ markAsUnread: (notificationId: string) => Promise;
+ refreshNotifications: () => Promise;
+};
+
+const NotificationsContext = createContext(
+ null,
+);
+
+export function NotificationsProvider(props: { children: ReactNode }) {
+ const [notifications, setNotifications] = useState([]);
+
+ const refreshNotifications = useCallback(async (): Promise => {
+ try {
+ const response: ServiceResponse<
+ NotificationResponse | NotificationResponse[]
+ > = await NotificationService.getNotifications();
+
+ if (response.success && response.data) {
+ const rawArray = Array.isArray(response.data)
+ ? response.data
+ : [response.data];
+
+ const mapped = rawArray.map((n) => ({
+ ...n,
+ isRead: Boolean(n.isRead),
+ }));
+ console.log('Notificaciones recibidas:', mapped);
+ setNotifications(mapped);
+ } else {
+ console.error('Error cargando notificaciones:', response);
+ }
+ } catch (err) {
+ console.error('Excepción cargando notificaciones:', err);
+ }
+ }, []);
+
+ const markAsRead = useCallback(async (notificationId: string) => {
+ try {
+ await NotificationService.markAsRead(notificationId);
+ // optimismo: actualizamos localmente
+ setNotifications((prev) =>
+ prev.map((n) => (n.id === notificationId ? { ...n, isRead: true } : n)),
+ );
+ } catch (err) {
+ console.error(
+ `Error marcando notificación ${notificationId} como leída:`,
+ err,
+ );
+ }
+ }, []);
+
+ const markAsUnread = useCallback(async (notificationId: string) => {
+ // si tu API no ofrece un endpoint "unread", lo simulamos localmente
+ setNotifications((prev) =>
+ prev.map((n) => (n.id === notificationId ? { ...n, isRead: false } : n)),
+ );
+ }, []);
+
+ // Conteos derivados
+ const totalCount = notifications.length;
+ const unreadCount = useMemo(
+ () => notifications.filter((n) => !n.isRead).length,
+ [notifications],
+ );
+
+ useEffect(() => {
+ let es: EventSource | null = null;
+
+ async function setupSSE() {
+ // 1) carga inicial
+ await refreshNotifications();
+
+ // 2) stream SSE
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) return;
+
+ const axiosClient = api.client['client'];
+ const baseURL: string | undefined = axiosClient.getUri
+ ? axiosClient.getUri({ url: '' })
+ : axiosClient.defaults.baseURL;
+ if (!baseURL) return;
+
+ const url = `${baseURL.replace(/\/$/, '')}/notification/stream`;
+
+ es = new EventSource(url, {
+ headers: { Authorization: `Bearer ${token}` },
+ lineEndingCharacter: '\n',
+ });
+
+ // Cada vez que llegue un mensaje, recargamos TODO el listado:
+ es.addEventListener('message', () => {
+ console.log('[SSE] mensaje recibido → refrescando notificaciones');
+ void refreshNotifications();
+ });
+
+ es.addEventListener('error', (err) => {
+ console.error('[SSE] error:', err);
+ });
+ }
+
+ setupSSE();
+
+ return () => {
+ if (es) {
+ es.removeAllEventListeners();
+ es.close();
+ }
+ };
+ }, [refreshNotifications]);
+
+ return React.createElement(
+ NotificationsContext.Provider,
+ {
+ value: {
+ notifications,
+ totalCount,
+ unreadCount,
+ markAsRead,
+ markAsUnread,
+ refreshNotifications,
+ },
+ },
+ props.children,
+ );
+}
+
+export function useNotifications(): NotificationsContextType {
+ const ctx = useContext(NotificationsContext);
+ if (!ctx) {
+ throw new Error(
+ 'useNotifications debe usarse dentro de NotificationsProvider',
+ );
+ }
+ return ctx;
+}
diff --git a/src/lib/deliverySocket/deliverySocket.ts b/src/lib/deliverySocket/deliverySocket.ts
new file mode 100644
index 0000000..5e5e3ec
--- /dev/null
+++ b/src/lib/deliverySocket/deliverySocket.ts
@@ -0,0 +1,52 @@
+import io, { Socket } from 'socket.io-client';
+import { SOCKET_URL } from '../socketUrl';
+import * as SecureStore from 'expo-secure-store';
+
+let socket: Socket | null = null;
+
+export const initializeSocket = async (): Promise => {
+ if (socket) return socket;
+
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ console.error('Token de autenticación no encontrado');
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ console.log('Inicializando WebSocket con URL:', SOCKET_URL);
+ console.log('Enviando token JWT:', token);
+
+ socket = io(SOCKET_URL, {
+ autoConnect: false,
+ transports: ['polling'], // Asegurarnos de que ambos transportes estén habilitados
+ transportOptions: {
+ polling: {
+ extraHeaders: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ },
+ });
+
+ socket.on('connect', () => {
+ console.log('WebSocket conectado exitosamente');
+ });
+
+ socket.on('connect_error', (error) => {
+ console.error('Error de conexión al WebSocket:', error);
+ });
+
+ socket.on('disconnect', (reason) => {
+ console.warn('WebSocket desconectado. Razón:', reason);
+ });
+
+ return socket;
+};
+
+export const disconnectSocket = () => {
+ if (socket) {
+ console.log('Desconectando WebSocket');
+ socket.disconnect();
+ socket = null;
+ }
+};
diff --git a/src/lib/sdkConfig.ts b/src/lib/sdkConfig.ts
index f578bac..f176265 100644
--- a/src/lib/sdkConfig.ts
+++ b/src/lib/sdkConfig.ts
@@ -1,12 +1,37 @@
import { PharmaTech } from '@pharmatech/sdk';
+import * as SecureStore from 'expo-secure-store';
import { PHARMATECH_DEV_MODE } from '@env';
+import { AxiosRequestConfig } from 'axios'; // Importa el tipo AxiosRequestConfig
const isDevelopment = PHARMATECH_DEV_MODE === 'true';
-// Instancia del SDK para solicitudes con Origin
export const api = new PharmaTech(
isDevelopment,
isDevelopment
? 'http://localhost:3000' // Origin para desarrollo
: 'https://pharmatech.site', // Origin para producción
);
+
+// Configurar un interceptor para manejar dinámicamente el JWT
+api.client['client'].interceptors.request.use(
+ async (config: AxiosRequestConfig) => {
+ // Si ya se pasa un jwt explícitamente, no agregar el auth_token
+ if (config.headers?.Authorization) {
+ return config;
+ }
+
+ // Obtener el auth_token de SecureStore
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (token) {
+ config.headers = {
+ ...config.headers,
+ Authorization: `Bearer ${token}`,
+ };
+ } else {
+ if (config.headers) {
+ delete config.headers['Authorization'];
+ }
+ }
+ return config;
+ },
+);
diff --git a/src/lib/socketUrl.ts b/src/lib/socketUrl.ts
new file mode 100644
index 0000000..7788574
--- /dev/null
+++ b/src/lib/socketUrl.ts
@@ -0,0 +1,5 @@
+const devModeFlag = process.env.PHARMATECH_DEV_MODE === 'true';
+const devUrl = 'https://api-dev-8jfx.onrender.com';
+const prodUrl = 'https://api-d8h5.onrender.com';
+
+export const SOCKET_URL = devModeFlag ? devUrl : prodUrl;
diff --git a/src/redux/slices/cartSlice.ts b/src/redux/slices/cartSlice.ts
new file mode 100644
index 0000000..017b1d9
--- /dev/null
+++ b/src/redux/slices/cartSlice.ts
@@ -0,0 +1,86 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+export type CartItem = {
+ id: string;
+ name: string;
+ price: number;
+ quantity: number;
+ image: string;
+ discount?: number;
+};
+
+type CartState = {
+ userId: string | null;
+ items: CartItem[];
+ total: number;
+};
+
+const initialState: CartState = {
+ userId: null,
+ items: [],
+ total: 0,
+};
+
+const calculateTotal = (items: CartItem[]) =>
+ items.reduce((sum, item) => sum + item.price * item.quantity, 0);
+
+const cartSlice = createSlice({
+ name: 'cart',
+ initialState,
+ reducers: {
+ setUserId: (state, action: PayloadAction) => {
+ state.userId = action.payload;
+ if (!action.payload) {
+ state.items = [];
+ state.total = 0;
+ }
+ },
+ addItem: (state, action: PayloadAction) => {
+ const existing = state.items.find(
+ (item) => item.id === action.payload.id,
+ );
+ if (existing) {
+ // Si ya existe, actualiza cantidad y descuento correctamente
+ existing.quantity = action.payload.quantity;
+ existing.price = action.payload.price;
+ existing.discount = action.payload.discount; // <-- ¡Asegúrate de actualizar el descuento!
+ } else {
+ state.items.push({ ...action.payload });
+ }
+ state.total = calculateTotal(state.items);
+ },
+ removeItem: (state, action: PayloadAction) => {
+ state.items = state.items.filter((item) => item.id !== action.payload);
+ state.total = calculateTotal(state.items);
+ },
+ updateQuantity: (
+ state,
+ action: PayloadAction<{
+ id: string;
+ quantity: number;
+ discount?: number;
+ price?: number;
+ }>,
+ ) => {
+ const item = state.items.find((item) => item.id === action.payload.id);
+ if (item) {
+ item.quantity = action.payload.quantity;
+ if (typeof action.payload.discount === 'number') {
+ item.discount = action.payload.discount;
+ }
+ if (typeof action.payload.price === 'number') {
+ item.price = action.payload.price;
+ }
+ }
+ state.total = calculateTotal(state.items);
+ },
+ clearCart: (state) => {
+ state.items = [];
+ state.total = 0;
+ },
+ },
+});
+
+export const { setUserId, addItem, removeItem, updateQuantity, clearCart } =
+ cartSlice.actions;
+export default cartSlice.reducer;
diff --git a/src/redux/slices/checkoutSlice.ts b/src/redux/slices/checkoutSlice.ts
new file mode 100644
index 0000000..7d41542
--- /dev/null
+++ b/src/redux/slices/checkoutSlice.ts
@@ -0,0 +1,80 @@
+// src/redux/slices/checkoutSlice.ts
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+export type PurchaseOption = 'pickup' | 'delivery' | null;
+export type PaymentOption =
+ | 'punto_de_venta'
+ | 'efectivo'
+ | 'transferencia'
+ | 'pago_movil'
+ | null;
+
+interface CheckoutState {
+ step: number;
+ option: PurchaseOption;
+ payment: PaymentOption;
+ locationId: string | null;
+ paymentInfoValid: boolean;
+ couponDiscount: number;
+ couponApplied: boolean;
+ lastAppliedCoupon: string | null; // Add a field to store the last applied coupon code
+}
+
+const initialState: CheckoutState = {
+ step: 1,
+ option: 'pickup',
+ payment: null,
+ locationId: null,
+ paymentInfoValid: false,
+ couponDiscount: 0,
+ couponApplied: false,
+ lastAppliedCoupon: null,
+};
+
+const checkoutSlice = createSlice({
+ name: 'checkout',
+ initialState,
+ reducers: {
+ setStep(state, action: PayloadAction) {
+ state.step = action.payload;
+ },
+ setOption(state, action: PayloadAction) {
+ state.option = action.payload;
+ },
+ setPayment(state, action: PayloadAction) {
+ state.payment = action.payload;
+ },
+ setLocationId(state, action: PayloadAction) {
+ state.locationId = action.payload;
+ },
+ setPaymentInfoValid(state, action: PayloadAction) {
+ state.paymentInfoValid = action.payload;
+ },
+ setCouponDiscount(state, action: PayloadAction) {
+ state.couponDiscount = action.payload;
+ },
+ setCouponApplied(state, action: PayloadAction) {
+ state.couponApplied = action.payload;
+ },
+ setLastAppliedCoupon(state, action: PayloadAction) {
+ state.lastAppliedCoupon = action.payload;
+ },
+ resetCheckout(state) {
+ Object.assign(state, initialState);
+ },
+ },
+});
+
+export const {
+ setStep,
+ setOption,
+ setPayment,
+ setLocationId,
+ setPaymentInfoValid,
+ setCouponDiscount,
+ setCouponApplied,
+ setLastAppliedCoupon,
+ resetCheckout,
+} = checkoutSlice.actions;
+
+export default checkoutSlice.reducer;
diff --git a/src/redux/slices/deliverySlice.ts b/src/redux/slices/deliverySlice.ts
new file mode 100644
index 0000000..4de3264
--- /dev/null
+++ b/src/redux/slices/deliverySlice.ts
@@ -0,0 +1,58 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import {
+ OrderDeliveryDetailedResponse,
+ OrderDeliveryStatus,
+} from '@pharmatech/sdk';
+
+interface DeliveryState {
+ orders: Record; // Almacenar múltiples órdenes por ID
+ deliveryState: Record; // Estado de entrega por ID de orden
+}
+
+const initialState: DeliveryState = {
+ orders: {},
+ deliveryState: {},
+};
+
+const deliverySlice = createSlice({
+ name: 'delivery',
+ initialState,
+ reducers: {
+ setOrderDetails(
+ state,
+ action: PayloadAction<{
+ id: string;
+ details: OrderDeliveryDetailedResponse;
+ }>,
+ ) {
+ state.orders[action.payload.id] = action.payload.details;
+ },
+ setDeliveryState(
+ state,
+ action: PayloadAction<{ id: string; state: number }>,
+ ) {
+ state.deliveryState[action.payload.id] = action.payload.state;
+ },
+ updateDeliveryStatus(
+ state,
+ action: PayloadAction<{ id: string; status: OrderDeliveryStatus }>,
+ ) {
+ if (state.orders[action.payload.id]) {
+ state.orders[action.payload.id].deliveryStatus = action.payload
+ .status as OrderDeliveryStatus;
+ }
+ },
+ resetOrderState(state, action: PayloadAction) {
+ delete state.orders[action.payload];
+ delete state.deliveryState[action.payload];
+ },
+ },
+});
+
+export const {
+ setOrderDetails,
+ setDeliveryState,
+ updateDeliveryStatus,
+ resetOrderState,
+} = deliverySlice.actions;
+export default deliverySlice.reducer;
diff --git a/src/redux/store.ts b/src/redux/store.ts
new file mode 100644
index 0000000..c0eb398
--- /dev/null
+++ b/src/redux/store.ts
@@ -0,0 +1,17 @@
+import { configureStore } from '@reduxjs/toolkit';
+import cartReducer from './slices/cartSlice'; // Importa el reducer del carrito
+import deliveryReducer from './slices/deliverySlice'; // Importa el reducer de entrega
+import checkoutReducer from './slices/checkoutSlice';
+
+export const store = configureStore({
+ reducer: {
+ cart: cartReducer, // Conecta el slice del carrito
+ delivery: deliveryReducer,
+ checkout: checkoutReducer,
+ },
+ devTools: true, // Habilita Redux DevTools
+});
+
+// Tipos para el store
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/screens/ActiveOrdersScreen.tsx b/src/screens/ActiveOrdersScreen.tsx
new file mode 100644
index 0000000..8b14053
--- /dev/null
+++ b/src/screens/ActiveOrdersScreen.tsx
@@ -0,0 +1,210 @@
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ RefreshControl,
+} from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import { useRouter } from 'expo-router';
+import Alert from '../components/Alerts';
+import { OrderResponse } from '@pharmatech/sdk';
+import { UserService } from '../services/user';
+import { truncateString } from '../utils/commons';
+import { formatPrice } from '../utils/formatPrice';
+
+const STATUS_LABELS: Record = {
+ requested: 'Pendiente',
+ ready_for_pickup: 'A Retirar',
+ in_progress: 'En Proceso',
+ approved: 'Aprobado',
+};
+
+const STATUS_COLORS: Record = {
+ requested: Colors.semanticDanger,
+ ready_for_pickup: Colors.semanticInfo,
+ in_progress: Colors.secondaryGray,
+ approved: Colors.secondary,
+};
+
+const ActiveOrdersScreen = () => {
+ const [activeOrdersList, setActiveOrdersList] = useState<
+ OrderResponse[] | undefined
+ >(undefined);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [showInfoAlert, setShowInfoAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const router = useRouter();
+ const [refreshing, setRefreshing] = useState(false);
+
+ const fetchActiveOrders = async () => {
+ try {
+ const order = await UserService.getUserOrders();
+ if (order.success) {
+ const activeOrders = order.data.results.filter(
+ (o) =>
+ o.status === 'requested' ||
+ o.status === 'approved' ||
+ o.status === 'ready_for_pickup' ||
+ o.status === 'in_progress',
+ );
+ if (activeOrders.length > 0) {
+ setActiveOrdersList(activeOrders);
+ } else {
+ setShowInfoAlert(true);
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ setErrorMessage('Ha ocurrido un error');
+ setShowErrorAlert(true);
+ }
+ };
+
+ useEffect(() => {
+ fetchActiveOrders();
+ }, []);
+
+ const onRefresh = async () => {
+ setRefreshing(true);
+ await fetchActiveOrders();
+ };
+
+ return (
+
+ }
+ >
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ borderColor
+ />
+ )}
+ {showInfoAlert && (
+ {
+ setShowInfoAlert(false);
+ }}
+ borderColor
+ />
+ )}
+
+
+
+ Pedidos Activos
+
+
+
+ {activeOrdersList &&
+ activeOrdersList.length > 0 &&
+ activeOrdersList.map((order, index) => (
+
+
+
+
+
+ {order ? truncateString(order?.id, 8) : ''}
+
+
+ {new Date(order.createdAt).toLocaleDateString()}
+
+
+ ${formatPrice(order.totalPrice)}
+
+
+
+ {STATUS_LABELS[order.status] || order.status}
+
+ router.push(`order/tracking/${order.id}`)}
+ >
+
+ Ver detalles
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 20,
+ paddingTop: -20,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -162,
+ top: 20,
+ right: 0,
+ zIndex: 1000,
+ },
+ orderHeader: {
+ alignItems: 'center',
+ marginBottom: 15,
+ },
+ orderInfo: {
+ marginVertical: 5,
+ },
+});
+
+export default ActiveOrdersScreen;
diff --git a/src/screens/BranchDetailScreen.tsx b/src/screens/BranchDetailScreen.tsx
new file mode 100644
index 0000000..1e96e00
--- /dev/null
+++ b/src/screens/BranchDetailScreen.tsx
@@ -0,0 +1,247 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ Image,
+} from 'react-native';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import { BranchService } from '../services/branches';
+import { ProductService } from '../services/products';
+import { BranchResponse, ProductPresentation } from '@pharmatech/sdk';
+import BranchMapModal from '../components/BranchMapModal';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import { usePagination } from '../hooks/usePagination';
+import { MapPinIcon } from 'react-native-heroicons/solid';
+
+export default function BranchDetailScreen() {
+ const { id } = useLocalSearchParams<{ id: string }>();
+ const [branch, setBranch] = useState(null);
+ const [mapVisible, setMapVisible] = useState(false);
+
+ const router = useRouter();
+
+ const {
+ data: products,
+ fetchData,
+ loading,
+ hasMore,
+ } = usePagination(async (page) => {
+ const productResponse = await ProductService.getProducts(page, 10, {
+ branchId: [id!],
+ });
+
+ if (!productResponse.success) {
+ return { data: [], next: null };
+ }
+
+ return {
+ data: productResponse.data.results,
+ next: productResponse.data.next,
+ };
+ });
+
+ useEffect(() => {
+ if (!id) {
+ console.error('El ID de la sucursal no está definido.');
+ return;
+ }
+
+ const fetchBranchDetails = async () => {
+ try {
+ const branchData = await BranchService.getById(id);
+ setBranch(branchData);
+ } catch (error) {
+ console.error('Error fetching branch details:', error);
+ }
+ };
+
+ fetchBranchDetails();
+ }, [id]);
+
+ const renderProduct = ({ item }: { item: ProductPresentation }) => {
+ const { product, presentation, stock } = item;
+
+ return (
+
+ router.push(`/products/${product.id}/presentation/${presentation.id}`)
+ }
+ >
+
+
+
+ {product.name || 'Nombre no disponible'} {presentation.name}{' '}
+ {presentation.quantity} {presentation.measurementUnit}
+
+
+ Cantidad disponible en sucursal: {stock}
+
+
+
+ );
+ };
+
+ if (loading && products.length === 0) {
+ return (
+
+ Cargando...
+
+ );
+ }
+
+ return (
+
+ {branch ? (
+ <>
+ {branch.name}
+ Dirección
+ {branch.address}
+ setMapVisible(true)}
+ style={styles.mapButton}
+ >
+
+ Ubicar en el mapa
+
+ >
+ ) : (
+
+ No se pudo cargar la información de la sucursal.
+
+ )}
+
+
+ Productos disponibles en {branch?.name || 'la sucursal'}
+
+ item.id}
+ renderItem={renderProduct}
+ contentContainerStyle={styles.list}
+ onEndReached={hasMore ? fetchData : null}
+ onEndReachedThreshold={0.5}
+ ListEmptyComponent={
+ !loading ? (
+
+ No hay productos disponibles.
+
+ ) : null
+ }
+ />
+ {branch && (
+ setMapVisible(false)}
+ branchName={branch.name}
+ branchCoordinates={{
+ latitude: branch.latitude,
+ longitude: branch.longitude,
+ }}
+ />
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 16,
+ paddingTop: 52,
+ },
+ title: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 8,
+ },
+ titleAddress: {
+ fontSize: FontSizes.s2.size,
+ lineHeight: FontSizes.s2.lineHeight,
+ color: Colors.primary,
+ marginBottom: 4,
+ },
+ address: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.textLowContrast,
+ marginBottom: 12,
+ },
+ map: {
+ fontSize: FontSizes.b2.size,
+ lineHeight: FontSizes.b2.lineHeight,
+ color: Colors.primary,
+ marginBottom: 4,
+ },
+ mapButton: {
+ flexDirection: 'row',
+ marginBottom: 8,
+ },
+ divider: {
+ marginBottom: 8,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.stroke,
+ },
+ mapButtonText: {
+ color: Colors.textWhite,
+ textAlign: 'center',
+ fontSize: FontSizes.b1.size,
+ },
+ list: {
+ marginTop: 10,
+ },
+ productCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 8,
+ padding: 10,
+ marginBottom: 10,
+ shadowColor: '#000',
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ productImage: {
+ width: 60,
+ height: 60,
+ borderRadius: 8,
+ marginRight: 10,
+ },
+ productInfo: {
+ flex: 1,
+ },
+ productName: {
+ fontSize: FontSizes.b1.size,
+ color: Colors.textMain,
+ marginBottom: 4,
+ },
+ productQuantity: {
+ fontSize: FontSizes.c1.size,
+ color: Colors.textLowContrast,
+ },
+ emptyText: {
+ textAlign: 'center',
+ color: Colors.textLowContrast,
+ marginTop: 20,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingText: {
+ fontSize: FontSizes.b1.size,
+ color: Colors.textLowContrast,
+ },
+});
diff --git a/src/screens/CartListScreen.tsx b/src/screens/CartListScreen.tsx
new file mode 100644
index 0000000..35762bc
--- /dev/null
+++ b/src/screens/CartListScreen.tsx
@@ -0,0 +1,341 @@
+import React from 'react';
+import {
+ View,
+ FlatList,
+ StyleSheet,
+ TouchableOpacity,
+ Image,
+} from 'react-native';
+import { useCart } from '../hooks/useCart';
+import CardButton from '../components/CardButton';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import type { CartItem } from '../redux/slices/cartSlice';
+import Button from '../components/Button';
+import { TrashIcon } from 'react-native-heroicons/outline';
+import { useRouter } from 'expo-router';
+import { formatPrice } from '../utils/formatPrice';
+
+const CartListScreen = () => {
+ const router = useRouter();
+ const { cartItems, removeFromCart, updateCartQuantity } = useCart();
+
+ const subtotal = cartItems.reduce(
+ (sum, item) => sum + item.price * item.quantity,
+ 0,
+ );
+ // Total price sum
+ const totalDiscount = cartItems.reduce(
+ (sum, item) =>
+ sum + item.price * item.quantity * ((item.discount ?? 0) / 100),
+ 0,
+ ); // Discount sum
+ const total = subtotal - totalDiscount; // Subtotal with discount
+
+ const renderItem = ({ item }: { item: CartItem }) => {
+ console.log('[CartListScreen] Renderizando item:', item); // <-- LOG
+ // Usar el descuento del item, si existe, si no 0
+ const discount = item.discount ?? 0;
+ const discountedPrice = item.price * (1 - discount / 100);
+ const totalDiscountedPrice = discountedPrice * item.quantity;
+ const totalOriginalPrice = item.price * item.quantity;
+
+ return (
+
+
+ {/* Discount badge */}
+ {discount > 0 && (
+
+
+ -{discount}%
+
+
+ )}
+
+
+
+
+ {item.name}
+
+
+ ${formatPrice(totalDiscountedPrice)}
+
+ {discount > 0 && (
+
+ ${formatPrice(totalOriginalPrice)}
+
+ )}
+
+
+
+ (${formatPrice(discountedPrice)} c/u)
+
+
+
+ updateCartQuantity(item.id, quantity, item.discount ?? 0)
+ }
+ initialValue={item.quantity > 0 ? item.quantity : 0}
+ syncQuantity={(quantity) =>
+ updateCartQuantity(item.id, quantity, item.discount ?? 0)
+ }
+ />
+ removeFromCart(item.id)}
+ style={styles.trashIconContainer}
+ >
+
+
+
+
+
+ );
+ };
+
+ const isCartEmpty =
+ cartItems.filter((item) => item.quantity > 0).length === 0;
+
+ return (
+
+
+ Carrito de compras
+
+ {isCartEmpty ? (
+
+
+ Aún no tienes productos en tu carrito
+
+
+ Explora y encuentra lo que buscas
+
+
+ ) : (
+ <>
+ item.quantity > 0)}
+ renderItem={renderItem}
+ keyExtractor={(item) => item.id}
+ contentContainerStyle={styles.listContainer}
+ showsVerticalScrollIndicator={false}
+ />
+
+
+
+ Subtotal
+
+ ${formatPrice(subtotal)}
+
+
+
+ Descuentos
+
+ -${formatPrice(totalDiscount)}
+
+
+
+ Total
+
+ ${formatPrice(total)}
+
+
+
+ >
+ )}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 16,
+ paddingTop: -16,
+ },
+ header: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ marginBottom: 24,
+ color: Colors.textMain,
+ },
+ listContainer: {
+ paddingBottom: 16,
+ },
+ card: {
+ flexDirection: 'row',
+ borderRadius: 8,
+ marginBottom: 24,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.gray_100,
+ paddingBottom: 18,
+ },
+ imageContainer: {
+ width: 100,
+ height: 100,
+
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 16,
+ },
+ detailsContainer: {
+ flex: 1,
+ },
+ productName: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ width: '50%',
+ },
+ productPrice: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.gray_500,
+ marginBottom: 2,
+ },
+ productTotalPrice: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 8,
+ },
+ quantityContainer: {
+ flexDirection: 'row',
+ left: -10,
+ },
+ removeText: {
+ fontSize: 14,
+ color: Colors.semanticDanger,
+ },
+ footerBackground: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ backgroundColor: Colors.menuWhite,
+ paddingTop: 310,
+ alignItems: 'center',
+ },
+ footer: {
+ paddingTop: 44,
+ alignItems: 'center',
+ position: 'relative',
+ },
+ totalText: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 16,
+ },
+ checkoutButton: {
+ marginBottom: 16,
+ width: '100%',
+ height: 50,
+ },
+ emptyCartText: {
+ fontSize: FontSizes.b2.size,
+ lineHeight: FontSizes.b2.lineHeight,
+ color: Colors.textMain,
+ textAlign: 'center',
+ marginBottom: 4,
+ },
+ emptyCartText1: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.textMain,
+ textAlign: 'center',
+ },
+ subtotalText: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 8,
+ },
+ discountText: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.secondary,
+ marginBottom: 8,
+ },
+ ivaText: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 20,
+ },
+ height: {
+ height: 64,
+ },
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ width: '100%',
+ marginBottom: 8,
+ },
+ productImage: {
+ width: '100%',
+ height: '100%',
+ marginTop: 16,
+ },
+ trashIconContainer: {
+ position: 'absolute',
+ bottom: 16,
+ right: -10,
+ },
+ discountBadge: {
+ zIndex: 999,
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ backgroundColor: Colors.semanticWarning,
+ borderRadius: 50,
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ },
+ discountBadgeText: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.primary,
+ },
+ productOriginalPrice: {
+ position: 'absolute',
+ right: 0,
+ top: 24,
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.gray_500,
+ textDecorationLine: 'line-through',
+ marginBottom: 8,
+ },
+ emptyCartContainer: {
+ flex: 1,
+ width: '75%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ alignSelf: 'center',
+ },
+ emptyCartButton: {
+ marginTop: 16,
+ },
+});
+
+export default CartListScreen;
diff --git a/src/screens/CategoryDetailScreen.tsx b/src/screens/CategoryDetailScreen.tsx
new file mode 100644
index 0000000..ca269bf
--- /dev/null
+++ b/src/screens/CategoryDetailScreen.tsx
@@ -0,0 +1,224 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ Image,
+} from 'react-native';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import { ProductService } from '../services/products';
+import { CategoryResponse, ProductPresentation } from '@pharmatech/sdk';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import { usePagination } from '../hooks/usePagination';
+import { CategoryService } from '../services/category';
+
+export default function CategoryDetailScreen() {
+ const { id } = useLocalSearchParams<{ id: string }>();
+ const [category, setCategory] = useState(null);
+
+ const router = useRouter();
+
+ const {
+ data: products,
+ fetchData,
+ loading,
+ hasMore,
+ } = usePagination(async (page) => {
+ const productResponse = await ProductService.getProducts(page, 10, {
+ categoryId: [id!],
+ });
+
+ if (!productResponse.success) {
+ return { data: [], next: null };
+ }
+
+ return {
+ data: productResponse.data.results,
+ next: productResponse.data.next,
+ };
+ });
+
+ useEffect(() => {
+ if (!id) {
+ console.error('El ID de la categoria no está definido.');
+ return;
+ }
+
+ const fetchCategoryDetails = async () => {
+ try {
+ const categoryData = await CategoryService.getById(id);
+ setCategory(categoryData);
+ } catch (error) {
+ console.error('Error fetching category details:', error);
+ }
+ };
+
+ fetchCategoryDetails();
+ }, [id]);
+
+ const renderProduct = ({ item }: { item: ProductPresentation }) => {
+ const { product, presentation, stock } = item;
+
+ return (
+
+ router.push(`/products/${product.id}/presentation/${presentation.id}`)
+ }
+ >
+
+
+
+ {product.name || 'Nombre no disponible'} {presentation.name}{' '}
+ {presentation.quantity} {presentation.measurementUnit}
+
+
+ Cantidad disponible: {stock || 0}
+
+
+
+ );
+ };
+
+ if (loading && products.length === 0) {
+ return (
+
+ Cargando...
+
+ );
+ }
+
+ return (
+
+ {category ? (
+ <>
+ {category.name}
+ >
+ ) : (
+
+ No se pudo cargar la información de la categoria.
+
+ )}
+
+
+ Productos disponibles en {category?.name || 'la categoria'}
+
+ item.id}
+ renderItem={renderProduct}
+ contentContainerStyle={styles.list}
+ onEndReached={hasMore ? fetchData : null}
+ onEndReachedThreshold={0.5}
+ ListEmptyComponent={
+ !loading ? (
+
+ No hay productos disponibles.
+
+ ) : null
+ }
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 16,
+ paddingTop: 52,
+ },
+ title: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 8,
+ },
+ titleAddress: {
+ fontSize: FontSizes.s2.size,
+ lineHeight: FontSizes.s2.lineHeight,
+ color: Colors.primary,
+ marginBottom: 4,
+ },
+ address: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.textLowContrast,
+ marginBottom: 12,
+ },
+ map: {
+ fontSize: FontSizes.b2.size,
+ lineHeight: FontSizes.b2.lineHeight,
+ color: Colors.primary,
+ marginBottom: 4,
+ },
+ mapButton: {
+ flexDirection: 'row',
+ marginBottom: 8,
+ },
+ divider: {
+ marginBottom: 8,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.stroke,
+ },
+ mapButtonText: {
+ color: Colors.textWhite,
+ textAlign: 'center',
+ fontSize: FontSizes.b1.size,
+ },
+ list: {
+ marginTop: 10,
+ },
+ productCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 8,
+ padding: 10,
+ marginBottom: 10,
+ shadowColor: '#000',
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ productImage: {
+ width: 60,
+ height: 60,
+ borderRadius: 8,
+ marginRight: 10,
+ },
+ productInfo: {
+ flex: 1,
+ },
+ productName: {
+ fontSize: FontSizes.b1.size,
+ color: Colors.textMain,
+ marginBottom: 4,
+ },
+ productQuantity: {
+ fontSize: FontSizes.c1.size,
+ color: Colors.textLowContrast,
+ },
+ emptyText: {
+ textAlign: 'center',
+ color: Colors.textLowContrast,
+ marginTop: 20,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingText: {
+ fontSize: FontSizes.b1.size,
+ color: Colors.textLowContrast,
+ },
+});
diff --git a/src/screens/CheckoutScreen.tsx b/src/screens/CheckoutScreen.tsx
new file mode 100644
index 0000000..90ad041
--- /dev/null
+++ b/src/screens/CheckoutScreen.tsx
@@ -0,0 +1,550 @@
+import React, { useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ Animated,
+ BackHandler,
+ RefreshControl,
+} from 'react-native';
+import { ShoppingBagIcon, TruckIcon } from 'react-native-heroicons/outline';
+import { Colors, FontSizes } from '../styles/theme';
+import RadioCard from '../components/RadioCard';
+import OrderSummary from '../components/OrderSummary';
+import { useCart } from '../hooks/useCart';
+import Button from '../components/Button';
+import Steps from '../components/Steps';
+import PaymentMethods from '../components/PaymentMethods';
+import PoppinsText from '../components/PoppinsText';
+import LocationSelector from '../components/LocationSelector';
+import Coupon from '../components/Coupon';
+import { useRouter } from 'expo-router';
+import { OrderService } from '../services/order';
+import BranchMapModal from '../components/BranchMapModal';
+import { useDispatch, useSelector } from 'react-redux';
+import { clearCart } from '../redux/slices/cartSlice';
+import { useFocusEffect } from '@react-navigation/native';
+import Popup from '../components/Popup';
+import EmailVerificationModal from './tab/EmailVerificationModal';
+import { RootState, AppDispatch } from '../redux/store';
+import {
+ setOption,
+ setPayment,
+ setLocationId,
+ setCouponDiscount,
+ setCouponApplied,
+} from '../redux/slices/checkoutSlice';
+
+import {
+ OrderType,
+ CreateOrder,
+ CreateOrderDetail,
+ PaymentMethod,
+} from '@pharmatech/sdk';
+import { formatPrice } from '../utils/formatPrice';
+
+const CheckoutScreen = () => {
+ const dispatch = useDispatch();
+ const { step, option, payment, locationId, couponDiscount, couponApplied } =
+ useSelector((state: RootState) => state.checkout);
+
+ const router = useRouter();
+ const { cartItems } = useCart();
+ const [errorMessage, setErrorMessage] = useState(null);
+ //const [userName, setUserName] = useState('Usuario');
+ const [modalVisible, setModalVisible] = useState(false);
+ const [selectedBranch, setSelectedBranch] = useState<{
+ name: string;
+ latitude: number;
+ longitude: number;
+ } | null>(null);
+ // const [orderNumber, setOrderNumber] = useState(null); // commented: unused
+ const [popupVisible, setPopupVisible] = useState(false);
+ const [popupMessages, setPopupMessages] = useState([]);
+ const [validationPopupVisible, setValidationPopupVisible] = useState(false);
+ const [emailVerificationModalVisible, setEmailVerificationModalVisible] =
+ useState(false); // Track modal visibility
+ // const [orderStatus, setOrderStatus] = useState(null); // commented: unused
+ // Suscribe al socket usando el hook; solo se conecta cuando orderNumber no es null
+
+ useFocusEffect(
+ React.useCallback(() => {
+ const onBackPress = () => {
+ if (step === stepsLabels.length) {
+ // Limpiar el carrito si el usuario está en el último paso
+ dispatch(clearCart());
+ }
+ return false; // Permitir el comportamiento predeterminado del retroceso
+ };
+
+ BackHandler.addEventListener('hardwareBackPress', onBackPress);
+
+ return () =>
+ BackHandler.removeEventListener('hardwareBackPress', onBackPress);
+ }, [step]),
+ );
+
+ // Solo mostrar el paso 1
+ const stepsLabels = ['Opciones de Compra'];
+
+ const subtotal = cartItems.reduce(
+ (sum, item) => sum + item.price * item.quantity,
+ 0,
+ );
+
+ // Calcula el descuento total de los productos (igual que OrderSummary)
+ const totalDiscount = cartItems.reduce(
+ (sum, item) =>
+ sum + item.price * item.quantity * ((item.discount ?? 0) / 100),
+ 0,
+ );
+
+ // Subtotal después de descuentos de productos
+ const subtotalAfterDiscount = subtotal - totalDiscount;
+
+ // Aplica el cupón sobre el subtotal ya descontado
+ const subtotalAfterCoupon = couponApplied
+ ? subtotalAfterDiscount * (1 - couponDiscount / 100)
+ : subtotalAfterDiscount;
+
+ const total = subtotalAfterCoupon;
+
+ const renderFooterMessage = () => {
+ if (option === 'pickup' && payment === 'punto_de_venta') {
+ return 'Por favor dirigirse a su sucursal más cercana y pagar en el sitio. La orden estará en proceso de pago hasta que pague en el sitio.';
+ }
+ if (option === 'delivery' && payment === 'efectivo') {
+ return 'Debe pagar al personal del delivery la cantidad exacta de su pedido. La orden estará en proceso de pago hasta que pague en el sitio.';
+ }
+ return null;
+ };
+
+ // const isValidUUID = (value: string | null): boolean => {
+ // const uuidRegex =
+ // /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+ // return value !== null && uuidRegex.test(value);
+ // };
+
+ const handleContinue = async () => {
+ const missingFields: string[] = [];
+
+ // Validación solo para el paso 1
+ if (step === 1) {
+ if (!option) missingFields.push('Seleccionar una opción de compra.');
+ if (!locationId)
+ missingFields.push('Seleccionar una opción de locación.');
+ if (!payment) missingFields.push('Seleccionar un método de pago.');
+
+ if (missingFields.length > 0) {
+ setPopupMessages(missingFields);
+ setPopupVisible(true);
+ return;
+ }
+
+ // Validar los productos del carrito
+ const products: CreateOrderDetail[] = cartItems
+ .filter((item) => item.quantity > 0)
+ .map((item) => ({
+ productPresentationId: item.id,
+ quantity: item.quantity,
+ }));
+
+ setErrorMessage(null);
+
+ let sdkPaymentMethod: PaymentMethod;
+ switch (payment) {
+ case 'efectivo':
+ sdkPaymentMethod = PaymentMethod.CASH;
+ break;
+ case 'punto_de_venta':
+ sdkPaymentMethod = PaymentMethod.CARD;
+ break;
+ case 'transferencia':
+ sdkPaymentMethod = PaymentMethod.BANK_TRANSFER;
+ break;
+ case 'pago_movil':
+ sdkPaymentMethod = PaymentMethod.MOBILE_PAYMENT;
+ break;
+ default:
+ sdkPaymentMethod = PaymentMethod.CASH;
+ }
+
+ // Construir el payload de la orden usando el método seleccionado
+ const orderPayload: CreateOrder = {
+ type: option === 'pickup' ? OrderType.PICKUP : OrderType.DELIVERY,
+ branchId: option === 'pickup' ? locationId || undefined : undefined,
+ userAddressId:
+ option === 'delivery' ? locationId || undefined : undefined,
+ products,
+ paymentMethod: sdkPaymentMethod,
+ ...(couponApplied && {
+ couponCode:
+ typeof couponDiscount === 'number' && couponDiscount > 0
+ ? 'COUPON'
+ : undefined,
+ }),
+ };
+
+ try {
+ // Enviar la orden al backend
+ const orderResponse = await OrderService.create(orderPayload);
+
+ if (!orderResponse?.id) {
+ setErrorMessage(
+ 'No pudimos procesar tu orden. Inténtalo nuevamente.',
+ );
+ }
+
+ // Redirigir a la pantalla de orden en progreso (step 2 y 3)
+ router.push({
+ pathname: '/in-progress-order',
+ params: {
+ orderNumber: orderResponse.id,
+ },
+ });
+ } catch (error) {
+ console.error('Error al procesar la orden:', error);
+ setErrorMessage('Ocurrió un error inesperado. Inténtalo nuevamente.');
+ }
+ return;
+ }
+ };
+
+ //const handleOpenMapModal = () => {
+ //if (selectedBranch) {
+ // setModalVisible(true); // Open the modal only if a branch is selected
+ // } else {
+ // console.error('No branch selected to display on the map.');
+ //}
+ // };
+
+ // const renderConfirmationContent = (status: OrderStatus | null) => {
+ // if (status === 'approved' && option === 'pickup') {
+ // return Tu pedido está listo para recoger.;
+ // }
+ // if (status === 'approved' && option === 'delivery') {
+ // return Tu pedido está en camino.;
+ // }
+ // return null;
+ // };
+
+ const handleApplyCoupon = (discountAmount: number) => {
+ dispatch(setCouponDiscount(discountAmount));
+ dispatch(setCouponApplied(true));
+ };
+
+ const [refreshing, setRefreshing] = useState(false); // <-- Agregado
+
+ const onRefresh = React.useCallback(() => {
+ setRefreshing(true);
+ // Aquí puedes recargar datos del carrito, usuario, etc.
+ // Simulación de recarga:
+ setTimeout(() => {
+ setRefreshing(false);
+ }, 1000);
+ }, []);
+
+ return (
+ <>
+ {
+ setValidationPopupVisible(false);
+ setEmailVerificationModalVisible(true);
+ },
+ }}
+ onClose={() => {
+ setValidationPopupVisible(false);
+ router.replace({
+ pathname: '/(tabs)',
+ });
+ }}
+ />
+ setEmailVerificationModalVisible(false)} // Close the modal
+ />
+
+ }
+ >
+
+ {/* Solo mostrar el step 1 */}
+
+
+
+
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+ {/* Solo el paso 1 */}
+
+ Opciones de Compra
+
+
+
+ }
+ selected={option === 'pickup'}
+ onPress={() => {
+ dispatch(setOption('pickup'));
+ dispatch(setLocationId(null));
+ }}
+ />
+
+
+ }
+ selected={option === 'delivery'}
+ onPress={() => {
+ dispatch(setOption('delivery'));
+ dispatch(setLocationId(null));
+ }}
+ />
+
+
+ dispatch(setLocationId(val))}
+ setSelectedBranch={setSelectedBranch}
+ />
+
+ dispatch(setPayment(val))}
+ />
+
+ {renderFooterMessage() && (
+
+ {renderFooterMessage()}
+
+ )}
+
+ dispatch(setCouponApplied(true))}
+ />
+
+
+ {couponApplied && (
+ <>
+
+
+ Subtotal después del Cupón:
+
+
+ ${subtotalAfterCoupon.toFixed(2)}
+
+
+ >
+ )}
+
+
+ Total:
+
+ ${formatPrice(total)}
+
+
+
+
+
+ setModalVisible(false)}
+ branchName={selectedBranch?.name || null}
+ branchCoordinates={
+ selectedBranch
+ ? {
+ latitude: selectedBranch.latitude,
+ longitude: selectedBranch.longitude,
+ }
+ : null
+ }
+ />
+
+
+ `${index + 1}. ${msg}`)
+ .join('\n')}
+ primaryButton={{
+ text: 'Aceptar',
+ onPress: () => setPopupVisible(false), // Close popup
+ }}
+ onClose={() => setPopupVisible(false)}
+ />
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ scrollContainer: {
+ flexGrow: 1,
+ },
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ backButton: {
+ position: 'absolute',
+ top: 16,
+ left: 16,
+ zIndex: 1,
+ padding: 12,
+ marginLeft: -12,
+ marginTop: -8,
+ },
+ steps: {
+ marginTop: 60,
+ },
+ purchaseOptionsTitle: {
+ paddingVertical: 16,
+ fontSize: FontSizes.h5.size,
+ color: Colors.textMain,
+ marginBottom: 30,
+ alignSelf: 'flex-start',
+ paddingHorizontal: 20,
+ },
+ radioContainer: {
+ width: '100%',
+ paddingHorizontal: 20,
+ marginTop: -30,
+ },
+ radioItem: {
+ marginBottom: 24,
+ },
+ paymentMethods: {
+ paddingHorizontal: 20,
+ },
+ footerMessage: {
+ fontSize: FontSizes.c1.size,
+ color: Colors.textLowContrast,
+ marginBottom: 10,
+ paddingHorizontal: 20,
+ marginTop: 10,
+ },
+ whiteBackgroundContainer: {
+ backgroundColor: '#FFFFFF',
+ width: '100%',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ marginTop: 20,
+ padding: 20,
+ },
+ spacer: {
+ height: 20,
+ },
+ totalContainer: {
+ width: '100%',
+ marginBottom: 10,
+ },
+ totalRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ width: '100%',
+ },
+ totalLabel: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ },
+ descuentoLabel: {
+ fontSize: FontSizes.b2.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ },
+ descuentoAmount: {
+ fontSize: FontSizes.b2.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ },
+ totalAmount: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ },
+ strikethroughAmount: {
+ fontSize: FontSizes.b2.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textLowContrast,
+ textDecorationLine: 'line-through',
+ },
+ checkoutButton: {
+ marginBottom: 16,
+ width: '100%',
+ height: 50,
+ marginTop: 15,
+ },
+ step2Title: {
+ marginBottom: -20,
+ },
+ paymentInfoFormContainer: {
+ paddingHorizontal: 20,
+ paddingTop: 0,
+ },
+ confirmationContainer: {
+ marginTop: 20,
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ },
+ confirmationMessage: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ textAlign: 'left',
+ marginBottom: 10,
+ },
+ sucursalText: {
+ fontSize: FontSizes.b2.size,
+ lineHeight: FontSizes.b2.lineHeight,
+ color: Colors.textLowContrast,
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ secondaryButton: {
+ marginBottom: 10,
+ backgroundColor: Colors.secondaryLight,
+ borderRadius: 8,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
+ primaryButton: {
+ marginTop: 10,
+ width: '100%',
+ },
+ errorMessage: {
+ color: Colors.semanticDanger,
+ fontSize: FontSizes.b2.size,
+ marginBottom: 10,
+ textAlign: 'center',
+ },
+});
+
+export default CheckoutScreen;
diff --git a/src/screens/DirectionChangeScreen.tsx b/src/screens/DirectionChangeScreen.tsx
new file mode 100644
index 0000000..500ef20
--- /dev/null
+++ b/src/screens/DirectionChangeScreen.tsx
@@ -0,0 +1,315 @@
+import React, { useEffect, useState } from 'react';
+import { View, StyleSheet, ScrollView } from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import Input from '../components/Input';
+import Button from '../components/Button';
+import { UserService } from '../services/user';
+import { UserAddressResponse } from '../types/api';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import { validateRequiredFields } from '../utils/validators';
+import Alert from '../components/Alerts';
+import { StateService } from '../services/state';
+import { State, CityResponse } from '../types/api';
+import Dropdown from '../components/Dropdown';
+import { CreateUserAddressRequest } from '@pharmatech/sdk';
+
+const ChangeDirectionScreen = () => {
+ const { id } = useLocalSearchParams<{ id: string }>();
+
+ const [address, setAddress] = useState(
+ {} as UserAddressResponse,
+ );
+ const [states, setStates] = useState([]);
+ const [cities, setCities] = useState([]);
+ const [selectedState, setSelectedState] = useState('');
+ const [selectedCity, setSelectedCity] = useState('');
+ const [isEditable, setIsEditable] = useState(false);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [showSuccessAlert, setShowSuccessAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [loading, setLoading] = useState(false);
+ const router = useRouter();
+
+ useEffect(() => {
+ if (!id) setIsEditable(true);
+ const fetchStates = async () => {
+ const states = await StateService.getStates(1, 40);
+ if (states.success) {
+ setStates(states.data?.results ?? []);
+ }
+ };
+ fetchStates();
+ }, []);
+
+ useEffect(() => {
+ const fetchCities = async () => {
+ const cities = await StateService.getCities(1, 40, selectedState);
+ if (cities.success) {
+ setCities(cities.data?.results ?? []);
+ }
+ };
+ fetchCities();
+ }, [selectedState]);
+
+ useEffect(() => {
+ const fetchAddress = async (id: string) => {
+ try {
+ const address = await UserService.getDirection(id);
+
+ if (address.success) {
+ setAddress(address.data);
+ const city = await StateService.getCity(address.data.cityId);
+ if (city.success) {
+ setSelectedCity(city.data?.name ?? '');
+ const state = city.data
+ ? await StateService.getState(city.data.state.id)
+ : null;
+ if (state && state.success && state.data)
+ setSelectedState(state.data.name);
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ if (id) fetchAddress(id);
+ }, []);
+
+ const submitAddress = async () => {
+ if (!validateRequiredFields([address.adress])) {
+ setShowErrorAlert(true);
+ setErrorMessage('Por favor completa todos los campos');
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const updatedAddress: CreateUserAddressRequest = {
+ adress: address.adress,
+ cityId: selectedCity,
+ additionalInformation: address.additionalInformation,
+ referencePoint: address.referencePoint,
+ latitude: null,
+ longitude: null,
+ };
+
+ const response = id
+ ? await UserService.updateDirection(updatedAddress, id)
+ : await UserService.saveDirection(updatedAddress);
+
+ if (response.success) {
+ setIsEditable(false);
+ alert('Dirección actualizada exitosamente');
+ } else {
+ alert('Error al actualizar la dirección');
+ }
+ setLoading(false);
+ } catch (error) {
+ console.error('Error al actualizar la dirección:', error);
+ setErrorMessage(String(error));
+ setShowErrorAlert(true);
+ }
+ };
+
+ return (
+
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ borderColor
+ />
+ )}
+ {showSuccessAlert && (
+ {
+ setShowSuccessAlert(false);
+ router.replace('/success');
+ }}
+ borderColor
+ />
+ )}
+
+
+ {isEditable ? (
+
+ {!id ? 'Agregar' : 'Editar'} Dirección
+
+ ) : (
+
+ Ver Dirección
+
+ )}
+
+
+ {/* Información de direccion */}
+
+ {isEditable ? (
+ state.name)}
+ borderColor={Colors.gray_100}
+ onSelect={(option) => {
+ const sState = states.find((state) => state.name === option);
+ if (sState) setSelectedState(sState.id);
+ }}
+ />
+ ) : (
+
+ )}
+ {isEditable ? (
+ city.name)}
+ borderColor={Colors.gray_100}
+ onSelect={(option) => {
+ const sCity = cities.find((city) => city.name === option);
+ if (sCity) setSelectedCity(sCity.id);
+ }}
+ />
+ ) : (
+
+ )}
+
+ setAddress({ ...address, adress: value })}
+ errorText="El campo no puede estar vacío"
+ backgroundColor={isEditable ? Colors.textWhite : undefined}
+ />
+
+ setAddress({ ...address, additionalInformation: value })
+ }
+ errorText="El campo no puede estar vacío"
+ backgroundColor={isEditable ? Colors.textWhite : undefined}
+ />
+
+ setAddress({ ...address, referencePoint: value })
+ }
+ errorText="El campo no puede estar vacío"
+ backgroundColor={isEditable ? Colors.textWhite : undefined}
+ />
+
+
+ {/* Botón de editar al final */}
+ {!isEditable ? (
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 20,
+ paddingTop: -20,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -162,
+ top: 20,
+ right: 0,
+ zIndex: 1000,
+ },
+ addressHeader: {
+ alignItems: 'center',
+ marginBottom: 15,
+ },
+ addressImage: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ marginBottom: 15,
+ borderWidth: 2,
+ borderColor: '#e0e0e0',
+ },
+ editButton: {
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ borderRadius: 20,
+ },
+ editButtonText: {
+ color: Colors.primary,
+ },
+ addressInfo: {
+ marginBottom: 20,
+ },
+ fieldContainer: {
+ marginBottom: 15,
+ },
+ fieldLabel: {
+ fontSize: 14,
+ color: '#666',
+ marginBottom: 5,
+ },
+ fieldValue: {
+ fontSize: 18,
+ color: '#333',
+ marginBottom: 10,
+ },
+ separator: {
+ height: 1,
+ backgroundColor: '#e0e0e0',
+ marginBottom: 15,
+ },
+ bottomEditButton: {
+ alignSelf: 'center',
+ marginTop: 20,
+ marginBottom: 40,
+ width: '50%',
+ alignItems: 'center',
+ },
+});
+
+export default ChangeDirectionScreen;
diff --git a/src/screens/DirectionCreateScreen.tsx b/src/screens/DirectionCreateScreen.tsx
new file mode 100644
index 0000000..3e6f478
--- /dev/null
+++ b/src/screens/DirectionCreateScreen.tsx
@@ -0,0 +1,230 @@
+import React, { useEffect, useState } from 'react';
+import { View, StyleSheet, ScrollView } from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import Input from '../components/Input';
+import Button from '../components/Button';
+import { UserService } from '../services/user';
+import { CreateUserAddressRequest } from '@pharmatech/sdk';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import { validateRequiredFields } from '../utils/validators';
+import Alert from '../components/Alerts';
+import { StateService } from '../services/state';
+import { State, CityResponse } from '../types/api';
+import Dropdown from '../components/Dropdown';
+
+const DirectionCreateScreen = () => {
+ const { latitude, longitude, fromCheckout } = useLocalSearchParams();
+
+ console.log('Parámetros recibidos en DirectionCreateScreen:', {
+ latitude,
+ longitude,
+ });
+
+ const [address, setAddress] = useState({
+ adress: '',
+ additionalInformation: '',
+ referencePoint: '',
+ latitude: latitude ? parseFloat(latitude as string) : null,
+ longitude: longitude ? parseFloat(longitude as string) : null,
+ cityId: '',
+ });
+
+ console.log('Estado inicial de address:', address);
+
+ const [states, setStates] = useState([]);
+ const [cities, setCities] = useState([]);
+ const [selectedState, setSelectedState] = useState('');
+ const [selectedCity, setSelectedCity] = useState('');
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [showSuccessAlert, setShowSuccessAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [loading, setLoading] = useState(false);
+ const router = useRouter();
+
+ useEffect(() => {
+ const fetchStates = async () => {
+ const states = await StateService.getStates(1, 40);
+ if (states.success) {
+ setStates(states.data?.results ?? []);
+ }
+ };
+ fetchStates();
+ }, []);
+
+ useEffect(() => {
+ const fetchCities = async () => {
+ const cities = await StateService.getCities(1, 40, selectedState);
+ if (cities.success) {
+ setCities(cities.data?.results ?? []);
+ }
+ };
+ fetchCities();
+ }, [selectedState]);
+
+ const submitAddress = async () => {
+ if (!validateRequiredFields([address.adress])) {
+ setShowErrorAlert(true);
+ setErrorMessage('Por favor completa todos los campos obligatorios');
+ return;
+ }
+
+ if (!address.latitude || !address.longitude) {
+ console.log('Coordenadas faltantes:', {
+ latitude: address.latitude,
+ longitude: address.longitude,
+ });
+ setShowErrorAlert(true);
+ setErrorMessage('Las coordenadas de la dirección son obligatorias');
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const newAddress: CreateUserAddressRequest = {
+ adress: address.adress,
+ latitude: address.latitude,
+ longitude: address.longitude,
+ cityId: selectedCity,
+ additionalInformation: address.additionalInformation || null,
+ referencePoint: address.referencePoint || null,
+ };
+
+ console.log('Datos enviados a la API:', newAddress);
+
+ const response = await UserService.saveDirection(newAddress);
+
+ if (response.success) {
+ setShowSuccessAlert(true);
+ setTimeout(() => {
+ if (fromCheckout === 'true') {
+ router.dismiss(2);
+ router.replace('/checkout');
+ } else {
+ router.replace('/direction');
+ }
+ }, 2000);
+ } else {
+ setShowErrorAlert(true);
+ setErrorMessage('Error al guardar la dirección');
+ }
+ } catch (error) {
+ console.error('Error al guardar la dirección:', error);
+ setShowErrorAlert(true);
+ setErrorMessage('Ocurrió un error al intentar guardar la dirección');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ borderColor
+ />
+ )}
+ {showSuccessAlert && (
+ setShowSuccessAlert(false)}
+ borderColor
+ />
+ )}
+
+
+
+ Agregar Dirección
+
+
+
+ {/* Información de direccion */}
+
+ state.name)}
+ borderColor={selectedState ? Colors.gray_100 : Colors.semanticDanger}
+ onSelect={(option) => {
+ const sState = states.find((state) => state.name === option);
+ if (sState) setSelectedState(sState.id);
+ }}
+ />
+ city.name)}
+ borderColor={selectedCity ? Colors.gray_100 : Colors.semanticDanger}
+ onSelect={(option) => {
+ const sCity = cities.find((city) => city.name === option);
+ if (sCity) setSelectedCity(sCity.id);
+ }}
+ />
+ setAddress({ ...address, adress: value })}
+ errorText="El campo no puede estar vacío"
+ backgroundColor={Colors.textWhite}
+ />
+
+ setAddress({ ...address, additionalInformation: value })
+ }
+ backgroundColor={Colors.textWhite}
+ />
+
+ setAddress({ ...address, referencePoint: value })
+ }
+ backgroundColor={Colors.textWhite}
+ />
+
+
+ {/* Botón de guardar */}
+ submitAddress()}
+ loading={loading}
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 20,
+ paddingTop: -20,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -163,
+ top: 20,
+ zIndex: 1000,
+ transform: [{ translateX: 20 }],
+ },
+ addressHeader: {
+ alignItems: 'center',
+ marginBottom: 15,
+ },
+ addressInfo: {
+ marginBottom: 20,
+ },
+});
+
+export default DirectionCreateScreen;
diff --git a/src/screens/DirectionScreen.tsx b/src/screens/DirectionScreen.tsx
new file mode 100644
index 0000000..0a0f58a
--- /dev/null
+++ b/src/screens/DirectionScreen.tsx
@@ -0,0 +1,270 @@
+import React, { useEffect, useState } from 'react';
+import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import Input from '../components/Input';
+import Button from '../components/Button';
+import { UserService } from '../services/user';
+import { UserAddressResponse } from '@pharmatech/sdk';
+import {
+ MagnifyingGlassIcon,
+ PencilIcon,
+ TrashIcon,
+} from 'react-native-heroicons/outline';
+import { useRouter } from 'expo-router';
+import Alert from '../components/Alerts';
+import Popup from '../components/Popup'; // Importamos el componente Popup
+
+const DirectionScreen = () => {
+ const [searchDirection, setSearchDirection] = useState('');
+ const [directionsList, setDirectionList] = useState<
+ UserAddressResponse[] | undefined
+ >(undefined);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [showInfoAlert, setShowInfoAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [selectedDirectionId, setSelectedDirectionId] = useState(
+ null,
+ ); // Dirección seleccionada para eliminar
+ const [showDeletePopup, setShowDeletePopup] = useState(false); // Controla la visibilidad del popup
+ const router = useRouter();
+
+ useEffect(() => {
+ const fetchDirection = async () => {
+ try {
+ const direction = await UserService.getUserDirections();
+
+ if (direction.success) {
+ if (direction.data.length > 0) {
+ setDirectionList(direction.data);
+ } else {
+ setShowInfoAlert(true);
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ setErrorMessage('Ha ocurrido un error');
+ setShowErrorAlert(true);
+ }
+ };
+
+ fetchDirection();
+ }, []);
+
+ const handleDeleteDirection = async () => {
+ if (!selectedDirectionId) return;
+
+ try {
+ // Llamada al endpoint para eliminar la dirección
+ await UserService.deleteAddress(selectedDirectionId);
+ setDirectionList((prev) =>
+ prev?.filter((direction) => direction.id !== selectedDirectionId),
+ );
+ setShowDeletePopup(false); // Cierra el popup
+ } catch (error) {
+ console.error('Error al eliminar la dirección:', error);
+ setErrorMessage('No se pudo eliminar la dirección');
+ setShowErrorAlert(true);
+ }
+ };
+
+ return (
+
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ borderColor
+ />
+ )}
+ {showInfoAlert && (
+ {
+ setShowInfoAlert(false);
+ }}
+ borderColor
+ />
+ )}
+
+ {/* Header con foto de perfil */}
+
+
+ Mis Direcciones
+
+
+
+
+ }
+ backgroundColor={Colors.textWhite}
+ />
+
+
+
+ {directionsList &&
+ directionsList.length > 0 &&
+ directionsList.map((direction, index) => (
+
+ {/* Contenedor del texto */}
+
+
+ {direction.adress}
+
+
+ {direction.nameCity}, {direction.nameState},{' '}
+
+ {direction.additionalInformation && (
+
+ {direction.additionalInformation}
+
+ )}
+ {direction.referencePoint && (
+
+ {direction.referencePoint}
+
+ )}
+
+
+ {/* Contenedor de los íconos */}
+
+
+ router.push(`change-direction/${direction.id}`)
+ }
+ >
+
+
+ {
+ setSelectedDirectionId(direction.id); // Establece la dirección seleccionada
+ setShowDeletePopup(true); // Muestra el popup
+ }}
+ >
+
+
+
+
+ ))}
+
+
+
+ router.push({
+ pathname: '/selectLocation',
+ params: {
+ fromCheckout: 'false', // Convertimos a cadena
+ },
+ })
+ }
+ />
+
+ {/* Popup de confirmación */}
+ setShowDeletePopup(false),
+ }}
+ onClose={() => setShowDeletePopup(false)}
+ />
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 20,
+ paddingTop: -20,
+ },
+ height: {
+ height: 64,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -162,
+ top: 20,
+ right: 0,
+ zIndex: 1000,
+ },
+ directionHeader: {
+ alignItems: 'center',
+ marginBottom: 15,
+ },
+ directionInfo: {
+ marginVertical: 20,
+ borderWidth: 1,
+ borderColor: Colors.gray_100,
+ borderRadius: 10,
+ backgroundColor: Colors.textWhite,
+ },
+ cardContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: 15,
+ borderBottomWidth: 1,
+ borderColor: Colors.gray_100,
+ },
+ textContainer: {
+ flex: 1,
+ marginRight: 10, // Espaciado entre el texto y los íconos
+ },
+ addressText: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.primary,
+ marginBottom: 4,
+ },
+ cityText: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.primary,
+ marginBottom: 4,
+ },
+ additionalInfoText: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.primary,
+ marginBottom: 4,
+ },
+ referencePointText: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.primary,
+ },
+ iconContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ width: 50,
+ },
+});
+
+export default DirectionScreen;
diff --git a/src/screens/InProgressOrderScreen.tsx b/src/screens/InProgressOrderScreen.tsx
new file mode 100644
index 0000000..cebbf75
--- /dev/null
+++ b/src/screens/InProgressOrderScreen.tsx
@@ -0,0 +1,463 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ RefreshControl,
+ ActivityIndicator,
+} from 'react-native';
+import PaymentStatusMessage from '../components/PaymentStatusMessage';
+import PaymentInfoForm from '../components/PaymentInfoForm';
+import OrderSummary from '../components/OrderSummary';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import { useLocalSearchParams } from 'expo-router';
+import { UserService } from '../services/user';
+import Steps from '../components/Steps';
+import Button from '../components/Button';
+import { truncateString } from '../utils/commons';
+import {
+ OrderStatus,
+ OrderDetailedResponse,
+ PharmaTech,
+ PaymentConfirmation,
+} from '@pharmatech/sdk';
+import * as SecureStore from 'expo-secure-store';
+import Popup from '../components/Popup';
+import {
+ initializeSocket,
+ disconnectSocket,
+} from '../lib/deliverySocket/deliverySocket';
+import { Socket } from 'socket.io-client';
+import Alert from '../components/Alerts';
+import { useRouter } from 'expo-router';
+import { useDispatch } from 'react-redux';
+import { clearCart } from '../redux/slices/cartSlice';
+import { resetCheckout } from '../redux/slices/checkoutSlice';
+
+const stepsLabels = [
+ 'Opciones de Compra',
+ 'Visualización de datos',
+ 'Confirmación de orden',
+];
+
+const InProgressOrderScreen = () => {
+ const { orderNumber } = useLocalSearchParams();
+
+ // Siempre inicia en step 3
+ const [step, setStep] = useState(3);
+ const [userName, setUserName] = useState('Usuario');
+ const [order, setOrder] = useState(null);
+ const [showValidationPopup, setShowValidationPopup] = useState(false);
+ const [paymentFormValid, setPaymentFormValid] = useState(false);
+ const [bank, setBank] = useState('');
+ const [reference, setReference] = useState('');
+ const [documentNumber, setDocumentNumber] = useState('');
+ const [phoneNumber, setPhoneNumber] = useState('');
+ const [backendResponse, setBackendResponse] = useState(null);
+ const [refreshing, setRefreshing] = useState(false);
+ const [isSubmittingPayment, setIsSubmittingPayment] = useState(false);
+ const router = useRouter();
+ const dispatch = useDispatch();
+
+ // Extrae la lógica de obtención de la orden
+ const fetchOrder = async () => {
+ if (!orderNumber) return;
+ try {
+ const sdk = PharmaTech.getInstance();
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ console.error('No JWT found in SecureStore');
+ return;
+ }
+ const orderData = await sdk.order.getById(orderNumber as string, jwt);
+ setOrder(orderData);
+
+ // Solo cambia a step 2 si status es 'approved' y paymentMethod es BANK_TRANSFER o MOBILE_PAYMENT
+ if (
+ orderData &&
+ orderData.status &&
+ orderData.status.toLowerCase() === 'approved' &&
+ orderData.paymentMethod &&
+ ['BANK_TRANSFER', 'MOBILE_PAYMENT'].includes(
+ orderData.paymentMethod.toUpperCase(),
+ )
+ ) {
+ setStep(2);
+ return;
+ }
+ setStep(3);
+ } catch (error) {
+ console.error('Error fetching order:', error);
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ const fetchUserName = async () => {
+ const response = await UserService.getProfile();
+ if (response.success && response.data) {
+ setUserName(response.data.firstName);
+ }
+ };
+ fetchUserName();
+ }, []);
+
+ useEffect(() => {
+ fetchOrder();
+ }, [orderNumber]);
+
+ // 1) socket-only: actualiza order.status
+ useEffect(() => {
+ let socket: Socket;
+ const setupSocket = async () => {
+ socket = await initializeSocket();
+ socket.connect();
+ socket.on('orderUpdated', (data: { orderId: string; status: string }) => {
+ console.log('[Socket] orderUpdated received →', data);
+ if (orderNumber && data.orderId === orderNumber) {
+ setOrder((prev) =>
+ prev ? { ...prev, status: data.status as OrderStatus } : prev,
+ );
+ setIsSubmittingPayment(false); // <-- Oculta el loader cuando llega el update
+ }
+ });
+ };
+ setupSocket();
+ return () => {
+ socket?.off('orderUpdated');
+ disconnectSocket();
+ };
+ }, []); // solo al montar
+
+ // 2) efecto separado: cada vez que 'order' cambia, recalcula el step
+ useEffect(() => {
+ if (!order) return;
+ const normalized = order.status.toLowerCase();
+ const pm = order.paymentMethod?.toLowerCase();
+ if (
+ normalized === 'approved' &&
+ (pm === 'bank_transfer' || pm === 'mobile_payment')
+ ) {
+ setStep(2);
+ } else {
+ setStep(3);
+ }
+ }, [order]);
+
+ const handleContinue = () => {
+ // Limpiar el carrito al salir del flujo de checkout
+ dispatch(clearCart());
+ dispatch(resetCheckout());
+ router.dismissAll();
+ router.replace({
+ pathname: '/(tabs)',
+ });
+ };
+
+ // Handler para pull-to-refresh
+ const onRefresh = async () => {
+ setRefreshing(true);
+ await fetchOrder();
+ };
+
+ return (
+ <>
+ {/* Alerta para mostrar la respuesta del backend */}
+ {backendResponse && (
+ setBackendResponse(null)}
+ />
+ )}
+
+ }
+ >
+
+
+
+
+ {step === 1 && (
+ <>
+
+ Opciones de Compra
+
+ >
+ )}
+ {/* Ocultar step 2 si el método de pago es CARD o CASH */}
+ {step === 2 &&
+ order &&
+ order.paymentMethod &&
+ !['CARD', 'CASH'].includes(order.paymentMethod.toUpperCase()) && (
+ <>
+
+ Visualización de datos
+
+
+ {isSubmittingPayment ? (
+
+
+
+ Procesando pago...
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ Total:
+
+
+ ${order.totalPrice ? order.totalPrice : ''}
+
+
+
+
+ {
+ if (!paymentFormValid) {
+ setShowValidationPopup(true);
+ return;
+ }
+ setIsSubmittingPayment(true); // <-- Muestra loader
+ try {
+ const sdk = PharmaTech.getInstance();
+ const jwt =
+ await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ setBackendResponse('No JWT found en SecureStore');
+ setIsSubmittingPayment(false);
+ return;
+ }
+ const paymentConfirmation: PaymentConfirmation = {
+ bank,
+ reference,
+ documentId: documentNumber,
+ phoneNumber,
+ orderId: order.id,
+ };
+ const response = await sdk.paymentConfirmation.create(
+ paymentConfirmation,
+ jwt,
+ );
+ setBackendResponse(
+ response && response.id
+ ? '¡Pago enviado correctamente!'
+ : 'Respuesta recibida del servidor.',
+ );
+ // NO cambies el step aquí, espera al socket
+ } catch (error: unknown) {
+ let errorMessage =
+ 'Error enviando confirmación de pago. Intenta nuevamente.';
+ function hasMessage(
+ e: unknown,
+ ): e is { message: string } {
+ return (
+ typeof e === 'object' &&
+ e !== null &&
+ 'message' in e &&
+ typeof (e as { message?: unknown }).message ===
+ 'string'
+ );
+ }
+ if (hasMessage(error)) {
+ errorMessage = error.message;
+ }
+ setBackendResponse(errorMessage);
+ setIsSubmittingPayment(false); // Oculta loader si hay error
+ }
+ }}
+ />
+
+
+ >
+ )}
+ {step === 3 && order && (
+ <>
+
+ Confirmacion de la orden
+
+
+ {/* Botón Volver al Home en un View con padding 20 */}
+
+
+
+
+
+
+
+
+
+ Total:
+
+
+ ${order.totalPrice ? order.totalPrice : ''}
+
+
+
+
+
+ >
+ )}
+
+
+ setShowValidationPopup(false),
+ }}
+ onClose={() => setShowValidationPopup(false)}
+ />
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ scrollContainer: {
+ flexGrow: 1,
+ justifyContent: 'flex-start', // Fuerza el contenido arriba SIEMPRE
+ },
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ steps: {
+ marginTop: 30,
+ padding: 20,
+ // No uses flex ni justifyContent aquí
+ },
+ stepLabel: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.textMain,
+ alignSelf: 'flex-start',
+ paddingVertical: 8,
+ },
+ purchaseOptionsTitle: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.textMain,
+ alignSelf: 'flex-start',
+ padding: 20,
+ },
+ confirmationContainer: {
+ marginTop: 20,
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ },
+ buttonContainer: {
+ marginTop: 20,
+ alignItems: 'flex-end',
+ },
+ nextButton: {
+ color: Colors.primary,
+ fontSize: FontSizes.h5.size,
+ fontWeight: 'bold',
+ padding: 10,
+ width: '100%',
+ height: 50,
+ marginTop: 15,
+ marginBottom: 16,
+ },
+ whiteBackgroundContainer: {
+ backgroundColor: '#FFFFFF',
+ padding: 20,
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ marginTop: 20,
+ },
+ paymentInfoFormContainer: {
+ paddingHorizontal: 20,
+ paddingTop: 0,
+ },
+ totalContainer: {
+ width: '100%',
+ marginBottom: 10,
+ },
+ totalRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ width: '100%',
+ },
+ totalLabel: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ },
+ totalAmount: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.primary,
+ },
+ checkoutButton: {
+ marginTop: 24,
+ width: '100%',
+ },
+ loaderContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 40,
+ },
+});
+
+export default InProgressOrderScreen;
diff --git a/src/screens/LoggedInPasswordRecoveryScreen.tsx b/src/screens/LoggedInPasswordRecoveryScreen.tsx
new file mode 100644
index 0000000..880e6f5
--- /dev/null
+++ b/src/screens/LoggedInPasswordRecoveryScreen.tsx
@@ -0,0 +1,359 @@
+import React, { useState, useCallback, useRef, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ TextInput,
+ Animated,
+} from 'react-native';
+import { useRouter } from 'expo-router';
+import Button from '../components/Button';
+import Input from '../components/Input';
+import PoppinsText from '../components/PoppinsText';
+import Steps from '../components/Steps';
+import Alert from '../components/Alerts';
+import { Colors, FontSizes } from '../styles/theme';
+import { AuthService } from '../services/auth';
+import { UserService } from '../services/user';
+
+export default function LoggedInPasswordRecoveryScreen() {
+ const router = useRouter();
+ const [currentStep, setCurrentStep] = useState(1); // Comenzar en el primer step visual (Código)
+ const fadeAnim = useRef(new Animated.Value(1)).current;
+ const [loading, setLoading] = useState(false);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [showSuccessAlert, setShowSuccessAlert] = useState(false);
+ const codeRefs = useRef>([]);
+ const [code, setCode] = useState(['', '', '', '', '', '']);
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+
+ useEffect(() => {
+ const sendOtpAutomatically = async () => {
+ setLoading(true);
+ try {
+ // Obtener el correo del usuario logueado
+ const profileResponse = await UserService.getProfile();
+ if (!profileResponse.success || !profileResponse.data?.email) {
+ throw new Error('No se pudo obtener el correo del usuario.');
+ }
+
+ const email = profileResponse.data.email;
+
+ // Enviar OTP automáticamente
+ const forgotPasswordResponse = await AuthService.forgotPassword(email);
+ if (!forgotPasswordResponse.success) {
+ throw new Error(
+ forgotPasswordResponse.error || 'Error al enviar el OTP.',
+ );
+ }
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ setErrorMessage(error.message || 'Error inesperado.');
+ } else {
+ setErrorMessage('Error inesperado.');
+ }
+ setShowErrorAlert(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ sendOtpAutomatically();
+ }, []);
+
+ const handleStepChange = (newStep: number) => {
+ // Eliminar cualquier alerta de error al cambiar de paso
+ setShowErrorAlert(false);
+ setErrorMessage('');
+
+ Animated.timing(fadeAnim, {
+ toValue: 0,
+ duration: 200,
+ useNativeDriver: true,
+ }).start(() => {
+ setCurrentStep(newStep);
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 200,
+ useNativeDriver: true,
+ }).start();
+ });
+ };
+
+ const handleVerifyCode = async () => {
+ const enteredCode = code.join('');
+ if (enteredCode.length !== 6) {
+ setErrorMessage('El código debe tener 6 dígitos');
+ setShowErrorAlert(true);
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const result = await AuthService.resetPassword(enteredCode);
+ if (result.success) {
+ handleStepChange(2); // Avanzar al siguiente paso (Recuperar Contraseña)
+ } else {
+ setCode(['', '', '', '', '', '']);
+ setErrorMessage(
+ result.error ||
+ 'El código ingresado es incorrecto. Inténtalo nuevamente.',
+ );
+ setShowErrorAlert(true);
+ }
+ } catch (error) {
+ console.error('Error al verificar el código:', error);
+ setErrorMessage(
+ 'Error inesperado al verificar el código. Inténtalo nuevamente.',
+ );
+ setShowErrorAlert(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleChangePassword = async () => {
+ if (newPassword !== confirmPassword) {
+ setErrorMessage('Las contraseñas no coinciden');
+ setShowErrorAlert(true);
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const result = await AuthService.updatePassword(
+ newPassword,
+ confirmPassword,
+ );
+ if (result.success) {
+ // Mostrar alerta de éxito
+ setShowSuccessAlert(true);
+
+ // Cerrar sesión y redirigir al login
+ setTimeout(async () => {
+ await AuthService.logout();
+ router.dismissAll(); // Cierra toda la pila de pantallas
+ router.replace('/login'); // Redirige al login
+ }, 2000); // Tiempo para mostrar la alerta
+ } else {
+ setErrorMessage(result.error || 'No se pudo cambiar la contraseña.');
+ setShowErrorAlert(true);
+ }
+ } catch (error) {
+ console.error('Error al cambiar la contraseña:', error);
+ setErrorMessage('Ocurrió un error inesperado.');
+ setShowErrorAlert(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const renderCodeInputs = useCallback(() => {
+ return (
+
+ {code.map((digit, index) => (
+ (codeRefs.current[index] = ref as TextInput)}
+ placeholder="•"
+ placeholderTextColor={Colors.textLowContrast}
+ style={styles.codeInput}
+ caretHidden={true}
+ keyboardType="number-pad"
+ maxLength={1}
+ value={digit}
+ onChangeText={(text) => {
+ const newCode = [...code];
+ const cleanedText = text.replace(/[^0-9]/g, '');
+ newCode[index] = cleanedText;
+ setCode(newCode);
+ if (cleanedText && index < 5) {
+ codeRefs.current[index + 1]?.focus();
+ }
+ }}
+ onKeyPress={({ nativeEvent }) => {
+ if (nativeEvent.key === 'Backspace' && !digit && index > 0) {
+ const newCode = [...code];
+ newCode[index - 1] = '';
+ setCode(newCode);
+ codeRefs.current[index - 1]?.focus();
+ }
+ }}
+ />
+ ))}
+
+ );
+ }, [code]);
+
+ const renderStepContent = () => {
+ switch (currentStep) {
+ case 1:
+ return (
+
+
+ Introducir código
+
+
+ Introduce el código de 6 dígitos que te enviamos a tu correo.
+
+ {renderCodeInputs()}
+
+
+ );
+
+ case 2:
+ return (
+
+
+ Recuperar contraseña
+
+
+ Ingresa tu nueva contraseña y confírmala.
+
+
+
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ />
+ )}
+ {showSuccessAlert && (
+ setShowSuccessAlert(false)}
+ />
+ )}
+
+
+
+
+
+
+ {renderStepContent()}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: Colors.bgColor,
+ justifyContent: 'flex-start',
+ paddingHorizontal: 20,
+ },
+ stepsWrapper: {
+ marginBottom: 12,
+ paddingHorizontal: 16,
+ },
+ stepContainer: {
+ width: '100%',
+ marginVertical: 16,
+ },
+ alertsContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -162,
+ top: 20,
+ right: 0,
+ zIndex: 1000,
+ },
+ stepTitle: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 24,
+ textAlign: 'center',
+ },
+ stepDescription: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textLowContrast,
+ textAlign: 'center',
+ marginBottom: 24,
+ },
+ codeContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginHorizontal: 8,
+ marginBottom: 16,
+ },
+ codeInput: {
+ width: 42,
+ height: 48,
+ borderWidth: 1,
+ borderColor: Colors.stroke,
+ textAlign: 'center',
+ fontFamily: 'Poppins_400Regular',
+ color: Colors.textMain,
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ borderRadius: 8,
+ backgroundColor: Colors.menuWhite,
+ includeFontPadding: false,
+ padding: 0,
+ textAlignVertical: 'center',
+ },
+ button: {
+ marginTop: 20,
+ width: '100%',
+ height: 50,
+ },
+});
diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx
index 292dd92..716c083 100644
--- a/src/screens/LoginScreen.tsx
+++ b/src/screens/LoginScreen.tsx
@@ -17,6 +17,7 @@ import { Colors, FontSizes } from '../styles/theme';
import Logo from '../assets/images/logos/PharmaTech_Logo.svg';
import GoogleLogo from '../assets/images/logos/Google_Logo.png';
import { AuthService } from '../services/auth';
+import { UserService } from '../services/user';
import Alert from '../components/Alerts';
export default function LoginScreen() {
@@ -32,18 +33,52 @@ export default function LoginScreen() {
if (loading) return;
setLoading(true);
- const result = await AuthService.login(email, password);
- setLoading(false);
+ try {
+ const result = await AuthService.login(email, password);
+ if (result.success) {
+ const profileResponse = await UserService.getProfile();
+ if (profileResponse.success) {
+ const { role: userRole } = profileResponse.data;
+ const { isValidated } = result.data!;
- if (result.success) {
- setShowSuccessAlert(true);
- setTimeout(() => {
- setShowSuccessAlert(false);
- router.replace('/(tabs)');
- }, 2000);
- } else {
+ // Mostrar alerta de éxito para todos los usuarios
+ setShowSuccessAlert(true);
+
+ setTimeout(() => {
+ setShowSuccessAlert(false);
+
+ if (!isValidated) {
+ // Redirigir al home con el modal de verificación de correo
+ router.replace({
+ pathname: '/(tabs)',
+ params: { showEmailVerification: 'true' },
+ });
+ } else if (userRole === 'delivery') {
+ // Redirigir a (delivery-tabs) si es un usuario de tipo delivery
+ router.replace('/(delivery-tabs)');
+ } else {
+ // Redirigir a (tabs) si es un usuario regular
+ router.replace('/(tabs)');
+ }
+ }, 2000);
+ } else {
+ console.error(
+ 'Error al obtener el perfil del usuario:',
+ profileResponse.error,
+ );
+ setShowErrorAlert(true);
+ setErrorMessage('Error al obtener el perfil del usuario.');
+ }
+ } else {
+ setShowErrorAlert(true);
+ setErrorMessage(result.error || 'Error al iniciar sesión.');
+ }
+ } catch (error) {
+ console.error('Error during login:', error);
setShowErrorAlert(true);
- setErrorMessage(result.error);
+ setErrorMessage('Error inesperado al iniciar sesión.');
+ } finally {
+ setLoading(false);
}
};
@@ -63,10 +98,7 @@ export default function LoginScreen() {
return (
-
+
{/* Alerts */}
{showErrorAlert && (
@@ -88,80 +120,86 @@ export default function LoginScreen() {
/>
)}
- {/* Logo */}
-
- {/* Titles */}
-
- Bienvenido
-
-
- Por favor introduce tus datos para iniciar sesión
-
- {/* Inputs */}
-
-
-
-
- {/* Forgot my password */}
-
-
- ¿Olvidaste tu contraseña?
+ {/* Logo */}
+
+ {/* Titles */}
+
+ Bienvenido
-
- {/* Login button */}
-
- {/* Google button */}
-
-
-
- Iniciar sesión con Google
+
+ Por favor introduce tus datos para iniciar sesión
-
- {/* Register link */}
-
-
- ¿No tienes cuenta?{' '}
-
- Regístrate
+ {/* Inputs */}
+
+
+
+
+ {/* Forgot my password */}
+
+
+ ¿Olvidaste tu contraseña?
-
-
-
+
+ {/* Login button */}
+
+ {/* Google button */}
+
+
+
+ Iniciar sesión con Google
+
+
+ {/* Register link */}
+
+
+ ¿No tienes cuenta?{' '}
+
+ Regístrate
+
+
+
+
+
);
@@ -178,10 +216,11 @@ const styles = StyleSheet.create({
},
alertContainer: {
position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -163,
top: 20,
- width: '100%',
zIndex: 1000,
- alignItems: 'center',
},
logo: {
width: 192,
diff --git a/src/screens/MenuScreen.tsx b/src/screens/MenuScreen.tsx
new file mode 100644
index 0000000..47df5a4
--- /dev/null
+++ b/src/screens/MenuScreen.tsx
@@ -0,0 +1,248 @@
+import { useEffect, useState } from 'react';
+import { StyleSheet, TouchableOpacity, View, SafeAreaView } from 'react-native';
+import { Colors } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import Avatar from '../components/Avatar';
+import { UserService } from '../services/user';
+import {
+ ArrowUpTrayIcon,
+ MapIcon,
+ ShieldCheckIcon,
+ ShoppingCartIcon,
+ UserIcon,
+ ArrowsRightLeftIcon,
+} from 'react-native-heroicons/outline';
+import { AuthService } from '../services/auth';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import Popup from '../components/Popup';
+
+export default function MenuScreen() {
+ const [userName, setUserName] = useState('');
+ const [userRole, setUserRole] = useState('');
+ const [isActive, setIsActive] = useState('');
+ const [isLogoutConfirmationVisible, setIsLogoutConfirmationVisible] =
+ useState(false);
+ const router = useRouter();
+ const { context } = useLocalSearchParams(); // Obtener el contexto desde los parámetros
+
+ const items = [
+ {
+ id: '1',
+ title: 'Mi Perfil',
+ icon: ,
+ onPress: () => router.push('/profile'),
+ },
+ {
+ id: '2',
+ title: 'Mis Direcciones',
+ icon: ,
+ onPress: () => router.push('/direction'),
+ },
+ {
+ id: '3',
+ title: 'Seguridad',
+ icon: ,
+ onPress: () => router.push('/change-password'),
+ },
+ {
+ id: '4',
+ title: 'Mis Pedidos',
+ icon: ,
+ onPress: () => router.push('/orders'),
+ },
+ ];
+
+ // Agregar la opción para usuarios de tipo "delivery" solo si el contexto no es "topBarDelivery"
+ if (userRole === 'delivery' && context !== 'topBarDelivery') {
+ items.push({
+ id: '5',
+ title: 'Ir al flujo de Delivery',
+ icon: ,
+ onPress: () => router.push('/(delivery-tabs)'),
+ });
+ }
+
+ // Agregar la opción para volver al flujo regular si el contexto es "topBarDelivery"
+ if (context === 'topBarDelivery') {
+ items.push({
+ id: '6',
+ title: 'Ir al flujo regular',
+ icon: ,
+ onPress: () => router.push('/(tabs)'),
+ });
+ }
+
+ const handleLogout = async () => {
+ try {
+ await AuthService.logout(); // Llama al método logout del servicio de autenticación
+ setIsLogoutConfirmationVisible(false); // Cierra el popup de confirmación
+
+ // Verificar si hay pantallas en la pila antes de llamar a dismissAll
+ if (router.canGoBack()) {
+ router.dismissAll();
+ }
+
+ router.replace('/login'); // Redirige a la pantalla de inicio de sesión
+ } catch (error) {
+ console.error('Error al cerrar sesión:', error);
+ }
+ };
+
+ useEffect(() => {
+ const fetchProfile = async () => {
+ try {
+ const profile = await UserService.getProfile();
+
+ if (profile.success) {
+ setUserName(profile.data.firstName + ' ' + profile.data.lastName);
+ setUserRole(profile.data.role); // Guardar el rol del usuario
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ };
+ fetchProfile();
+ setIsActive('');
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {userName}
+
+
+ Cuenta Personal
+
+
+
+
+ {items.map((item) => (
+ {
+ item.onPress();
+ }}
+ >
+ {item.icon && (
+ {item.icon}
+ )}
+
+ {item.title}
+
+
+ ))}
+
+
+
+ setIsLogoutConfirmationVisible(true)}
+ >
+
+
+
+
+ Cerrar Sesión
+
+
+
+
+
+
+
+
+ {/* Popup for Logout Confirmation */}
+ setIsLogoutConfirmationVisible(false),
+ }}
+ onClose={() => setIsLogoutConfirmationVisible(false)}
+ />
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ menu: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ zIndex: 100,
+ elevation: 10,
+ },
+ height: {
+ height: 64,
+ },
+ safeArea: {
+ flex: 1,
+ },
+ menuContent: {
+ flex: 1,
+ paddingHorizontal: 10,
+ justifyContent: 'space-between',
+ },
+ menuItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginVertical: 8,
+ paddingVertical: 3,
+ paddingHorizontal: 20,
+ borderColor: Colors.primary,
+ borderRadius: 5,
+ },
+ menuIcon: {
+ marginRight: 20,
+ fontSize: 24,
+ width: 30,
+ textAlign: 'center',
+ },
+ menuText: {
+ fontSize: 18,
+ color: '#333',
+ },
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ zIndex: 99,
+ },
+});
diff --git a/src/screens/NotificationsScreen.tsx b/src/screens/NotificationsScreen.tsx
new file mode 100644
index 0000000..f644e49
--- /dev/null
+++ b/src/screens/NotificationsScreen.tsx
@@ -0,0 +1,303 @@
+// src/screens/NotificationsScreen.tsx
+import React, { useEffect, useState, useCallback } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ ActivityIndicator,
+ TouchableOpacity,
+} from 'react-native';
+import { useNavigation, useFocusEffect } from '@react-navigation/native';
+import {
+ formatDistanceToNow,
+ parseISO,
+ differenceInDays,
+ format,
+} from 'date-fns';
+import { es } from 'date-fns/locale';
+import { ChevronLeftIcon } from 'react-native-heroicons/solid';
+import TopBar from '../components/TopBar';
+import PoppinsText from '../components/PoppinsText';
+import Alert from '../components/Alerts';
+import { Colors, FontSizes } from '../styles/theme';
+import { NotificationService } from '../services/notifications';
+import { SvgProps } from 'react-native-svg';
+
+// Importa tus SVG como componentes React
+import CompletedSvg from '../assets/images/notifications/f.svg';
+import InProgressSvg from '../assets/images/notifications/r.svg';
+import ApprovedSvg from '../assets/images/notifications/image.svg';
+import CanceledSvg from '../assets/images/notifications/w.svg';
+import ReadyForPickupSvg from '../assets/images/notifications/e.svg';
+import DeliverySvg from '../assets/images/notifications/m.svg';
+
+type NotificationItem = {
+ id: string;
+ title: string;
+ message: string;
+ createdAt: string;
+ isRead: boolean;
+ orderId: string;
+ status: string;
+};
+
+type NotificationResponse = {
+ id: string;
+ title: string;
+ message: string;
+ createdAt: string;
+ isRead: boolean;
+ order?: {
+ id: string;
+ status: string;
+ };
+};
+
+// Mapeo de status → componente SVG
+const notificationIcons: Record> = {
+ completed: CompletedSvg,
+ in_progress: InProgressSvg,
+ approved: ApprovedSvg,
+ canceled: CanceledSvg,
+ ready_for_pickup: ReadyForPickupSvg,
+ delivery: DeliverySvg,
+};
+
+export default function NotificationsScreen() {
+ const navigation = useNavigation();
+ const [notificationsList, setNotificationsList] = useState<
+ NotificationItem[]
+ >([]);
+ const [loading, setLoading] = useState(true);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+
+ // Carga inicial de notificaciones
+ useEffect(() => {
+ const fetchNotifications = async () => {
+ setLoading(true);
+ setShowErrorAlert(false);
+ try {
+ const res = await NotificationService.getNotifications();
+ if (!res.success || !Array.isArray(res.data)) {
+ const errorMsg =
+ !res.success && 'error' in res && res.error
+ ? res.error
+ : 'Respuesta inesperada del servidor';
+ throw new Error(errorMsg);
+ }
+
+ const items = res.data.map((nt: NotificationResponse) => {
+ const order = nt.order || { id: '', status: '' };
+ return {
+ id: nt.id,
+ title: nt.title,
+ message: nt.message.trim(),
+ createdAt: nt.createdAt,
+ isRead: !!nt.isRead,
+ orderId: order.id,
+ status: order.status,
+ };
+ });
+
+ setNotificationsList(items);
+ } catch (err: unknown) {
+ setErrorMessage(
+ err instanceof Error ? err.message : 'Error desconocido',
+ );
+ setShowErrorAlert(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchNotifications();
+ }, []);
+
+ // Marca todas como leídas al entrar o salir de la pantalla
+ useFocusEffect(
+ useCallback(() => {
+ const markAllRead = async () => {
+ try {
+ const toMark = notificationsList.filter((n) => !n.isRead);
+ if (toMark.length === 0) return;
+
+ await Promise.all(
+ toMark.map((n) => NotificationService.markAsRead(n.orderId)),
+ );
+ setNotificationsList((prev) =>
+ prev.map((n) => ({ ...n, isRead: true })),
+ );
+ } catch (e) {
+ console.warn(
+ 'Error al marcar todas las notificaciones como leídas:',
+ e,
+ );
+ }
+ };
+ markAllRead();
+ }, [notificationsList]),
+ );
+
+ const formatRelativeDate = (dateString: string) => {
+ const date = parseISO(dateString);
+ const daysDiff = differenceInDays(new Date(), date);
+ if (daysDiff <= 7) {
+ return formatDistanceToNow(date, { locale: es }).replace(
+ 'alrededor de ',
+ 'Hace ',
+ );
+ }
+ return format(date, 'yyyy-MM-dd', { locale: es });
+ };
+
+ const renderNotification = (nt: NotificationItem) => {
+ const Icon = notificationIcons[nt.status];
+ return (
+
+
+
+
+ {nt.title}
+
+ {formatRelativeDate(nt.createdAt)}
+
+
+ {nt.message}
+
+
+ );
+ };
+
+ return (
+
+
+ navigation.goBack()}
+ style={styles.backButton}
+ >
+
+
+ Volver
+
+
+
+ {showErrorAlert && (
+
+ setShowErrorAlert(false)}
+ borderColor
+ />
+
+ )}
+
+
+ Notificaciones
+
+
+ {loading ? (
+
+ ) : notificationsList.length === 0 ? (
+
+ No tienes notificaciones pendientes.
+
+ ) : (
+
+ {notificationsList.map(renderNotification)}
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ backButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 10,
+ marginTop: 8,
+ marginBottom: -4,
+ },
+ backText: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.primary,
+ },
+ alertContainer: {
+ position: 'absolute',
+ top: 20,
+ left: '50%',
+ transform: [{ translateX: -162 }],
+ width: 326,
+ zIndex: 1000,
+ },
+ header: {
+ flexDirection: 'row',
+ paddingTop: 16,
+ paddingHorizontal: 20,
+ },
+ title: {
+ fontSize: FontSizes.s1.size,
+ color: Colors.primary,
+ },
+ loader: {
+ marginTop: 40,
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ listContainer: {
+ paddingVertical: 10,
+ },
+ item: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.bgColor,
+ backgroundColor: Colors.bgColor,
+ },
+ unreadBackground: {
+ backgroundColor: '#FFFFFF',
+ },
+ textContainer: {
+ flex: 1,
+ marginLeft: 12,
+ },
+ itemHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 4,
+ },
+ date: {
+ color: Colors.textLowContrast,
+ fontSize: FontSizes.c1.size,
+ },
+ message: {
+ color: Colors.textLowContrast,
+ fontSize: FontSizes.c1.size,
+ flexWrap: 'wrap',
+ },
+});
diff --git a/src/screens/OrderDetailScreen.tsx b/src/screens/OrderDetailScreen.tsx
new file mode 100644
index 0000000..e8cd074
--- /dev/null
+++ b/src/screens/OrderDetailScreen.tsx
@@ -0,0 +1,296 @@
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ ActivityIndicator,
+ Image,
+} from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import Button from '../components/Button';
+import Alert from '../components/Alerts';
+import { UserService } from '../services/user';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import { OrderDetailedResponse } from '@pharmatech/sdk';
+import { truncateString } from '../utils/commons';
+import { useCart } from '../hooks/useCart';
+import { formatPrice } from '../utils/formatPrice';
+
+const OrderDetailScreen = () => {
+ const { id } = useLocalSearchParams<{ id: string }>();
+
+ const [order, setOrder] = useState(
+ undefined,
+ );
+ const [loading, setLoading] = useState(true);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [showSuccessAlert, setShowSuccessAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const { addToCart, getItemQuantity, updateCartQuantity } = useCart();
+ const router = useRouter();
+ const [subTotal, setSubTotal] = useState(0);
+ const [discount, setDiscount] = useState(0);
+
+ const handleReorder = () => {
+ order?.details.forEach((detail) => {
+ const existingQuantity = getItemQuantity(detail.productPresentation.id);
+ if (existingQuantity > 0) {
+ updateCartQuantity(
+ detail.productPresentation.id,
+ existingQuantity + detail.quantity,
+ );
+ } else {
+ addToCart({
+ id: detail.productPresentation.id,
+ quantity: detail.quantity,
+ price: detail.subtotal,
+ name: detail.productPresentation.product.name,
+ image: detail.productPresentation.product.images[0].url,
+ });
+ }
+ });
+ setShowSuccessAlert(true);
+ router.push('/cart');
+ };
+
+ useEffect(() => {
+ const fetchOrder = async () => {
+ try {
+ const order = await UserService.getOrder(id);
+
+ if (order.success) {
+ setOrder(order.data);
+
+ setSubTotal(
+ order.data.details.reduce(
+ (acc, t) => acc + t.price * t.quantity,
+ 0,
+ ),
+ );
+ setDiscount(
+ order.data.details.reduce(
+ (acc, t) => acc + ((t.price * t.discount) / 100) * t.quantity,
+ 0,
+ ),
+ );
+ console.log(order);
+ }
+ } catch (error) {
+ console.log(error);
+ setErrorMessage('Ocurrió un error');
+ setShowErrorAlert(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchOrder();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ borderColor
+ />
+ )}
+ {showSuccessAlert && (
+ {
+ setShowSuccessAlert(false);
+ }}
+ borderColor
+ />
+ )}
+
+
+
+ Detalle del pedido
+
+
+
+
+
+
+ Número de pedido:
+
+ #{order ? truncateString(order?.id, 8, '') : ''}
+
+
+ router.push(`order/tracking/${order?.id}`)}
+ />
+
+
+ {order?.details.map((detail, index) => (
+
+
+
+
+
+
+ {detail.productPresentation.product.name +
+ ' ' +
+ detail.productPresentation.presentation.name +
+ ' ' +
+ detail.productPresentation.presentation.quantity +
+ ' ' +
+ detail.productPresentation.presentation.measurementUnit}
+
+
+ ${formatPrice(detail.subtotal)}
+ Cantidad: {detail.quantity}
+
+
+ {/* */}
+
+ router.push(
+ `products/${detail.productPresentation.product.id}/presentation/${detail.productPresentation.presentation.id}`,
+ )
+ }
+ >
+ Ir al producto
+
+
+
+
+ ))}
+
+
+
+ Subtotal
+ ${formatPrice(subTotal)}
+
+
+
+ Descuentos
+
+
+ -${formatPrice(discount)}
+
+
+
+ {/* IVA
+${order?.totalPrice} */}
+
+
+ Total
+ ${formatPrice(subTotal - discount)}
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 20,
+ paddingTop: -20,
+ paddingBottom: 0,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '56%',
+ marginLeft: -162,
+ top: 40,
+ right: 0,
+ zIndex: 1000,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.bgColor,
+ },
+ orderHeader: {
+ alignItems: 'center',
+ marginBottom: 15,
+ },
+ orderImage: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ marginBottom: 15,
+ borderWidth: 2,
+ borderColor: '#e0e0e0',
+ },
+ orderInfo: {
+ marginBottom: 20,
+ },
+});
+
+export default OrderDetailScreen;
diff --git a/src/screens/OrderTrackingScreen.tsx b/src/screens/OrderTrackingScreen.tsx
new file mode 100644
index 0000000..8470eb0
--- /dev/null
+++ b/src/screens/OrderTrackingScreen.tsx
@@ -0,0 +1,478 @@
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ ActivityIndicator,
+ RefreshControl,
+ Modal,
+ TouchableOpacity,
+} from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import { useLocalSearchParams } from 'expo-router';
+import Alert from '../components/Alerts';
+import {
+ OrderDeliveryDetailedResponse,
+ OrderDetailedResponse,
+ OrderStatus,
+} from '@pharmatech/sdk';
+import { UserService } from '../services/user';
+import { formatDate, truncateString } from '../utils/commons';
+import {
+ CubeIcon,
+ PhoneIcon,
+ TruckIcon,
+ UserIcon,
+} from 'react-native-heroicons/outline';
+import {
+ BuildingStorefrontIcon,
+ MapPinIcon,
+ XMarkIcon,
+} from 'react-native-heroicons/solid';
+import VerticalStepper from '../components/VerticalStepper';
+import { DeliveryService } from '../services/delivery';
+import Button from '../components/Button';
+import DeliveryMap from '../components/DeliveryMap';
+import { useSelector } from 'react-redux';
+import { RootState } from '../redux/store';
+import {
+ initializeSocket,
+ disconnectSocket,
+} from '../lib/deliverySocket/deliverySocket';
+import { Socket } from 'socket.io-client';
+
+const OrderTrackingScreen = () => {
+ const { id } = useLocalSearchParams<{ id: string }>();
+
+ const [order, setOrder] = useState(
+ undefined,
+ );
+ const [loading, setLoading] = useState(true);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [showSuccessAlert, setShowSuccessAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [step, setStep] = useState(0);
+ const [refreshing, setRefreshing] = useState(false);
+ const [userAddress, setUserAddress] = useState('');
+ const [delivery, setDelivery] = useState<
+ OrderDeliveryDetailedResponse | undefined
+ >();
+ const [isMapModalVisible, setIsMapModalVisible] = useState(false);
+
+ // Estado para la ubicación del repartidor
+ const [deliveryLocation, setDeliveryLocation] = useState<{
+ latitude: number;
+ longitude: number;
+ } | null>(null);
+
+ const deliveryState = useSelector(
+ (state: RootState) => state.delivery.deliveryState[id as string] || 0,
+ );
+
+ const changeTrackingStatus = (status: OrderStatus) => {
+ switch (status) {
+ case OrderStatus.REQUESTED:
+ case OrderStatus.CANCELED:
+ setStep(0);
+ break;
+ case OrderStatus.APPROVED:
+ setStep(1);
+ break;
+ case OrderStatus.IN_PROGRESS:
+ case OrderStatus.READY_FOR_PICKUP:
+ setStep(2);
+ break;
+ case OrderStatus.COMPLETED:
+ setStep(3);
+ break;
+ default:
+ setStep(0);
+ break;
+ }
+ };
+
+ const fetchOrder = async () => {
+ try {
+ const order = await UserService.getOrder(id);
+ if (order.success) {
+ if (
+ order.data.orderDeliveries &&
+ order.data.orderDeliveries.length > 0
+ ) {
+ const delivery = await DeliveryService.getOrderDetails(
+ order.data.orderDeliveries[0].id,
+ );
+ setDelivery(delivery);
+ setUserAddress(
+ delivery.address.adress + '. \n' + delivery.address.referencePoint,
+ );
+ }
+ setOrder(order.data);
+ changeTrackingStatus(order.data.status);
+ }
+ } catch (error) {
+ console.error('Error al obtener la orden:', error);
+ setErrorMessage('Ocurrió un error');
+ setShowErrorAlert(true);
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ let socket: Socket;
+
+ const setupSocket = async () => {
+ try {
+ socket = await initializeSocket();
+ socket.connect();
+
+ socket.on('connect', () => {
+ console.log('WebSocket conectado en OrderTrackingScreen');
+ });
+
+ socket.on(
+ 'coordinatesUpdated',
+ (data: { latitude: number; longitude: number }) => {
+ setDeliveryLocation({
+ latitude: data.latitude,
+ longitude: data.longitude,
+ });
+ },
+ );
+
+ socket.on('connect_error', (error) => {
+ console.error(
+ 'Error de conexión al WebSocket en OrderTrackingScreen:',
+ error,
+ );
+ });
+
+ socket.on('disconnect', (reason) => {
+ console.warn(
+ 'WebSocket desconectado en OrderTrackingScreen. Razón:',
+ reason,
+ );
+ });
+ } catch (error) {
+ console.error(
+ 'Error configurando el WebSocket en OrderTrackingScreen:',
+ error,
+ );
+ }
+ };
+
+ setupSocket();
+
+ return () => {
+ if (socket) {
+ socket.off('coordinatesUpdated');
+ disconnectSocket();
+ }
+ };
+ }, [id]);
+ // --- FIN INTEGRACIÓN WEBSOCKET ---
+
+ useEffect(() => {
+ if (id) {
+ fetchOrder();
+ }
+ }, [id]);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ const onRefresh = async () => {
+ setRefreshing(true);
+ await fetchOrder();
+ };
+
+ const dateFormat: Intl.DateTimeFormatOptions = {
+ day: '2-digit',
+ month: 'long',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true,
+ };
+
+ const steps = [
+ {
+ title: 'Orden Confirmada',
+ description: order?.orderDeliveries
+ ? new Date(order?.updatedAt).toLocaleString('es-ES', dateFormat)
+ : '',
+ },
+ {
+ title:
+ order?.type === 'delivery' ? 'Repartidor en camino' : 'En preparación',
+ description: order?.orderDeliveries
+ ? new Date(order?.updatedAt).toLocaleString('es-ES', dateFormat)
+ : '',
+ },
+ {
+ title:
+ order?.type === 'delivery' ? 'Pedido entregado' : 'Listo para recoger',
+ description: order?.orderDeliveries
+ ? new Date(order?.updatedAt).toLocaleString('es-ES', dateFormat)
+ : '',
+ },
+ ];
+
+ return (
+
+ }
+ >
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ borderColor
+ />
+ )}
+ {showSuccessAlert && (
+ {
+ setShowSuccessAlert(false);
+ }}
+ borderColor
+ />
+ )}
+
+
+
+ Seguimiento del Pedido
+
+
+ {order && order.orderDeliveries && (
+
+
+
+ Orden ID: #{truncateString(order?.id, 8, '')}
+
+
+ Fecha del pedido: {formatDate(order?.createdAt)}
+
+
+
+ {order?.type === 'delivery' ? (
+ <>
+
+
+ Entrega estimada:{' '}
+ {new Date(
+ order?.orderDeliveries[0]?.estimatedTime,
+ ).toLocaleString('es-ES', {
+ day: '2-digit',
+ month: 'long',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true,
+ })}
+
+ >
+ ) : (
+ <>
+
+
+ Pickup:{' '}
+ {new Date(order?.updatedAt).toLocaleString('es-ES', {
+ day: '2-digit',
+ month: 'long',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true,
+ })}
+
+ >
+ )}
+
+
+
+
+ {/* Botón para abrir el mapa ampliado */}
+ {order?.type !== 'pickup' && (
+ setIsMapModalVisible(true)}
+ variant={'primary'}
+ style={{ marginBottom: 12 }}
+ />
+ )}
+ {/* Modal para el mapa ampliado */}
+ setIsMapModalVisible(false)}
+ >
+
+ setIsMapModalVisible(false)}
+ >
+
+
+
+
+
+ {/* Información de contacto y otros datos... */}
+
+
+ {order?.type === 'pickup' ? (
+
+
+
+
+
+ Dirección de Sucursal:
+
+ {order?.branch?.name}
+
+ {order?.branch?.address}.
+ {order?.branch?.city.name}
+
+
+ ) : (
+ <>
+
+
+
+
+
+
+ Dirección de Entrega:
+
+ {userAddress}
+
+
+
+
+
+
+
+
+ Nombre del Repartidor:
+
+
+ {order?.orderDeliveries[0].employee?.firstName}{' '}
+ {order?.orderDeliveries[0].employee?.lastName}
+
+
+
+
+
+
+
+
+
+ Teléfono de Contacto:
+
+
+ {order?.orderDeliveries[0].employee?.phoneNumber}
+
+
+
+ >
+ )}
+
+
+
+ )}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 20,
+ },
+ height: {
+ height: 64,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -162,
+ top: 20,
+ right: 0,
+ zIndex: 1000,
+ },
+ orderHeader: {
+ alignItems: 'center',
+ marginBottom: 15,
+ },
+ orderInfo: {
+ marginVertical: 5,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.bgColor,
+ },
+});
+
+export default OrderTrackingScreen;
diff --git a/src/screens/OrdersScreen.tsx b/src/screens/OrdersScreen.tsx
new file mode 100644
index 0000000..d2865b8
--- /dev/null
+++ b/src/screens/OrdersScreen.tsx
@@ -0,0 +1,231 @@
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ ActivityIndicator,
+} from 'react-native';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import { useRouter } from 'expo-router';
+import Alert from '../components/Alerts';
+import Button from '../components/Button';
+import { OrderResponse } from '@pharmatech/sdk';
+import { UserService } from '../services/user';
+import { truncateString } from '../utils/commons';
+import OrderBadge from '../components/OrderBadge';
+import { useCart } from '../hooks/useCart';
+import { formatPrice } from '../utils/formatPrice';
+
+const OrdersScreen = () => {
+ const [ordersList, setOrdersList] = useState(
+ undefined,
+ );
+ const [loading, setLoading] = useState(true);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [showInfoAlert, setShowInfoAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [showSuccessAlert, setShowSuccessAlert] = useState(false);
+ const { addToCart, getItemQuantity, updateCartQuantity } = useCart();
+ const router = useRouter();
+
+ const handleReorder = async (id: string) => {
+ const orderData = await UserService.getOrder(id);
+
+ if (orderData.success) {
+ orderData.data.details.forEach((detail) => {
+ const existingQuantity = getItemQuantity(detail.productPresentation.id);
+ if (existingQuantity > 0) {
+ updateCartQuantity(
+ detail.productPresentation.id,
+ existingQuantity + detail.quantity,
+ );
+ } else {
+ addToCart({
+ id: detail.productPresentation.id,
+ quantity: detail.quantity,
+ price: detail.subtotal,
+ name: detail.productPresentation.product.name,
+ image: detail.productPresentation.product.images[0].url,
+ });
+ }
+ });
+ router.push('/cart');
+ }
+ setShowSuccessAlert(true);
+ };
+
+ useEffect(() => {
+ const fetchOrders = async () => {
+ try {
+ const order = await UserService.getUserOrders();
+ if (order.success) {
+ if (order.data.results.length > 0) {
+ setOrdersList(order.data.results);
+ } else {
+ setShowInfoAlert(true);
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ setErrorMessage('Ha ocurrido un error');
+ setShowErrorAlert(true);
+ } finally {
+ setLoading(false); // Finalizar carga
+ }
+ };
+
+ fetchOrders();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ borderColor
+ />
+ )}
+ {showInfoAlert && (
+ {
+ setShowInfoAlert(false);
+ }}
+ borderColor
+ />
+ )}
+ {showSuccessAlert && (
+ {
+ setShowInfoAlert(false);
+ }}
+ borderColor
+ />
+ )}
+
+
+
+ Mis Pedidos
+
+
+
+ {ordersList &&
+ ordersList.length > 0 &&
+ ordersList.map((order, index) => (
+
+
+
+
+
+ #{order ? truncateString(order?.id, 8, '') : ''}
+
+
+ {new Date(order.createdAt).toLocaleDateString()}{' '}
+ {/* Formatear fecha */}
+
+
+ ${formatPrice(order.totalPrice)}
+
+
+
+ router.push(`order/${order.id}`)}
+ >
+
+ Ver detalles
+
+
+ handleReorder(order.id)}
+ />
+
+
+
+ ))}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 20,
+ paddingTop: -20,
+ },
+ height: {
+ height: 64,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '56%',
+ marginLeft: -162,
+ top: 40,
+ right: 0,
+ zIndex: 1000,
+ },
+ orderHeader: {
+ alignItems: 'center',
+ marginBottom: 15,
+ },
+ orderInfo: {
+ marginVertical: 5,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.bgColor,
+ },
+});
+
+export default OrdersScreen;
diff --git a/src/screens/PasswordChangeScreen.tsx b/src/screens/PasswordChangeScreen.tsx
new file mode 100644
index 0000000..9e35277
--- /dev/null
+++ b/src/screens/PasswordChangeScreen.tsx
@@ -0,0 +1,275 @@
+import React, { useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ KeyboardAvoidingView,
+ ScrollView,
+} from 'react-native';
+import { useRouter } from 'expo-router';
+import Button from '../components/Button';
+import Input from '../components/Input';
+import PoppinsText from '../components/PoppinsText';
+import Alert from '../components/Alerts';
+import { Colors, FontSizes } from '../styles/theme';
+import { AuthService } from '../services/auth';
+import {
+ validatePassword,
+ validatePasswordMatch,
+ validateRequiredFields,
+} from '../utils/validators';
+
+export default function ChangePasswordScreen() {
+ const router = useRouter();
+ const [currentPassword, setCurrentPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [alert, setAlert] = useState({
+ visible: false,
+ type: 'info',
+ title: '',
+ message: '',
+ });
+
+ const showAlert = (
+ type: 'success' | 'error' | 'info',
+ title: string,
+ message: string,
+ ) => {
+ setAlert({ visible: true, type, title, message });
+ setTimeout(() => setAlert({ ...alert, visible: false }), 3000);
+ };
+
+ const handleChangePassword = async () => {
+ if (loading) return;
+
+ if (
+ !validateRequiredFields([currentPassword, newPassword, confirmPassword])
+ ) {
+ showAlert('error', 'Error', 'Todos los campos son obligatorios.');
+ return;
+ }
+
+ if (!validatePassword(newPassword)) {
+ showAlert(
+ 'error',
+ 'Error',
+ 'La nueva contraseña debe tener al menos 8 caracteres.',
+ );
+ return;
+ }
+
+ if (!validatePasswordMatch(newPassword, confirmPassword)) {
+ showAlert('error', 'Error', 'Las contraseñas no coinciden.');
+ return;
+ }
+
+ if (newPassword === currentPassword) {
+ showAlert(
+ 'error',
+ 'Error',
+ 'La nueva contraseña debe ser diferente a la actual.',
+ );
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const result = await AuthService.changePassword(
+ currentPassword,
+ newPassword,
+ );
+ if (result.success) {
+ // Mostrar alerta de éxito
+ showAlert('success', 'Éxito', 'Contraseña cambiada correctamente.');
+
+ // Esperar un momento para que el usuario vea la alerta
+ setTimeout(async () => {
+ // Cerrar sesión y redirigir al login
+ await AuthService.logout();
+ router.dismissAll(); // Cierra toda la pila de pantallas
+ router.replace('/login'); // Redirige al login
+ }, 2000); // Tiempo para mostrar la alerta
+ } else {
+ showAlert(
+ 'error',
+ 'Error',
+ result.error || 'No se pudo cambiar la contraseña.',
+ );
+ }
+ } catch (error) {
+ console.error('Error al cambiar la contraseña:', error);
+ showAlert('error', 'Error', 'Ocurrió un error inesperado.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ {/* Contenedor de la alerta personalizada */}
+ {alert.visible && (
+
+ setAlert({ ...alert, visible: false })}
+ />
+
+ )}
+
+
+
+ {/* Título */}
+
+ Cambiar Contraseña
+
+
+ {/* Descripción debajo del título */}
+
+ Crea una nueva contraseña.
+
+
+ Asegúrate de que sea diferente a las anteriores por seguridad.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Enlace de recuperación de contraseña */}
+
+
+
+ ¿Olvidaste tu contraseña?{' '}
+
+ router.push('/loggedInPasswordRecovery')}
+ >
+
+ Ingresa aquí
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ paddingTop: 32,
+ paddingHorizontal: 20,
+ },
+ alertContainer: {
+ position: 'absolute',
+ top: 0,
+ left: 2,
+ right: 0,
+ zIndex: 1000,
+ padding: 16,
+ },
+ title: {
+ fontSize: FontSizes.h4.size,
+ lineHeight: FontSizes.h4.lineHeight,
+ marginBottom: 24,
+ color: Colors.textMain,
+ },
+ description: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ textAlign: 'center',
+ marginBottom: 0,
+ color: Colors.textLowContrast,
+ },
+ description2: {
+ marginTop: 0,
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ textAlign: 'center',
+ marginBottom: 22,
+ color: Colors.textLowContrast,
+ },
+ inputsContainer: {
+ width: '100%',
+ },
+ inputGroup: {
+ marginBottom: 16,
+ },
+ changePasswordButton: {
+ marginTop: 16,
+ width: '100%',
+ height: 50,
+ },
+ forgotPasswordContainer: {
+ alignSelf: 'flex-start',
+ },
+ forgotPasswordRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ forgotPasswordText: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.textMain,
+ },
+ forgotPasswordLink: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.secondaryLight,
+ },
+ button: {
+ marginTop: 24,
+ width: '100%',
+ height: 50,
+ },
+});
diff --git a/src/screens/PasswordRecoveryScreen.tsx b/src/screens/PasswordRecoveryScreen.tsx
index 5192ece..11537be 100644
--- a/src/screens/PasswordRecoveryScreen.tsx
+++ b/src/screens/PasswordRecoveryScreen.tsx
@@ -29,16 +29,32 @@ export default function PasswordRecoveryScreen() {
if (currentStep === 1) {
navigation.setOptions({
headerLeft: () => (
- navigation.goBack()}>
-
+ navigation.goBack()}
+ style={{
+ padding: 12, // Aumentado
+ marginLeft: -12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ }}
+ >
+
),
});
} else {
navigation.setOptions({
headerLeft: () => (
- handleStepChange(currentStep - 1)}>
-
+ handleStepChange(currentStep - 1)}
+ style={{
+ padding: 12, // Aumentado
+ marginLeft: -12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ }}
+ >
+
),
});
@@ -107,15 +123,28 @@ export default function PasswordRecoveryScreen() {
}
setLoading(true);
- const result = await AuthService.resetPassword(enteredCode);
- setLoading(false);
-
- if (result.success) {
- setAccessToken(result.data);
- handleStepChange(3);
- } else {
- setErrorMessage(result.error);
+ try {
+ const result = await AuthService.resetPassword(enteredCode);
+ if (result.success) {
+ setAccessToken(result.data);
+ handleStepChange(3); // Avanzar al siguiente paso
+ } else {
+ // Limpiar el estado del código y permitir un nuevo intento
+ setCode(['', '', '', '', '', '']);
+ setErrorMessage(
+ result.error ||
+ 'El código ingresado es incorrecto. Inténtalo nuevamente.',
+ );
+ setShowErrorAlert(true);
+ }
+ } catch (error) {
+ console.error('Error al verificar el código:', error);
+ setErrorMessage(
+ 'Error inesperado al verificar el código. Inténtalo nuevamente.',
+ );
setShowErrorAlert(true);
+ } finally {
+ setLoading(false);
}
};
diff --git a/src/screens/ProductDetailScreen.tsx b/src/screens/ProductDetailScreen.tsx
new file mode 100644
index 0000000..80c2f73
--- /dev/null
+++ b/src/screens/ProductDetailScreen.tsx
@@ -0,0 +1,798 @@
+import React, { useState, useRef, useEffect } from 'react';
+import {
+ View,
+ Image,
+ ScrollView,
+ StyleSheet,
+ TouchableOpacity,
+ SafeAreaView,
+ Dimensions,
+ NativeSyntheticEvent,
+ NativeScrollEvent,
+ ActivityIndicator,
+} from 'react-native';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import TopBar from '../components/TopBar';
+import { useCart } from '../hooks/useCart';
+import { Product as CardProduct } from '../types/Product';
+import { CheckCircleIcon, ChevronLeftIcon } from 'react-native-heroicons/solid';
+import { Colors, FontSizes } from '../styles/theme';
+import Dropdown from '../components/Dropdown';
+import CardButton from '../components/CardButton';
+import PoppinsText from '../components/PoppinsText';
+import { ProductService } from '../services/products';
+import { TruckIcon } from 'react-native-heroicons/outline';
+import Carousel from '../components/Carousel';
+import { StateService } from '../services/state';
+import { State } from '../types/api';
+import { InventoryService } from '../services/inventory';
+import { useNavigation } from '@react-navigation/native';
+import BranchMap from '../components/BranchMap';
+import {
+ ProductImage,
+ ProductPresentationDetailResponse,
+ ProductPresentationResponse,
+ InventoryResponse,
+} from '@pharmatech/sdk';
+import Button from '../components/Button';
+import { BranchService } from '../services/branches';
+import { formatPrice } from '../utils/formatPrice';
+import Alert from '../components/Alerts';
+
+const ProductDetailScreen: React.FC = () => {
+ const { id, productId } = useLocalSearchParams<{
+ id: string;
+ productId: string;
+ }>();
+ const navigation = useNavigation(); // Obtén la instancia de navegación
+
+ const [showMap, setShowMap] = useState(false);
+ const [inventory, setInventory] = useState([]);
+ const [states, setStates] = useState([]);
+ const [selectedState, setSelectedState] = useState('');
+ const [product, setProduct] = useState();
+ const [images, setImages] = useState();
+ const [presentations, setPresentations] =
+ useState();
+ const [products, setProducts] = useState([]);
+ const [currentImageIndex, setCurrentImageIndex] = useState(0);
+ const [isLoadingInventory, setIsLoadingInventory] = useState(false);
+ const [showNoStockAlert, setShowNoStockAlert] = useState(false);
+ const imagesScrollRef = useRef(null);
+ //const discount = 10;
+ const router = useRouter();
+
+ const { cartItems, addToCart, getItemQuantity, updateCartQuantity } =
+ useCart();
+
+ const getQuantity = (): number => {
+ return product?.id ? getItemQuantity(product.id) : 0;
+ };
+
+ const obtainProducts = async () => {
+ const productsData = await ProductService.getProducts(1, 20);
+
+ if (productsData.success) {
+ const pd = productsData.data.results;
+ const carouselProducts = pd.map((p) => ({
+ id: p.id,
+ presentationId: p.presentation.id,
+ productId: p.product.id,
+ imageUrl: p.product.images[0].url,
+ name:
+ p.product.name +
+ ' ' +
+ p.presentation.name +
+ ' ' +
+ p.presentation.quantity +
+ ' ' +
+ p.presentation.measurementUnit,
+ category: p.product.categories[0].name,
+ //originalPrice: p.price,
+ //discount: 10,
+ finalPrice: p.price,
+ quantity: getItemQuantity(p.product.id),
+ getQuantity: (quantity: number) => {
+ addToCart({
+ id: p.id,
+ name:
+ p.product.name +
+ ' ' +
+ p.presentation.name +
+ ' ' +
+ p.presentation.quantity +
+ ' ' +
+ p.presentation.measurementUnit,
+ price: p.price,
+ quantity,
+ image: p.product.images[0].url,
+ });
+ updateCartQuantity(p.id, quantity);
+ },
+ }));
+
+ setProducts(carouselProducts);
+ } else {
+ console.log(productsData.error);
+ }
+ };
+
+ const changeState = async (name: string) => {
+ if (!states) return;
+ const state = states.find((p) => p.name === name);
+ if (state) {
+ setSelectedState(state.id);
+ }
+ };
+
+ const changePresentation = async (description: string) => {
+ if (!presentations) return;
+ const presentation = presentations.find(
+ (p) =>
+ product?.product.name +
+ ' ' +
+ p.presentation.name +
+ ' ' +
+ p.presentation.quantity +
+ ' ' +
+ p.presentation.measurementUnit ===
+ description,
+ );
+ if (presentation) {
+ router.replace(
+ '/products/' +
+ productId +
+ '/presentation/' +
+ presentation.presentation.id,
+ );
+ }
+ };
+
+ useEffect(() => {
+ obtainProducts();
+ }, [cartItems]);
+
+ useEffect(() => {
+ const obtainProduct = async () => {
+ const productPresentations =
+ await ProductService.getProductPresentations(productId);
+ const productImages = await ProductService.getProductImages(productId);
+ const productData = await ProductService.getPresentation(productId, id);
+ const states = await StateService.getStates(1, 40);
+
+ if (productData.success) setProduct(productData.data);
+ if (productImages.success) setImages(productImages.data);
+ if (productPresentations.success) {
+ const presentations = productPresentations.data.filter(
+ (item) => item.presentation.id !== id,
+ );
+ setPresentations(presentations);
+ }
+ if (states.success) setStates(states.data?.results || []);
+ };
+
+ obtainProduct();
+ }, []);
+
+ useEffect(() => {
+ const loadBranchesWithStock = async () => {
+ if (!selectedState || !product) return;
+
+ setInventory([]); // Limpiar el estado antes de cargar nuevos datos
+ setIsLoadingInventory(true); // Mostrar el indicador de carga
+
+ try {
+ // Obtener las sucursales del estado seleccionado
+ const branchesRes = await BranchService.findAll({
+ page: 1,
+ limit: 100,
+ stateId: selectedState,
+ });
+
+ const results: InventoryResponse[] = [];
+
+ for (const branch of branchesRes.results) {
+ // Obtener el inventario de la sucursal para la presentación del producto
+ const inventoryRes = await InventoryService.getBranchInventory({
+ branchId: branch.id,
+ page: 1,
+ limit: 100,
+ });
+
+ // Validar que inventoryRes.results sea un array
+ if (!Array.isArray(inventoryRes.results)) {
+ console.error(
+ 'inventoryRes.results no es un array:',
+ inventoryRes.results,
+ );
+ continue;
+ }
+
+ // Validar que product esté definido
+ if (!product || !product.id) {
+ console.error(
+ 'El producto no está definido o no tiene un ID:',
+ product,
+ );
+ continue;
+ }
+
+ // Buscar el inventario que coincida con la presentación del producto
+ const match = inventoryRes.results.find(
+ (inv) =>
+ inv.productPresentation &&
+ inv.productPresentation.id === product.id,
+ );
+
+ if (match && match.stockQuantity > 0) {
+ results.push(match); // Agregar directamente el objeto InventoryResponse
+ }
+ }
+
+ setInventory(results); // Actualizar el estado con los datos obtenidos
+ } catch (error) {
+ console.error('Error cargando sucursales con stock:', error);
+ } finally {
+ setIsLoadingInventory(false); // Ocultar el indicador de carga
+ }
+ };
+
+ loadBranchesWithStock();
+ }, [selectedState, product]);
+
+ const handleScroll = (event: NativeSyntheticEvent) => {
+ const contentOffsetX = event.nativeEvent.contentOffset.x;
+ const newIndex = Math.round(contentOffsetX / width);
+ setCurrentImageIndex(newIndex);
+ };
+
+ const scrollToImage = (index: number) => {
+ setCurrentImageIndex(index);
+ if (imagesScrollRef.current) {
+ imagesScrollRef.current.scrollTo({
+ x: index * width,
+ animated: true,
+ });
+ }
+ };
+
+ // const handleRating = (rating: number) => {
+ // setUserRating(rating);
+ // // Aquí podrías enviar la calificación a tu API
+ // console.log('Calificación enviada:', rating);
+ // };
+
+ // const RatingStars = () => {
+ // return (
+ //
+ // {[1, 2, 3, 4, 5].map((star) => (
+ // handleRating(star)}
+ // onPressIn={() => setHoverRating(star)}
+ // onPressOut={() => setHoverRating(0)}
+ // >
+ //
+ //
+ // ))}
+ //
+ // );
+ // };
+
+ const getProductDiscount = () => {
+ // Busca promo en product.promo
+ if (product?.promo && typeof product.promo.discount === 'number') {
+ return product.promo.discount;
+ }
+ return 0;
+ };
+
+ const getOriginalPrice = () => product?.price ?? 0;
+ const getDiscount = () => getProductDiscount();
+ const getFinalPrice = () => {
+ const original = getOriginalPrice();
+ const discount = getDiscount();
+ return discount > 0 ? (original * (100 - discount)) / 100 : original;
+ };
+
+ console.log('Stock actual:', product?.stock);
+
+ return (
+
+
+ {/* Botón de volver */}
+ navigation.goBack()} // Navega a la pantalla anterior
+ style={{
+ paddingHorizontal: 10,
+ marginBottom: -4,
+ flexDirection: 'row',
+ alignSelf: 'flex-start',
+ }}
+ >
+
+
+ Volver
+
+
+
+ {/* ALERTA EN LA PARTE SUPERIOR */}
+ {showNoStockAlert && (
+
+ setShowNoStockAlert(false)}
+ />
+
+ )}
+
+
+ {/* Carrusel de imágenes */}
+
+ {images?.map((image, index) => (
+
+ ))}
+
+
+ {/* Indicadores de imágenes */}
+
+ {images?.map((_, index) => (
+ scrollToImage(index)}
+ >
+
+
+ ))}
+
+
+
+ {product ? (
+ `${product.product.name} ${product.presentation.name} ${product.presentation.quantity} ${product.presentation.measurementUnit}`
+ ) : (
+
+ )}
+
+
+ {/*
+ {userRating > 0 && (
+
+ ¡Gracias por tu calificación de {userRating} estrella
+ {userRating !== 1 ? 's' : ''}!
+
+ )} */}
+
+
+ {product?.presentation.description}
+
+
+ {/* Información del producto */}
+
+
+ {product ? (
+ getDiscount() > 0 ? (
+ <>
+
+ ${formatPrice(getFinalPrice())}
+
+
+ -{getDiscount()}%
+
+ >
+ ) : (
+
+ ${formatPrice(getOriginalPrice())}
+
+ )
+ ) : (
+
+ )}
+
+
+ Selecciona la presentación
+
+
+ {product ? (
+
+ product?.product.name +
+ ' ' +
+ p.presentation.name +
+ ' ' +
+ p.presentation.quantity +
+ ' ' +
+ p.presentation.measurementUnit,
+ ) || []
+ }
+ borderColor={Colors.gray_100}
+ onSelect={(e) => changePresentation(e)}
+ />
+ ) : (
+
+ )}
+
+
+ Disponibilidad en sucursales
+
+
+ Selecciona un estado para ver disponibilidad
+
+
+ state.name)}
+ borderColor={Colors.gray_100}
+ onSelect={(e) => changeState(e)}
+ />
+
+
+ {selectedState ? (
+ isLoadingInventory ? (
+
+ ) : inventory && inventory.length > 0 ? (
+ inventory.map((inv) => (
+
+
+
+ {inv.branch.name}
+
+
+ {inv.branch.address}
+
+
+
+
+
+ {inv.stockQuantity} unidades{' '}
+
+
+
+
+
+
+ Envio en menos de 3h
+
+
+
+
+ ))
+ ) : (
+
+ No hay productos disponibles
+
+ )
+ ) : null}
+
+
+ {selectedState ? (
+ showMap ? (
+
+ {inventory && inventory.length > 0 ? (
+ ({
+ id: inv.branch.id,
+ name: inv.branch.name,
+ address: inv.branch.address,
+ latitude: inv.branch.latitude,
+ longitude: inv.branch.longitude,
+ stockQuantity: inv.stockQuantity,
+ }))}
+ />
+ ) : (
+
+ No hay productos disponibles
+
+ )}
+
+ ) : (
+
+ setShowMap(true)}
+ variant={
+ selectedState && inventory.length > 0
+ ? 'primary'
+ : 'disabled'
+ }
+ />
+
+ )
+ ) : null}
+
+
+ Productos relacionados
+
+
+
+
+
+
+
+
+ setShowNoStockAlert(true)}
+ getValue={(quantity) => {
+ const stock = product?.stock ?? 0;
+ // Si no hay stock o la cantidad supera el stock, muestra alerta y no actualices
+ if (stock === 0 || quantity > stock) {
+ setShowNoStockAlert(true);
+ return;
+ }
+ // Si la cantidad es 0, elimina del carrito
+ if (product?.id && quantity === 0) {
+ updateCartQuantity(product.id, 0);
+ return;
+ }
+ // Si hay stock y cantidad válida, agrega o actualiza en el carrito
+ if (product?.id && quantity > 0) {
+ const promo = product.promo;
+ const discount =
+ typeof promo?.discount === 'number' ? promo.discount : 0;
+ updateCartQuantity(product.id, quantity);
+ addToCart({
+ id: product.id,
+ name:
+ product.product.name +
+ ' ' +
+ product.presentation.name +
+ ' ' +
+ product.presentation.quantity +
+ ' ' +
+ product.presentation.measurementUnit,
+ price: product.price,
+ quantity,
+ image: images?.[0]?.url || 'https://via.placeholder.com/150',
+ discount,
+ });
+ }
+ }}
+ />
+
+
+
+ );
+};
+
+const { width } = Dimensions.get('window');
+
+const styles = StyleSheet.create({
+ container: {
+ paddingTop: 15,
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ productImage: {
+ width: width,
+ height: 200,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -163,
+ top: 20,
+ zIndex: 1000,
+ },
+ imageIndicators: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginVertical: 8,
+ },
+ imageIndicator: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: '#CCCCCC',
+ marginHorizontal: 4,
+ },
+ activeImageIndicator: {
+ backgroundColor: Colors.gray_500,
+ width: 17,
+ },
+ productInfo: {
+ padding: 16,
+ paddingVertical: 0,
+ },
+ priceRatingContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ price: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: Colors.primary,
+ marginRight: 10,
+ },
+ originalPrice: {
+ fontSize: FontSizes.b1.size,
+ color: Colors.disableText,
+ textDecorationLine: 'line-through',
+ marginRight: 14,
+ },
+ discountBadge: {
+ fontSize: FontSizes.c1.size,
+ backgroundColor: Colors.semanticInfo,
+ borderRadius: 5,
+ padding: 4,
+ color: Colors.textWhite,
+ marginRight: 10,
+ },
+ productName: {
+ textAlign: 'center',
+ fontSize: FontSizes.h5.size,
+ color: Colors.textMain,
+ margin: 20,
+ },
+ description: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.textLowContrast,
+ marginHorizontal: 20,
+ marginBottom: 20,
+ lineHeight: 20,
+ },
+ sectionTitle: {
+ fontSize: FontSizes.s2.size,
+ color: Colors.textLowContrast,
+ marginBottom: 3,
+ },
+ quantitySelector: {
+ flexDirection: 'row',
+ marginBottom: 8,
+ },
+ ratingStarsContainer: {
+ flexDirection: 'row',
+ marginBottom: 20,
+ justifyContent: 'center',
+ },
+ starIcon: {
+ marginHorizontal: 4,
+ },
+ ratingFeedback: {
+ textAlign: 'center',
+ color: '#4CAF50',
+ marginBottom: 20,
+ fontStyle: 'italic',
+ },
+ mapContainer: {
+ marginVertical: 8,
+ flex: 1,
+ alignItems: 'center',
+ },
+ availableContainer: {
+ marginBottom: 8,
+ backgroundColor: Colors.textWhite,
+ borderRadius: 15,
+ borderWidth: 1,
+ borderColor: Colors.gray_100,
+ },
+ availableCard: {
+ marginBottom: 10,
+ justifyContent: 'space-between',
+ borderBottomWidth: 1,
+ borderColor: Colors.gray_100,
+ },
+ cardButtonContainer: {
+ position: 'relative',
+ top: -40,
+ left: 120,
+ maxWidth: '65%',
+ alignItems: 'flex-end',
+ zIndex: 999,
+ },
+});
+
+export default ProductDetailScreen;
diff --git a/src/screens/ProfileScreen.tsx b/src/screens/ProfileScreen.tsx
new file mode 100644
index 0000000..1e60176
--- /dev/null
+++ b/src/screens/ProfileScreen.tsx
@@ -0,0 +1,375 @@
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ Image,
+} from 'react-native';
+import Avatar from '../components/Avatar';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import Input from '../components/Input';
+import Button from '../components/Button';
+import { UserService } from '../services/user';
+import { UpdateUser, UserList } from '@pharmatech/sdk';
+import { PencilIcon, TrashIcon } from 'react-native-heroicons/outline';
+import DatePickerInput from '../components/DatePickerInput';
+import * as ImagePicker from 'expo-image-picker';
+import {
+ validateDateFormat,
+ validatePhoneNumberLength,
+ validateRequiredFields,
+} from '../utils/validators';
+import Alert from '../components/Alerts';
+import { ImageService } from '../services/images';
+
+const formatDate = (dateString: Date): string => {
+ const date = new Date(dateString);
+ const day = String(date.getDate()).padStart(2, '0');
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const year = date.getFullYear();
+ return `${year}-${month}-${day}`;
+};
+
+const ProfileScreen = () => {
+ const [profile, setProfile] = useState({} as UserList);
+ const [image, setImage] = useState(null);
+ const [isEditable, setIsEditable] = useState(false);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [showSuccessAlert, setShowSuccessAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ const fetchProfile = async () => {
+ try {
+ const profile = await UserService.getProfile();
+
+ if (profile.success) {
+ setProfile(profile.data);
+ setImage(profile.data.profile.profilePicture);
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ fetchProfile();
+ }, []);
+
+ const submitProfile = async () => {
+ if (
+ !validateRequiredFields([
+ profile.firstName,
+ profile.lastName,
+ profile.phoneNumber,
+ formatDate(profile.profile?.birthDate),
+ ])
+ ) {
+ setShowErrorAlert(true);
+ setErrorMessage('Por favor completa todos los campos');
+ return;
+ }
+ if (!validateDateFormat(formatDate(profile.profile?.birthDate))) {
+ setShowErrorAlert(true);
+ setErrorMessage('Formato de fecha inválido (Use YYYY-MM-DD)');
+ return;
+ }
+
+ // Validar que el teléfono tenga entre 8 y 15 caracteres
+ if (profile.phoneNumber.length < 8 || profile.phoneNumber.length > 15) {
+ setShowErrorAlert(true);
+ setErrorMessage(
+ 'El número de teléfono debe tener entre 8 y 15 caracteres',
+ );
+ return;
+ }
+ setLoading(true);
+ try {
+ const updatedProfile: Partial = {
+ firstName: profile.firstName,
+ lastName: profile.lastName,
+ birthDate: profile.profile?.birthDate.toString(),
+ phoneNumber: profile.phoneNumber,
+ };
+
+ if (image !== null) {
+ const imageResponse = await ImageService.uploadImage(image);
+ if (imageResponse) {
+ updatedProfile.profilePicture = imageResponse.secure_url; // Ahora accede correctamente a secure_url
+ }
+ } else {
+ updatedProfile.profilePicture = 'https://via.placeholder.com/150';
+ }
+
+ const response = await UserService.updateProfile(updatedProfile);
+
+ if (response.success) {
+ setIsEditable(false);
+ setShowSuccessAlert(true);
+ } else {
+ setShowErrorAlert(true);
+ setErrorMessage('Error al actualizar el perfil');
+ }
+ setLoading(false);
+ } catch (error) {
+ console.error('Error al actualizar el perfil:', error);
+ alert('Ocurrió un error al intentar actualizar el perfil');
+ }
+ };
+
+ const handleImageUpload = async () => {
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ['images'],
+ allowsEditing: true,
+ aspect: [4, 3],
+ quality: 1,
+ });
+
+ console.log(result);
+
+ if (!result.canceled) {
+ setImage(result.assets[0].uri);
+ }
+ };
+
+ return (
+
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ borderColor
+ />
+ )}
+ {showSuccessAlert && (
+ {
+ setShowSuccessAlert(false);
+ }}
+ borderColor
+ />
+ )}
+
+ {/* Header con foto de perfil */}
+
+ {isEditable ? (
+
+ Editar Perfil
+
+ ) : (
+
+ Mi Perfil
+
+ )}
+
+ {isEditable ? (
+
+ {
+ setImage(null);
+ }}
+ >
+
+
+
+
+ ) : (
+
+ )}
+ {isEditable ? (
+
+
+
+ ) : (
+ setIsEditable(true)}
+ >
+ EDITAR
+
+ )}
+
+
+ {/* Información del perfil */}
+
+ setProfile({ ...profile, firstName: value })}
+ errorText="El campo no puede estar vacío"
+ backgroundColor={isEditable ? Colors.textWhite : undefined}
+ />
+ setProfile({ ...profile, lastName: value })}
+ errorText="El campo no puede estar vacío"
+ backgroundColor={isEditable ? Colors.textWhite : undefined}
+ />
+
+
+ {isEditable ? (
+
+ setProfile({
+ ...profile,
+ profile: { ...profile.profile, birthDate: new Date(value) },
+ })
+ }
+ />
+ ) : (
+
+ )}
+ setProfile({ ...profile, phoneNumber: value })}
+ fieldType="number"
+ errorText="El campo no puede estar vacío"
+ backgroundColor={isEditable ? Colors.textWhite : undefined}
+ />
+
+
+ {/* Botón de editar al final */}
+ {!isEditable ? (
+ setIsEditable(true)} />
+ ) : (
+ submitProfile()}
+ loading={loading}
+ />
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexGrow: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 20,
+ paddingTop: -20,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '56%',
+ marginLeft: -162,
+ top: 40,
+ right: 0,
+ zIndex: 1000,
+ },
+ profileHeader: {
+ alignItems: 'center',
+ marginBottom: 15,
+ },
+ profileImage: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ marginBottom: 15,
+ borderWidth: 2,
+ borderColor: '#e0e0e0',
+ },
+ editButton: {
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ borderRadius: 20,
+ },
+ editButtonText: {
+ color: Colors.primary,
+ },
+ profileInfo: {
+ marginBottom: 20,
+ },
+ fieldContainer: {
+ marginBottom: 15,
+ },
+ fieldLabel: {
+ fontSize: 14,
+ color: '#666',
+ marginBottom: 5,
+ },
+ fieldValue: {
+ fontSize: 18,
+ color: '#333',
+ marginBottom: 10,
+ },
+ separator: {
+ height: 1,
+ backgroundColor: '#e0e0e0',
+ marginBottom: 15,
+ },
+ bottomEditButton: {
+ alignSelf: 'center',
+ marginTop: 20,
+ marginBottom: 40,
+ width: '50%',
+ alignItems: 'center',
+ },
+});
+
+export default ProfileScreen;
diff --git a/src/screens/RegisterScreen.tsx b/src/screens/RegisterScreen.tsx
index 9fd3397..d233bc0 100644
--- a/src/screens/RegisterScreen.tsx
+++ b/src/screens/RegisterScreen.tsx
@@ -19,6 +19,13 @@ import DatePickerInput from '../components/DatePickerInput';
import { ChevronLeftIcon } from 'react-native-heroicons/outline';
import { AuthService } from '../services/auth';
import Alert from '../components/Alerts';
+import {
+ validateEmail,
+ validatePassword,
+ validatePasswordMatch,
+ validateRequiredFields,
+ validateDateFormat,
+} from '../utils/validators';
export default function RegisterScreen() {
const router = useRouter();
@@ -52,16 +59,32 @@ export default function RegisterScreen() {
if (currentStep === 2) {
navigation.setOptions({
headerLeft: () => (
- handleStepChange(1)}>
-
+ handleStepChange(1)}
+ style={{
+ padding: 12, // Aumentado
+ marginLeft: -12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ }}
+ >
+
),
});
} else if (currentStep === 1) {
navigation.setOptions({
headerLeft: () => (
- router.replace('/login')}>
-
+ router.replace('/login')}
+ style={{
+ padding: 12, // Aumentado
+ marginLeft: -12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ }}
+ >
+
),
});
@@ -69,6 +92,10 @@ export default function RegisterScreen() {
}, [currentStep]);
const handleStepChange = (newStep: number) => {
+ // Clear alerts when changing steps
+ setShowErrorAlert(false);
+ setErrorMessage('');
+
Animated.timing(fadeAnim, {
toValue: 0,
duration: 200,
@@ -84,17 +111,22 @@ export default function RegisterScreen() {
};
const handleNext = () => {
- if (password.length < 8) {
+ if (!validateEmail(email)) {
+ setShowErrorAlert(true);
+ setErrorMessage('El correo ingresado no es válido');
+ return;
+ }
+ if (!validatePassword(password)) {
setShowErrorAlert(true);
setErrorMessage('La contraseña debe tener al menos 8 caracteres');
return;
}
- if (!email || !password || !confirmPassword) {
+ if (!validateRequiredFields([email, password, confirmPassword])) {
setShowErrorAlert(true);
setErrorMessage('Por favor completa todos los campos.');
return;
}
- if (password !== confirmPassword) {
+ if (!validatePasswordMatch(password, confirmPassword)) {
setShowErrorAlert(true);
setErrorMessage('Las contraseñas no coinciden.');
return;
@@ -103,20 +135,59 @@ export default function RegisterScreen() {
};
const handleRegister = async () => {
- if (!firstName || !lastName || !cedula || !phoneNumber || !dateOfBirth) {
+ if (
+ !validateRequiredFields([
+ firstName,
+ lastName,
+ cedula,
+ phoneNumber,
+ dateOfBirth,
+ ])
+ ) {
setShowErrorAlert(true);
setErrorMessage('Por favor completa todos los campos');
return;
}
- // Date validation
- const isValidDate = /^\d{4}-\d{2}-\d{2}$/.test(dateOfBirth);
- if (!isValidDate) {
+ if (!validateDateFormat(dateOfBirth)) {
setShowErrorAlert(true);
setErrorMessage('Formato de fecha inválido (Use YYYY-MM-DD)');
return;
}
+ // Validar que el usuario tenga al menos 14 años
+ const birthDate = new Date(dateOfBirth);
+ const today = new Date();
+ let age = today.getFullYear() - birthDate.getFullYear();
+ const monthDiff = today.getMonth() - birthDate.getMonth();
+ if (
+ monthDiff < 0 ||
+ (monthDiff === 0 && today.getDate() < birthDate.getDate())
+ ) {
+ age--;
+ }
+ if (age < 14) {
+ setShowErrorAlert(true);
+ setErrorMessage('Debes tener al menos 14 años para registrarte');
+ return;
+ }
+
+ // Validar que la cédula no exceda los 10 caracteres
+ if (cedula.length > 10) {
+ setShowErrorAlert(true);
+ setErrorMessage('La cédula no puede exceder los 10 caracteres');
+ return;
+ }
+
+ // Validar que el teléfono tenga entre 8 y 15 caracteres
+ if (phoneNumber.length < 8 || phoneNumber.length > 15) {
+ setShowErrorAlert(true);
+ setErrorMessage(
+ 'El número de teléfono debe tener entre 8 y 15 caracteres',
+ );
+ return;
+ }
+
setLoading(true);
try {
const result = await AuthService.register(
@@ -133,16 +204,19 @@ export default function RegisterScreen() {
if (result.success) {
setShowSuccessAlert(true);
setTimeout(() => {
- router.replace('/success');
+ router.replace({
+ pathname: '/success',
+ params: { email, password },
+ });
}, 2000);
} else {
setShowErrorAlert(true);
- setErrorMessage(result.error);
+ setErrorMessage(result.error); // Usamos el mensaje de error proporcionado por AuthService
}
} catch (error) {
console.error(error);
setShowErrorAlert(true);
- setErrorMessage('Error al crear la cuenta');
+ setErrorMessage('Ocurrió un error inesperado. Inténtalo nuevamente.'); // Mensaje genérico para errores no esperados
} finally {
setLoading(false);
}
diff --git a/src/screens/RegistrationSuccessScreen.tsx b/src/screens/RegistrationSuccessScreen.tsx
index d60d67d..68d2594 100644
--- a/src/screens/RegistrationSuccessScreen.tsx
+++ b/src/screens/RegistrationSuccessScreen.tsx
@@ -1,31 +1,72 @@
-import React from 'react';
-import { View, StyleSheet } from 'react-native';
-import { useRouter } from 'expo-router';
-import Button from '../components/Button';
+import React, { useEffect, useState } from 'react';
+import { View, StyleSheet, ActivityIndicator } from 'react-native';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import { AuthService } from '../services/auth';
import PoppinsText from '../components/PoppinsText';
+import Button from '../components/Button';
import { Colors, FontSizes } from '../styles/theme';
export default function RegistrationSuccessScreen() {
const router = useRouter();
+ const { email, password } = useLocalSearchParams(); // Recibe las credenciales del registro
+ const [loading, setLoading] = useState(true);
+ const [loginSuccess, setLoginSuccess] = useState(false);
- const handleContinue = () => {
- router.replace('/(tabs)');
+ const handleRedirectToHome = () => {
+ router.replace({
+ pathname: '/(tabs)',
+ params: { showEmailVerification: 'true' },
+ });
};
+ useEffect(() => {
+ const autoLogin = async () => {
+ if (email && password) {
+ const result = await AuthService.login(
+ email as string,
+ password as string,
+ );
+ if (result.success) {
+ setLoginSuccess(true); // Login exitoso
+ } else {
+ console.error(
+ 'Error en el inicio de sesión automático:',
+ result.error,
+ );
+ router.replace('/login'); // Redirige al login manualmente
+ }
+ }
+ setLoading(false);
+ };
+
+ autoLogin();
+ }, [email, password, router]);
+
return (
Registro Exitoso!
-
- Tu cuenta ha sido creada con éxito. ¡Bienvenido a PharmaTech!
-
-
+ {loading ? (
+ <>
+
+ Estamos configurando tu cuenta. Por favor, espera un momento...
+
+
+ >
+ ) : loginSuccess ? (
+ <>
+
+ Tu cuenta está lista. Presiona el botón para continuar.
+
+
+ >
+ ) : null}
);
}
diff --git a/src/screens/SearchProductScreen.tsx b/src/screens/SearchProductScreen.tsx
new file mode 100644
index 0000000..5fc354b
--- /dev/null
+++ b/src/screens/SearchProductScreen.tsx
@@ -0,0 +1,401 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ SafeAreaView,
+ Dimensions,
+ FlatList,
+ ListRenderItem,
+} from 'react-native';
+import { useLocalSearchParams } from 'expo-router';
+import TopBar from '../components/TopBar';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import { ProductService } from '../services/products';
+import { AdjustmentsHorizontalIcon } from 'react-native-heroicons/solid';
+import FilterOptions from '../components/FilterOptions';
+import {
+ CategoryResponse,
+ ManufacturerResponse,
+ PresentationResponse,
+ ProductPresentation,
+} from '@pharmatech/sdk';
+import Checkbox from '../components/Checkbox';
+import { ChevronLeftIcon } from 'react-native-heroicons/solid';
+import ProductCard from '../components/Card';
+import { CategoryService } from '../services/category';
+import { useNavigation } from '@react-navigation/native';
+
+export default function SearchProductScreen() {
+ const { query } = useLocalSearchParams<{ query: string }>();
+
+ const [initialSearchResults, setInitialSearchResults] = useState<
+ ProductPresentation[]
+ >([]); // Almacena los resultados iniciales
+ const [search, setSearch] = useState([]); // Almacena los resultados actuales (filtrados)
+ const [isVisible, setIsVisible] = useState(false);
+ const [brands, setBrands] = useState([]); // Cambiado a ManufacturerResponse
+ const [categories, setCategories] = useState([]); // Cambiado a CategoryResponse
+ const [presentations, setPresentations] = useState(
+ [],
+ );
+ const navigation = useNavigation();
+
+ const [selectedPresentations, setSelectedPresentations] = useState(
+ [],
+ );
+ const [selectedCategories, setSelectedCategories] = useState([]);
+ const [selectedBrands, setSelectedBrands] = useState([]);
+ const priceRange = useState({ min: 0, max: 1000 });
+
+ const handleBrandToggle = (brand: boolean, value: string) => {
+ if (brand) {
+ setSelectedBrands([...selectedBrands, value]);
+ } else {
+ setSelectedBrands((prev) => prev.filter((item) => item !== value));
+ }
+ };
+
+ const handleCategoryToggle = (category: boolean, value: string) => {
+ if (category) {
+ setSelectedCategories([...selectedCategories, value]);
+ } else {
+ setSelectedCategories((prev) => prev.filter((item) => item !== value));
+ }
+ };
+
+ const handlePresentationToggle = (presentation: boolean, value: string) => {
+ if (presentation) {
+ setSelectedPresentations([...selectedPresentations, value]);
+ } else {
+ setSelectedPresentations((prev) => prev.filter((item) => item !== value));
+ }
+ };
+
+ const clearFilters = () => {
+ setSelectedBrands([]);
+ setSelectedCategories([]);
+ setSelectedPresentations([]);
+ setSearch(initialSearchResults); // Restablecer los resultados a los iniciales
+ };
+
+ const submitFilters = (range: { min: number; max: number }) => {
+ const filteredResults = initialSearchResults.filter((item) => {
+ const matchesBrand =
+ selectedBrands.length === 0 ||
+ selectedBrands.includes(
+ (item.product.manufacturer as ManufacturerResponse).id,
+ );
+
+ const matchesCategory =
+ selectedCategories.length === 0 ||
+ (item.product.categories as CategoryResponse[]).some((category) =>
+ selectedCategories.includes(category.id),
+ );
+
+ const matchesPresentation =
+ selectedPresentations.length === 0 ||
+ selectedPresentations.includes(item.presentation.id);
+
+ const matchesPrice = item.price >= range.min && item.price <= range.max; // Usar el rango pasado como argumento
+
+ return (
+ matchesBrand && matchesCategory && matchesPresentation && matchesPrice
+ );
+ });
+
+ setSearch(filteredResults);
+ };
+
+ useEffect(() => {
+ const fetchSearchData = async () => {
+ const searchData = await ProductService.getProducts(1, 20, { q: query });
+ const brandsData = await ProductService.getBrands(1, 100);
+ const presentationsData = await ProductService.getPresentations(1, 100);
+ const categoriesData = await CategoryService.getCategories(1, 100);
+
+ if (searchData.success) {
+ setInitialSearchResults(searchData.data.results); // Guardar los resultados iniciales
+ setSearch(searchData.data.results); // Mostrar los resultados iniciales
+ }
+ if (brandsData.success) setBrands(brandsData.data.results);
+ if (presentationsData.success)
+ setPresentations(presentationsData.data.results);
+ if (categoriesData.success) setCategories(categoriesData.data.results);
+ };
+
+ fetchSearchData();
+ }, []);
+
+ const renderItem: ListRenderItem = ({ item }) => (
+ console.log}
+ />
+ );
+
+ return (
+
+
+ navigation.goBack()}
+ style={{
+ paddingHorizontal: 10,
+ marginBottom: -4,
+ flexDirection: 'row',
+ alignSelf: 'flex-start',
+ }}
+ >
+
+
+ Volver
+
+
+
+ setIsVisible(true)}
+ >
+
+ Filtrar
+
+
+
+
+
+ Resultados de la búsqueda: {query}
+
+ {search?.length} resultados
+
+
+ item.id.toString()}
+ numColumns={2}
+ contentContainerStyle={{ paddingHorizontal: 20, paddingBottom: 20 }}
+ showsVerticalScrollIndicator={true}
+ alwaysBounceVertical={true}
+ />
+ setIsVisible(false)}
+ onClearFilters={clearFilters}
+ onApplyFilters={(range) => {
+ priceRange[1](range); // Actualizar el rango de precios
+ submitFilters(range); // Aplicar los filtros
+ }}
+ >
+ {/* Filtros */}
+
+
+ Categoría
+
+ {categories?.map((b) => (
+ handleCategoryToggle(e, val)}
+ style={{ margin: 3 }}
+ size={20}
+ />
+ ))}
+
+
+
+ Marca o Laboratorio
+
+ {brands?.map((b) => (
+ handleBrandToggle(e, val)}
+ style={{ margin: 3 }}
+ size={20}
+ />
+ ))}
+
+
+
+ Presentación
+
+ {presentations?.map((b) => (
+ handlePresentationToggle(e, val)}
+ style={{ margin: 3 }}
+ size={20}
+ />
+ ))}
+
+
+
+
+
+ );
+}
+
+const { width } = Dimensions.get('window');
+
+const styles = StyleSheet.create({
+ container: {
+ paddingTop: 15,
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ listContainer: {
+ flex: 1,
+ marginHorizontal: 20,
+ },
+ productImage: {
+ width: width,
+ height: 200,
+ },
+ imageIndicators: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginVertical: 8,
+ },
+ text: {
+ fontSize: FontSizes.s1.size,
+ lineHeight: FontSizes.s1.lineHeight,
+ marginBottom: 8,
+ },
+ imageIndicator: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: '#CCCCCC',
+ marginHorizontal: 4,
+ },
+ activeImageIndicator: {
+ backgroundColor: Colors.gray_500,
+ width: 17,
+ },
+ productInfo: {
+ padding: 16,
+ paddingVertical: 0,
+ },
+ priceRatingContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ price: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: Colors.primary,
+ marginRight: 10,
+ },
+ productName: {
+ textAlign: 'center',
+ fontSize: FontSizes.h5.size,
+ color: Colors.textMain,
+ marginVertical: 15,
+ },
+ description: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.textLowContrast,
+ marginHorizontal: 20,
+ marginBottom: 20,
+ lineHeight: 20,
+ },
+ sectionTitle: {
+ fontSize: FontSizes.s2.size,
+ color: Colors.textLowContrast,
+ marginBottom: 3,
+ },
+ quantitySelector: {
+ flexDirection: 'row',
+ marginBottom: 15,
+ },
+ ratingStarsContainer: {
+ flexDirection: 'row',
+ marginBottom: 20,
+ justifyContent: 'center',
+ },
+ starIcon: {
+ marginHorizontal: 4,
+ },
+ ratingFeedback: {
+ textAlign: 'center',
+ color: '#4CAF50',
+ marginBottom: 20,
+ fontStyle: 'italic',
+ },
+ mapContainer: {
+ marginVertical: 8,
+ flex: 1,
+ alignItems: 'center',
+ },
+ availableContainer: {
+ marginBottom: 8,
+ backgroundColor: Colors.textWhite,
+ borderRadius: 15,
+ borderWidth: 1,
+ borderColor: Colors.gray_100,
+ },
+ availableCard: {
+ marginBottom: 10,
+ justifyContent: 'space-between',
+ borderBottomWidth: 1,
+ borderColor: Colors.gray_100,
+ },
+ cardButtonContainer: {
+ position: 'relative',
+ top: -40,
+ left: 120,
+ maxWidth: '65%',
+ alignItems: 'flex-end',
+ zIndex: 999,
+ },
+ discount: {
+ fontSize: FontSizes.b4.size,
+ backgroundColor: Colors.semanticInfo,
+ borderRadius: 5,
+ padding: 4,
+ },
+});
diff --git a/src/screens/SelectLocationScreen.tsx b/src/screens/SelectLocationScreen.tsx
new file mode 100644
index 0000000..637f2ac
--- /dev/null
+++ b/src/screens/SelectLocationScreen.tsx
@@ -0,0 +1,256 @@
+import React, { useState, useRef, useEffect } from 'react';
+import {
+ StyleSheet,
+ View,
+ TextInput,
+ Alert,
+ TouchableOpacity,
+} from 'react-native';
+import MapView, { PROVIDER_GOOGLE, Region } from 'react-native-maps'; // Importa el tipo Region
+import * as Location from 'expo-location';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import Button from '../components/Button';
+import { MapPinIcon } from 'react-native-heroicons/solid';
+import { Colors, FontSizes } from '../styles/theme';
+import PoppinsText from '../components/PoppinsText';
+import { Config } from '../config';
+import { ChevronLeftIcon } from 'react-native-heroicons/outline';
+import { useNavigation } from '@react-navigation/native';
+
+const SelectLocationScreen = () => {
+ console.log('Clave de API utilizada:', Config.googleMapsApiKey);
+
+ const { fromCheckout } = useLocalSearchParams();
+ const navigation = useNavigation();
+
+ const [selectedLocation, setSelectedLocation] = useState<{
+ latitude: number;
+ longitude: number;
+ } | null>(null);
+ const [address, setAddress] = useState('Cargando dirección...');
+ const [initialRegion, setInitialRegion] = useState<{
+ latitude: number;
+ longitude: number;
+ latitudeDelta: number;
+ longitudeDelta: number;
+ }>({
+ latitude: 10.0678, // Coordenadas iniciales (Barquisimeto)
+ longitude: -69.3467,
+ latitudeDelta: 0.1,
+ longitudeDelta: 0.1,
+ });
+ const mapRef = useRef(null);
+ const router = useRouter();
+
+ // Solicitar permisos y obtener la ubicación del usuario
+ useEffect(() => {
+ const getUserLocation = async () => {
+ try {
+ const { status } = await Location.getForegroundPermissionsAsync();
+ if (status !== 'granted') {
+ const { status: newStatus } =
+ await Location.requestForegroundPermissionsAsync();
+ if (newStatus !== 'granted') {
+ Alert.alert(
+ 'Permiso denegado',
+ 'No se pudo obtener la ubicación. El mapa cargará en la región predeterminada.',
+ );
+ return;
+ }
+ }
+
+ const location = await Location.getCurrentPositionAsync({});
+ const userRegion = {
+ latitude: location.coords.latitude,
+ longitude: location.coords.longitude,
+ latitudeDelta: 0.01,
+ longitudeDelta: 0.01,
+ };
+
+ setInitialRegion(userRegion);
+ setSelectedLocation({
+ latitude: location.coords.latitude,
+ longitude: location.coords.longitude,
+ });
+ mapRef.current?.animateToRegion(userRegion, 1000);
+ fetchAddress(location.coords.latitude, location.coords.longitude);
+ } catch (error) {
+ console.error('Error al obtener la ubicación:', error);
+ Alert.alert(
+ 'Error',
+ 'Hubo un problema al obtener la ubicación. El mapa cargará en la región predeterminada.',
+ );
+ }
+ };
+
+ getUserLocation();
+ }, []);
+
+ // Obtener la dirección desde la API de Google Maps
+ const fetchAddress = async (latitude: number, longitude: number) => {
+ try {
+ const response = await fetch(
+ `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${Config.googleMapsApiKey}`,
+ );
+ const data = await response.json();
+
+ if (data.plus_code) {
+ const plusCode = data.plus_code.compound_code || 'Código no disponible';
+ setAddress(plusCode);
+ } else if (data.results && data.results.length > 0) {
+ setAddress(data.results[0].formatted_address);
+ } else {
+ setAddress('No se pudo obtener la dirección.');
+ }
+ } catch (error) {
+ console.error('Error al obtener la dirección:', error);
+ setAddress('Error al obtener la dirección.');
+ }
+ };
+
+ const handleRegionChange = (region: Region) => {
+ setSelectedLocation({
+ latitude: region.latitude,
+ longitude: region.longitude,
+ });
+ fetchAddress(region.latitude, region.longitude);
+ };
+
+ const handleConfirmLocation = () => {
+ if (selectedLocation) {
+ console.log('Coordenadas seleccionadas:', selectedLocation); // Log para verificar las coordenadas
+ console.log('Navegando a createDirection con:', {
+ latitude: selectedLocation?.latitude,
+ longitude: selectedLocation?.longitude,
+ });
+ router.push({
+ pathname: '/createDirection',
+ params: {
+ latitude: selectedLocation.latitude.toString(),
+ longitude: selectedLocation.longitude.toString(),
+ fromCheckout: fromCheckout === 'true' ? 'true' : 'false', // Convertimos a cadena
+ },
+ });
+ } else {
+ console.log('No se seleccionó ninguna ubicación'); // Log para verificar si no hay ubicación seleccionada
+ }
+ };
+
+ return (
+
+ {/* Header con input para mostrar la dirección */}
+
+
+
+
+
+
+
+
+ {/* Marker siempre centrado */}
+
+
+
+ {/* Footer con mensajes y botón */}
+
+ Fija el marcador
+
+ Arrastra el mapa e indica la dirección exacta con el marcador
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ backgroundColor: Colors.textWhite,
+ padding: 10,
+ zIndex: 1000,
+ flexDirection: 'row',
+ },
+ addressInput: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 8,
+ padding: 10,
+ borderWidth: 1,
+ borderColor: Colors.stroke,
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ fontFamily: 'Poppins_400Regular',
+ color: Colors.textMain,
+ marginLeft: 14,
+ flex: 1,
+ },
+ map: {
+ ...StyleSheet.absoluteFillObject,
+ },
+ markerFixed: {
+ position: 'absolute',
+ top: '50%',
+ left: '50%',
+ marginLeft: -20,
+ marginTop: -40,
+ },
+ footer: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ backgroundColor: Colors.textWhite,
+ padding: 20,
+ borderTopLeftRadius: 20,
+ borderTopRightRadius: 20,
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: -2 },
+ shadowOpacity: 0.1,
+ elevation: 5,
+ },
+ footerTitle: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ fontWeight: 'bold',
+ color: Colors.textMain,
+ marginBottom: 12,
+ },
+ footerSubtitle: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.textLowContrast,
+ marginBottom: 12,
+ textAlign: 'center',
+ },
+});
+
+export default SelectLocationScreen;
diff --git a/src/screens/SplashScreen.tsx b/src/screens/SplashScreen.tsx
new file mode 100644
index 0000000..2257c3f
--- /dev/null
+++ b/src/screens/SplashScreen.tsx
@@ -0,0 +1,57 @@
+import React, { useEffect } from 'react';
+import { View, StyleSheet } from 'react-native';
+import { useRouter } from 'expo-router';
+import { AuthService } from '../services/auth';
+import { UserService } from '../services/user';
+import { Colors } from '../styles/theme';
+
+export default function SplashScreen() {
+ const router = useRouter();
+
+ useEffect(() => {
+ const checkSession = async () => {
+ try {
+ const isValidSession = await AuthService.validateSession();
+ if (isValidSession) {
+ // Obtener el perfil del usuario para determinar el rol
+ const profileResponse = await UserService.getProfile();
+ if (profileResponse.success) {
+ const userRole = profileResponse.data.role;
+
+ if (userRole === 'delivery') {
+ // Redirigir a (delivery-tabs) si es un usuario de tipo delivery
+ router.replace('/(delivery-tabs)');
+ } else {
+ // Redirigir a (tabs) si es un usuario regular
+ router.replace('/(tabs)');
+ }
+ } else {
+ console.error(
+ 'Error al obtener el perfil del usuario (se lleva al login):',
+ profileResponse.error,
+ );
+ router.replace('/login'); // Redirigir al login en caso de error
+ }
+ } else {
+ router.replace('/login'); // Redirigir al login si la sesión no es válida
+ }
+ } catch (error) {
+ console.error('Error al validar la sesión:', error);
+ router.replace('/login'); // Redirigir al login en caso de error
+ }
+ };
+
+ checkSession();
+ }, []);
+
+ return ;
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.bgColor,
+ },
+});
diff --git a/src/screens/delivery/DeliveryDetailScreen.tsx b/src/screens/delivery/DeliveryDetailScreen.tsx
new file mode 100644
index 0000000..48c84c0
--- /dev/null
+++ b/src/screens/delivery/DeliveryDetailScreen.tsx
@@ -0,0 +1,889 @@
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ ActivityIndicator,
+ Image,
+ Modal,
+ TouchableOpacity,
+} from 'react-native';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import {
+ InformationCircleIcon,
+ BuildingStorefrontIcon,
+ MapPinIcon,
+ PhoneIcon,
+ XMarkIcon,
+} from 'react-native-heroicons/solid';
+import Badge from '../../components/Badge';
+import PoppinsText from '../../components/PoppinsText';
+import Button from '../../components/Button';
+import DeliveryMap from '../../components/DeliveryMap';
+import CustomerAvatar from '../../components/CustomerAvatar';
+import Alert from '../../components/Alerts';
+import { Colors, FontSizes } from '../../styles/theme';
+import { DeliveryService } from '../../services/delivery';
+import { BranchService } from '../../services/branches';
+import { OrderDeliveryStatus, OrderDetailedResponse } from '@pharmatech/sdk';
+import { UserService } from '../../services/user';
+import { useDispatch, useSelector } from 'react-redux';
+import { RootState } from '../../redux/store';
+import {
+ setOrderDetails,
+ setDeliveryState,
+ updateDeliveryStatus,
+} from '../../redux/slices/deliverySlice';
+import Popup from '../../components/Popup';
+import {
+ initializeSocket,
+ disconnectSocket,
+} from '../../lib/deliverySocket/deliverySocket';
+import { Socket } from 'socket.io-client';
+import * as Location from 'expo-location';
+
+const DeliveryDetailScreen: React.FC = () => {
+ const { id } = useLocalSearchParams();
+ const dispatch = useDispatch();
+ const orderDetails = useSelector(
+ (state: RootState) => state.delivery.orders[id as string],
+ );
+ const [order, setOrder] = useState(
+ undefined,
+ );
+ const [isOrderDetailsLoading, setIsOrderDetailsLoading] = useState(true);
+ const [isFetchingOrder, setIsFetchingOrder] = useState(false); // Nuevo estado de carga para fetchOrder
+ const [branchNames, setBranchNames] = useState<
+ Record
+ >({});
+ const [deliveryStateBadge, setDeliveryStateBadge] = useState(0);
+ const router = useRouter();
+ const [isMapModalVisible, setIsMapModalVisible] = useState(false);
+
+ const branchLocation = {
+ latitude: branchNames[orderDetails?.branchId ?? '']?.latitude || 0,
+ longitude: branchNames[orderDetails?.branchId ?? '']?.longitude || 0,
+ };
+
+ const customerLocation = {
+ latitude: orderDetails?.address?.latitude || 0,
+ longitude: orderDetails?.address?.longitude || 0,
+ };
+
+ const deliveryState = useSelector(
+ (state: RootState) => state.delivery.deliveryState[id as string] || 0,
+ );
+
+ // Estados para las alertas
+ const [showAlert, setShowAlert] = useState(false);
+ const [alertType, setAlertType] = useState<'success' | 'error' | 'info'>(
+ 'info',
+ );
+ const [alertMessage, setAlertMessage] = useState('');
+
+ const deliveryStates = [
+ 'Buscando pedido en sucursal de origen',
+ 'Haciendo entrega del pedido',
+ ];
+
+ const buttonStates = [
+ 'Comenzar entrega',
+ 'Llegué a la sucursal',
+ 'Ya tengo los productos del pedido',
+ 'Ir a destino de entrega',
+ 'Ya hice la entrega',
+ ];
+
+ const [showConfirmationPopup, setShowConfirmationPopup] = useState(false);
+ const [deliveryLocation, setDeliveryLocation] = useState<{
+ latitude: number;
+ longitude: number;
+ } | null>(null);
+
+ useEffect(() => {
+ const fetchOrderDetails = async () => {
+ try {
+ if (!orderDetails) {
+ const details = await DeliveryService.getOrderDetails(id as string);
+ dispatch(setOrderDetails({ id: id as string, details }));
+ }
+ } catch (error) {
+ console.error('Error al obtener los detalles del pedido:', error);
+ } finally {
+ setIsOrderDetailsLoading(false);
+ }
+ };
+
+ fetchOrderDetails();
+ }, [id, orderDetails, dispatch]);
+
+ useEffect(() => {
+ const fetchBranches = async () => {
+ try {
+ const branches = await BranchService.findAll({ page: 1, limit: 100 });
+ const branchMap = branches.results.reduce(
+ (
+ acc: Record<
+ string,
+ { name: string; latitude: number; longitude: number }
+ >,
+ branch,
+ ) => {
+ acc[branch.id] = {
+ name: branch.name,
+ latitude: branch.latitude,
+ longitude: branch.longitude,
+ };
+ return acc;
+ },
+ {},
+ );
+
+ setBranchNames(branchMap);
+ } catch (error) {
+ console.error('Error al obtener las sucursales:', error);
+ } finally {
+ setIsFetchingOrder(false);
+ }
+ };
+
+ if (orderDetails) {
+ fetchBranches();
+ }
+ }, [orderDetails]);
+
+ useEffect(() => {
+ const fetchOrder = async () => {
+ setIsFetchingOrder(true); // Iniciar el indicador de carga
+ let timerId: NodeJS.Timeout | null = null; // Identificador del temporizador
+
+ try {
+ if (!orderDetails) {
+ // Agregar un temporizador de 2 segundos antes de mostrar el error
+ timerId = setTimeout(() => {
+ if (!orderDetails) {
+ console.error(
+ 'No se encontraron detalles de la orden de tipo delivery.',
+ );
+ }
+ }, 2000);
+ return;
+ }
+
+ const orderId = orderDetails.orderId;
+
+ const order = await UserService.getOrder(orderId);
+
+ if (order.success) {
+ setOrder(order.data);
+ } else {
+ console.error('Error al obtener el pedido:', order.error);
+ }
+ } catch (error) {
+ console.error('Error en fetchOrder:', error);
+ } finally {
+ setIsFetchingOrder(false); // Finalizar el indicador de carga
+ if (timerId) {
+ clearTimeout(timerId); // Cancelar el temporizador si los datos están disponibles
+ }
+ }
+ };
+
+ fetchOrder();
+ }, [orderDetails]);
+
+ // Configuración del WebSocket para recibir actualizaciones en tiempo real
+ useEffect(() => {
+ let socket: Socket;
+
+ const setupSocket = async () => {
+ try {
+ socket = await initializeSocket();
+ console.log('Conectando al WebSocket...');
+ socket.connect();
+
+ socket.on('connect', () => {
+ console.log('WebSocket conectado en DeliveryDetailScreen');
+ });
+
+ socket.on('connect_error', (error) => {
+ console.error(
+ 'Error de conexión al WebSocket en DeliveryDetailScreen:',
+ error,
+ );
+ });
+
+ socket.on('deliveryUpdated', (data: { id: string; status: string }) => {
+ console.log('Evento recibido: deliveryUpdated', data);
+ if (data.id === id) {
+ dispatch(
+ updateDeliveryStatus({
+ id: data.id,
+ status: data.status as OrderDeliveryStatus,
+ }),
+ );
+ }
+ });
+
+ socket.on('disconnect', (reason) => {
+ console.warn(
+ 'WebSocket desconectado en DeliveryDetailScreen. Razón:',
+ reason,
+ );
+ });
+ } catch (error) {
+ console.error(
+ 'Error configurando el WebSocket en DeliveryDetailScreen:',
+ error,
+ );
+ }
+ };
+
+ setupSocket();
+
+ return () => {
+ if (socket) {
+ console.log(
+ 'Limpiando listeners y desconectando WebSocket en DeliveryDetailScreen',
+ );
+ socket.off('deliveryUpdated');
+ disconnectSocket();
+ }
+ };
+ }, [id, dispatch]);
+
+ // ...existing code...
+
+ useEffect(() => {
+ let socket: Socket;
+ let locationSubscription: Location.LocationSubscription | null = null;
+
+ const startTracking = async () => {
+ try {
+ socket = await initializeSocket();
+ socket.connect();
+
+ // Permisos de ubicación
+ const { status } = await Location.requestForegroundPermissionsAsync();
+ if (status !== 'granted') {
+ console.warn('Permiso de ubicación denegado');
+ return;
+ }
+
+ // Suscribirse a cambios de ubicación
+ locationSubscription = await Location.watchPositionAsync(
+ {
+ accuracy: Location.Accuracy.High,
+ timeInterval: 5000, // cada 5 segundos
+ distanceInterval: 10, // o cada 10 metros
+ },
+ (location) => {
+ const { latitude, longitude } = location.coords;
+ setDeliveryLocation({ latitude, longitude });
+
+ // Emitir coordenadas por WebSocket
+ if (orderDetails?.id) {
+ socket.emit('updateCoordinates', {
+ orderId: orderDetails.id,
+ latitude,
+ longitude,
+ });
+ }
+ },
+ );
+ } catch (error) {
+ console.error('Error en el tracking de ubicación:', error);
+ }
+ };
+
+ startTracking();
+
+ return () => {
+ if (locationSubscription) {
+ locationSubscription.remove();
+ }
+ if (socket) {
+ disconnectSocket();
+ }
+ };
+ }, [orderDetails?.id]);
+
+ // Mostrar un indicador de carga mientras se obtienen los detalles del pedido o la orden
+ if (isOrderDetailsLoading || isFetchingOrder) {
+ return ;
+ }
+
+ if (!orderDetails) {
+ // Mostrar un mensaje de error si no se encuentran los detalles del pedido
+ return (
+
+
+ No se encontraron datos del pedido.
+
+
+ );
+ }
+
+ const handleNextState = async () => {
+ try {
+ if (deliveryState === buttonStates.length - 1) {
+ // Mostrar el popup de confirmación
+ setShowConfirmationPopup(true);
+ return;
+ }
+
+ let nextStatus: OrderDeliveryStatus | null = null;
+
+ // Lógica para otros estados
+ switch (buttonStates[deliveryState]) {
+ case 'Comenzar entrega':
+ nextStatus = OrderDeliveryStatus.WAITING_CONFIRMATION;
+ await DeliveryService.updateOrderStatus(orderDetails!.id, nextStatus);
+ break;
+ case 'Ya tengo los productos del pedido':
+ nextStatus = OrderDeliveryStatus.PICKED_UP;
+ await DeliveryService.updateOrderStatus(orderDetails!.id, nextStatus);
+ break;
+ case 'Ir a destino de entrega':
+ nextStatus = OrderDeliveryStatus.IN_ROUTE;
+ await DeliveryService.updateOrderStatus(orderDetails!.id, nextStatus);
+ setDeliveryStateBadge(1); // Cambiar a "Haciendo entrega del pedido"
+ break;
+ }
+
+ if (nextStatus) {
+ // Emitir el evento al WebSocket
+ const socket = await initializeSocket();
+ socket.emit('deliveryUpdated', {
+ id: orderDetails!.id,
+ status: nextStatus,
+ });
+ }
+
+ // Mostrar alerta de éxito
+ setAlertType('info');
+ setAlertMessage('Se actualizó el estado del pedido.');
+ setShowAlert(true);
+
+ setTimeout(() => {
+ setShowAlert(false);
+ }, 2500);
+
+ // Actualizar el estado local
+ dispatch(
+ setDeliveryState({ id: id as string, state: deliveryState + 1 }),
+ );
+ } catch (error) {
+ console.error('Error al actualizar el estado del delivery:', error);
+
+ // Mostrar alerta de error
+ setAlertType('error');
+ setAlertMessage('Hubo un problema al actualizar el estado del pedido.');
+ setShowAlert(true);
+
+ setTimeout(() => {
+ setShowAlert(false);
+ }, 2000);
+ }
+ };
+
+ const handleConfirmDelivery = async () => {
+ try {
+ // Actualizar el estado de la orden a DELIVERED
+ await DeliveryService.updateOrderStatus(
+ orderDetails!.id,
+ OrderDeliveryStatus.DELIVERED,
+ );
+
+ setAlertType('success');
+ setAlertMessage('La entrega se ha finalizado correctamente.');
+ setShowAlert(true);
+
+ setTimeout(() => {
+ setShowAlert(false);
+ }, 2500);
+
+ // Redirigir al usuario a (delivery-tabs)
+ router.replace('/(delivery-tabs)');
+ } catch (error) {
+ console.error('Error al finalizar la entrega:', error);
+
+ setAlertType('error');
+ setAlertMessage('Hubo un problema al finalizar la entrega.');
+ setShowAlert(true);
+
+ setTimeout(() => {
+ setShowAlert(false);
+ }, 2000);
+ } finally {
+ setShowConfirmationPopup(false); // Cerrar el popup
+ }
+ };
+
+ return (
+
+ {/* Mostrar alertas */}
+ {showAlert && (
+
+ setShowAlert(false)}
+ />
+
+ )}
+
+
+ {/* Título y botón de información */}
+
+ Detalle del pedido
+
+
+
+ {/* Estado del pedido */}
+
+ Estado:
+
+ {deliveryStates[deliveryStateBadge]}
+
+
+
+ {/* Ubicaciones del pedido */}
+
+ Ubicaciones del pedido
+
+
+ {/* Círculo superior */}
+
+
+ {/* Línea vertical punteada */}
+
+
+ {/* Círculo inferior */}
+
+
+ {/* Primera carta */}
+
+
+
+
+ Sucursal de origen
+
+
+
+ {branchNames[orderDetails.branchId]?.name ||
+ 'Sucursal no disponible'}
+
+
+
+ {/* Segunda carta */}
+
+
+
+ Entrega
+
+
+ {orderDetails.address.adress}
+
+
+ {orderDetails.address.referencePoint || 'Sin referencia'}
+
+
+
+
+ {/* Información de contacto */}
+
+ Información de contacto
+
+
+ {/* Avatar del cliente */}
+
+
+
+
+
+ {orderDetails.user.firstName} {orderDetails.user.lastName}
+
+
+
+ {/* Teléfono */}
+
+
+
+ {orderDetails.user.phoneNumber}
+
+
+
+
+ {/* Recorrido de entrega */}
+
+ Recorrido de entrega
+
+
+
+ setIsMapModalVisible(true)}
+ style={styles.expandButton}
+ />
+
+ {/* Pedido (contenido comentado) */}
+
+
+ Pedido
+
+
+ Total:{' '}
+ {order?.details?.reduce(
+ (total, detail) => total + detail.quantity,
+ 0,
+ ) || 0}{' '}
+ productos
+
+
+ {order?.details?.map((detail) => {
+ return (
+
+ {/* Imagen del producto */}
+
+
+ {/* Nombre de la presentación del producto */}
+
+ {detail.productPresentation.product.name}{' '}
+ {detail.productPresentation.presentation.name}{' '}
+ {detail.productPresentation.presentation.quantity}{' '}
+ {detail.productPresentation.presentation.measurementUnit}
+
+ {/* Cantidad del producto */}
+
+ Cantidad: {detail.quantity}
+
+
+
+ );
+ })}
+
+ {/* Espaciado adicional al final */}
+
+
+
+ {/* Botón flotante */}
+
+ setShowConfirmationPopup(false),
+ }}
+ onClose={() => setShowConfirmationPopup(false)}
+ />
+
+ {/* Modal para el mapa ampliado */}
+ setIsMapModalVisible(false)}
+ >
+
+ {/* Botón de cierre */}
+ setIsMapModalVisible(false)}
+ >
+
+
+
+ {/* Mapa ampliado */}
+
+
+ {/* Botón flotante sobre el modal */}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ title: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.textMain,
+ },
+ container: {
+ marginTop: 32,
+ padding: 16,
+ },
+ statusContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ statusLabel: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.primary,
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ sectionTitle: {
+ fontSize: FontSizes.s1.size,
+ lineHeight: FontSizes.s1.lineHeight,
+ color: Colors.primary,
+ },
+ locationsContainer: {
+ position: 'relative',
+ marginBottom: 16,
+ paddingLeft: 0,
+ },
+ verticalLine: {
+ position: 'absolute',
+ left: 4,
+ top: '25%',
+ height: '50%',
+ borderStyle: 'dashed',
+ borderWidth: 2,
+ borderColor: Colors.primary_300,
+ },
+ circleTop: {
+ position: 'absolute',
+ left: 0,
+ top: '25%',
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ backgroundColor: Colors.primary_300,
+ transform: [{ translateY: -4 }],
+ },
+ circleBottom: {
+ position: 'absolute',
+ left: 0,
+ top: '75%',
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ backgroundColor: Colors.primary_300,
+ transform: [{ translateY: -4 }],
+ },
+ card: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 16,
+ marginBottom: 16,
+ marginLeft: 28,
+ flex: 1,
+ },
+ card2: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 16,
+ marginLeft: 28,
+ flex: 1,
+ },
+ card3: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 16,
+ paddingBottom: 8,
+ marginBottom: 16,
+ flex: 1,
+ },
+ cardRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ icon: {
+ marginRight: 6,
+ },
+ cardTitle: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ },
+ cardSubtitle: {
+ fontSize: FontSizes.label.size,
+ color: Colors.textMain,
+ },
+ cardSubtitle1: {
+ fontSize: FontSizes.b3.size,
+ lineHeight: FontSizes.b3.lineHeight,
+ color: Colors.textMain,
+ },
+ mapPlaceholder: {
+ height: 300,
+ backgroundColor: Colors.gray_100,
+ borderRadius: 16,
+ marginBottom: 16,
+ },
+ totalProducts: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.textLowContrast,
+ },
+ productCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 16,
+ marginBottom: 16,
+ overflow: 'hidden', // Asegura que el contenido no se salga de la carta
+ },
+ productImage: {
+ width: 50,
+ height: 50,
+ backgroundColor: Colors.gray_100,
+ borderRadius: 8,
+ marginRight: 16,
+ },
+ productInfo: {
+ flex: 1, // Permite que el contenido ocupe el espacio restante
+ },
+ productName: {
+ fontSize: FontSizes.b2.size,
+ color: Colors.textMain,
+ flexShrink: 1, // Evita que el texto desborde
+ },
+ productQuantity: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.textLowContrast,
+ marginTop: 4,
+ },
+ productPrice: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.textLowContrast,
+ },
+ floatingButton: {
+ position: 'absolute',
+ bottom: 16,
+ left: 16,
+ right: 16,
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ errorText: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.semanticDanger,
+ },
+ scrollSpacer: {
+ height: 64 + 16,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -163,
+ top: 20,
+ zIndex: 1000,
+ },
+ expandButton: {
+ marginBottom: 12,
+ },
+ modalContainer: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ closeButton: {
+ position: 'absolute',
+ top: 16,
+ right: 16,
+ zIndex: 10,
+ backgroundColor: Colors.textWhite,
+ padding: 8,
+ borderRadius: 16,
+ },
+ fullScreenMapContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ modalFloatingButton: {
+ position: 'absolute',
+ bottom: 16,
+ left: 16,
+ right: 16,
+ zIndex: 10,
+ },
+});
+
+export default DeliveryDetailScreen;
diff --git a/src/screens/delivery/DeliveryHistoryDetailScreen.tsx b/src/screens/delivery/DeliveryHistoryDetailScreen.tsx
new file mode 100644
index 0000000..8dca6d3
--- /dev/null
+++ b/src/screens/delivery/DeliveryHistoryDetailScreen.tsx
@@ -0,0 +1,669 @@
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ ActivityIndicator,
+ Image,
+ Modal,
+ TouchableOpacity,
+} from 'react-native';
+import { useLocalSearchParams } from 'expo-router';
+import {
+ PhoneIcon,
+ EnvelopeIcon,
+ XMarkIcon,
+} from 'react-native-heroicons/solid';
+import Badge from '../../components/Badge';
+import PoppinsText from '../../components/PoppinsText';
+import CustomerAvatar from '../../components/CustomerAvatar';
+import HistoryMap from '../../components/HistoryMap';
+import { Colors, FontSizes } from '../../styles/theme';
+import { DeliveryService } from '../../services/delivery';
+import { BranchService } from '../../services/branches';
+import {
+ OrderDeliveryDetailedResponse,
+ BranchResponse,
+ OrderDetailedResponse,
+} from '@pharmatech/sdk';
+import { UserService } from '../../services/user';
+import Button from '../../components/Button';
+
+const DeliveryHistoryDetailScreen: React.FC = () => {
+ const { id } = useLocalSearchParams();
+ const [orderDetails, setOrderDetails] =
+ useState(null);
+ const [branchDetails, setBranchDetails] = useState(
+ null,
+ );
+ const [loading, setLoading] = useState(true);
+
+ const [isMapModalVisible, setIsMapModalVisible] = useState(false);
+
+ const [order, setOrder] = useState(
+ undefined,
+ );
+ useEffect(() => {
+ const fetchOrder = async () => {
+ try {
+ if (!orderDetails) {
+ return; // No intentes cargar si no hay detalles de la orden
+ }
+
+ const orderId = orderDetails.orderId; // Extraer el ID de la orden
+ console.log('ID de la orden:', orderId);
+
+ const order = await UserService.getOrder(orderId); // Usar el ID de la orden
+
+ if (order.success) {
+ console.log('Datos del pedido:', order.data); // Log para verificar los datos
+ setOrder(order.data);
+ } else {
+ console.error('Error al obtener el pedido:', order.error);
+ }
+ } catch (error) {
+ console.error('Error en fetchOrder:', error);
+ }
+ };
+
+ fetchOrder();
+ }, [orderDetails]);
+
+ useEffect(() => {
+ const fetchOrderDetails = async () => {
+ try {
+ setLoading(true); // Inicia la carga
+ if (!id) {
+ throw new Error('ID del pedido no proporcionado');
+ }
+
+ // Obtener detalles completos del pedido
+ const details = await DeliveryService.getOrderDetails(id as string);
+ setOrderDetails(details);
+
+ // Obtener detalles de la sucursal
+ const branches = await BranchService.findAll({ page: 1, limit: 100 });
+ const branch = branches.results.find(
+ (branch) => branch.id === details.branchId,
+ );
+ setBranchDetails(branch || null);
+ } catch (error) {
+ console.error('Error al obtener los detalles del pedido:', error);
+ } finally {
+ setLoading(false); // Finaliza la carga
+ }
+ };
+
+ fetchOrderDetails();
+ }, [id]);
+
+ if (loading) {
+ return ;
+ }
+
+ if (!orderDetails || !branchDetails) {
+ return (
+
+
+ No se encontraron datos del pedido.
+
+
+ );
+ }
+
+ // Calcular tiempos usando la función reutilizada
+ const elapsedTime = calculateElapsedTime(orderDetails.createdAt);
+ const completionTime = new Date(orderDetails.updatedAt).toLocaleTimeString(
+ 'es-VE',
+ {
+ hour: '2-digit',
+ minute: '2-digit',
+ },
+ );
+
+ return (
+
+
+
+ Resumen del pedido
+
+ {/* Badge con información del historial */}
+
+
+ Hace {`${elapsedTime}, ${completionTime}`}
+
+
+
+ {/* Información combinada en una sola carta */}
+
+ {/* Información del cliente */}
+
+
+
+
+
+ {orderDetails.user.firstName} {orderDetails.user.lastName}
+
+
+
+
+
+ {orderDetails.user.phoneNumber || 'Sin teléfono'}
+
+
+
+
+
+ {orderDetails.user.email || 'Sin correo'}
+
+
+
+
+
+ {/* Contenedor de la información de la sucursal */}
+
+ {/* Línea vertical punteada */}
+
+
+ {/* Línea vertical punteada */}
+
+
+ {/* Círculo inferior */}
+
+
+
+
+
+ {branchDetails.name}
+
+
+
+ {branchDetails.address}
+
+
+
+ {/* Contenedor de la información de la entrega */}
+
+
+ Entrega
+
+
+ {orderDetails.address.adress}
+
+
+
+
+
+ {/* Historial de la entrega
+
+
+ Historial de la entrega
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pedido iniciado
+
+
+ {`${creationTime}`}
+
+
+
+
+
+ Pedido recogido
+
+
+
+ {`${creationTimePlusOneHour}`}
+
+
+
+
+
+ Entregado
+
+
+ {`${completionTime}`}
+
+
+
+ */}
+
+ {/* Recorrido de entrega */}
+
+ Recorrido de entrega
+
+
+
+ setIsMapModalVisible(true)}
+ style={styles.expandButton}
+ />
+
+ {/* Pedido */}
+
+
+ Pedido
+
+
+ Total:{' '}
+ {order?.details?.reduce(
+ (total, detail) => total + detail.quantity,
+ 0,
+ ) || 0}{' '}
+ productos
+
+
+ {order?.details?.map((detail, index) => {
+ console.log(`Producto ${index + 1}:`, detail); // Log para depuración
+ return (
+
+ {/* Imagen del producto */}
+
+
+ {/* Nombre de la presentación del producto */}
+
+ {detail.productPresentation.product.name}{' '}
+ {detail.productPresentation.presentation.name}{' '}
+ {detail.productPresentation.presentation.quantity}{' '}
+ {detail.productPresentation.presentation.measurementUnit}
+
+ {/* Cantidad del producto */}
+
+ Cantidad: {detail.quantity}
+
+
+
+ );
+ })}
+
+
+
+
+ {/* Modal para el mapa ampliado */}
+ setIsMapModalVisible(false)}
+ >
+
+ {/* Botón de cierre */}
+ setIsMapModalVisible(false)}
+ >
+
+
+
+ {/* Mapa ampliado */}
+
+
+
+
+ );
+};
+
+// Función reutilizada para calcular el tiempo transcurrido
+const calculateElapsedTime = (createdAt: string): string => {
+ const now = new Date();
+ const createdDate = new Date(createdAt);
+ const diffInMinutes = Math.floor(
+ (now.getTime() - createdDate.getTime()) / (1000 * 60),
+ );
+
+ if (diffInMinutes < 60) {
+ return `${diffInMinutes} minutos`;
+ } else if (diffInMinutes < 1440) {
+ const hours = Math.floor(diffInMinutes / 60);
+ return `${hours} horas`;
+ } else {
+ const days = Math.floor(diffInMinutes / 1440);
+ return `${days} días`;
+ }
+};
+
+const styles = StyleSheet.create({
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ title: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.textMain,
+ },
+ container: {
+ marginTop: 32,
+ padding: 16,
+ },
+ statusContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ statusLabel: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textMain,
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ sectionTitle: {
+ fontSize: FontSizes.s1.size,
+ lineHeight: FontSizes.s1.lineHeight,
+ color: Colors.primary,
+ },
+ totalProducts: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.textLowContrast,
+ },
+ productCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 16,
+ marginBottom: 16,
+ overflow: 'hidden',
+ },
+ productImage: {
+ width: 50,
+ height: 50,
+ backgroundColor: Colors.gray_100,
+ borderRadius: 8,
+ marginRight: 16,
+ },
+ productInfo: {
+ flex: 1,
+ },
+ productName: {
+ fontSize: FontSizes.b2.size,
+ color: Colors.textMain,
+ flexShrink: 1,
+ },
+ productQuantity: {
+ fontSize: FontSizes.b3.size,
+ color: Colors.textLowContrast,
+ marginTop: 4,
+ },
+ locationsContainer: {
+ position: 'relative',
+ marginBottom: 16,
+ paddingLeft: 0,
+ },
+ verticalLine: {
+ position: 'absolute',
+ left: -12,
+ top: '25%',
+ height: '50%',
+ width: 2,
+ borderStyle: 'dashed',
+ borderWidth: 2,
+ borderColor: Colors.primary_300,
+ },
+ circleTop: {
+ position: 'absolute',
+ left: -16,
+ top: '25%',
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ backgroundColor: Colors.primary_300,
+ transform: [{ translateY: -8 }],
+ },
+ circleBottom: {
+ position: 'absolute',
+ left: -16,
+ top: '75%',
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ backgroundColor: Colors.primary_300,
+ transform: [{ translateY: -2 }],
+ },
+ verticalLineHistory: {
+ position: 'absolute',
+ left: -12,
+ top: '15%',
+ height: '75%',
+ width: 2,
+ borderWidth: 2,
+ borderColor: Colors.primary_300,
+ },
+ circleTopHistory: {
+ position: 'absolute',
+ left: -16,
+ top: '25%',
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ backgroundColor: Colors.primary_700,
+ transform: [{ translateY: -22 }],
+ },
+ circleMiddleHistory: {
+ position: 'absolute',
+ left: -16,
+ top: '75%',
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ backgroundColor: Colors.primary_700,
+ transform: [{ translateY: -45 }],
+ },
+ circleBottomHistory: {
+ position: 'absolute',
+ left: -16,
+ top: '75%',
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ backgroundColor: Colors.primary_700,
+ transform: [{ translateY: 14 }],
+ },
+ card: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 16,
+ marginBottom: 16,
+ marginLeft: 28,
+ flex: 1,
+ },
+ card2: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 16,
+ marginLeft: 28,
+ flex: 1,
+ },
+ card3: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 16,
+ paddingBottom: 8,
+ marginBottom: 16,
+ flex: 1,
+ },
+ combinedCard: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 16,
+ padding: 16,
+ marginBottom: 16,
+ flex: 1,
+ },
+ combinedCardHistory: {
+ borderRadius: 16,
+ padding: 16,
+ marginBottom: 16,
+ flex: 1,
+ },
+ cardRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ icon: {
+ marginRight: 6,
+ },
+ avatar: {
+ marginRight: 8,
+ },
+ cardTitle: {
+ fontSize: FontSizes.label.size,
+ lineHeight: FontSizes.label.lineHeight,
+ color: Colors.primary,
+ marginBottom: -6, // Reduce el espacio entre el título y el subtítulo
+ },
+ historyTitle: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.primary,
+ marginBottom: -9, // Reduce el espacio entre el título y el subtítulo
+ },
+ cardSubtitle: {
+ fontSize: FontSizes.label.size,
+ color: Colors.textMain,
+ },
+ historySubtitle: {
+ fontSize: FontSizes.label.size,
+ color: Colors.textLowContrast,
+ },
+ userName: {
+ fontSize: FontSizes.s2.size,
+ lineHeight: FontSizes.s2.lineHeight,
+ color: Colors.textMain,
+ },
+ sectionContainer: {
+ marginLeft: 4,
+ marginBottom: 16, // Espaciado entre secciones
+ },
+ sectionContainerB: {
+ marginLeft: 4,
+ },
+ spacing: {
+ marginBottom: 8, // Espaciado entre secciones
+ },
+ info: {
+ marginLeft: 18, // Espaciado entre secciones
+ },
+ mapPlaceholder: {
+ height: 300,
+ backgroundColor: Colors.gray_100,
+ borderRadius: 16,
+ marginBottom: 16,
+ },
+ floatingButton: {
+ position: 'absolute',
+ bottom: 16,
+ left: 16,
+ right: 16,
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ errorText: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.semanticDanger,
+ },
+ scrollSpacer: {
+ height: 64,
+ },
+ expandButton: {
+ marginBottom: 12,
+ },
+ modalContainer: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ closeButton: {
+ position: 'absolute',
+ top: 16,
+ right: 16,
+ zIndex: 10,
+ backgroundColor: Colors.textWhite,
+ padding: 8,
+ borderRadius: 16,
+ },
+});
+
+export default DeliveryHistoryDetailScreen;
diff --git a/src/screens/delivery/DeliveryHistoryScreen.tsx b/src/screens/delivery/DeliveryHistoryScreen.tsx
new file mode 100644
index 0000000..5112bc5
--- /dev/null
+++ b/src/screens/delivery/DeliveryHistoryScreen.tsx
@@ -0,0 +1,198 @@
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ ActivityIndicator,
+ RefreshControl,
+} from 'react-native';
+import * as SecureStore from 'expo-secure-store';
+import { useRouter } from 'expo-router';
+import PoppinsText from '../../components/PoppinsText';
+import HistoryOrderCard from '../../components/HistoryOrderCard';
+import { Colors, FontSizes } from '../../styles/theme';
+import { DeliveryService } from '../../services/delivery';
+import { BranchService } from '../../services/branches';
+import {
+ OrderDeliveryDetailedResponse,
+ BranchResponse,
+ OrderDeliveryStatus,
+} from '@pharmatech/sdk';
+
+const DeliveryHistoryScreen: React.FC = () => {
+ const router = useRouter();
+ const [completedOrders, setCompletedOrders] = useState<
+ OrderDeliveryDetailedResponse[]
+ >([]);
+ const [branchNames, setBranchNames] = useState>({});
+ const [loading, setLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+
+ const fetchCompletedOrders = async () => {
+ try {
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ const payload = JSON.parse(atob(jwt.split('.')[1]));
+ const employeeId = payload.sub;
+
+ // Obtener órdenes completadas
+ const response = await DeliveryService.getAssignedOrders(employeeId, [
+ OrderDeliveryStatus.DELIVERED,
+ ]);
+
+ // Obtener detalles de las órdenes
+ const detailedOrders = await Promise.all(
+ response.results.map(async (order) => {
+ const details = await DeliveryService.getOrderDetails(order.id);
+ return details;
+ }),
+ );
+
+ // Obtener nombres de sucursales
+ const branchIds = [
+ ...new Set(detailedOrders.map((order) => order.branchId)),
+ ];
+ const branches = await BranchService.findAll({ page: 1, limit: 100 });
+ const branchMap = branches.results.reduce(
+ (acc: Record, branch: BranchResponse) => {
+ if (branchIds.includes(branch.id)) {
+ acc[branch.id] = branch.name;
+ }
+ return acc;
+ },
+ {},
+ );
+
+ setBranchNames(branchMap);
+ setCompletedOrders(detailedOrders);
+ } catch (error) {
+ console.error('Error al obtener el historial de pedidos:', error);
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ const handleViewDetails = (order: OrderDeliveryDetailedResponse) => {
+ const query = encodeURIComponent(JSON.stringify(order));
+ router.push(`/deliveryHistoryDetail/${order.id}?data=${query}`);
+ };
+
+ const onRefresh = async () => {
+ setRefreshing(true);
+ await fetchCompletedOrders();
+ };
+
+ useEffect(() => {
+ fetchCompletedOrders();
+ }, []);
+
+ return (
+
+
+ }
+ >
+
+ Historial de Pedidos
+
+ {loading ? (
+
+ ) : completedOrders.length === 0 ? (
+
+ No tienes pedidos completados.
+
+ ) : (
+ completedOrders.map((order) => (
+ handleViewDetails(order)}
+ />
+ ))
+ )}
+
+
+
+ );
+};
+
+const calculateElapsedTime = (
+ createdAt: string,
+ updatedAt?: string,
+): string => {
+ const endDate = updatedAt ? new Date(updatedAt) : new Date();
+ const createdDate = new Date(createdAt);
+ const diffInMinutes = Math.floor(
+ (endDate.getTime() - createdDate.getTime()) / (1000 * 60),
+ );
+
+ if (diffInMinutes < 60) {
+ return `${diffInMinutes} minutos`;
+ } else if (diffInMinutes < 1440) {
+ const hours = Math.floor(diffInMinutes / 60);
+ return `${hours} horas`;
+ } else {
+ const days = Math.floor(diffInMinutes / 1440);
+ return `${days} días`;
+ }
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 16,
+ marginHorizontal: 4,
+ },
+ height: {
+ height: 64,
+ },
+ title: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 16,
+ },
+ noOrdersText: {
+ fontSize: FontSizes.b2.size,
+ color: Colors.textLowContrast,
+ textAlign: 'center',
+ marginTop: 16,
+ },
+});
+
+export default DeliveryHistoryScreen;
diff --git a/src/screens/delivery/DeliveryHomeScreen.tsx b/src/screens/delivery/DeliveryHomeScreen.tsx
new file mode 100644
index 0000000..3485722
--- /dev/null
+++ b/src/screens/delivery/DeliveryHomeScreen.tsx
@@ -0,0 +1,387 @@
+import { useFocusEffect } from '@react-navigation/native'; // Importar useFocusEffect
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ ActivityIndicator,
+ RefreshControl,
+} from 'react-native';
+import * as SecureStore from 'expo-secure-store';
+import { useRouter } from 'expo-router';
+import PoppinsText from '../../components/PoppinsText';
+import OrderCard from '../../components/OrderCard';
+import Popup from '../../components/Popup';
+import { Colors, FontSizes } from '../../styles/theme';
+import { DeliveryService } from '../../services/delivery';
+import { BranchService } from '../../services/branches';
+import {
+ OrderDeliveryDetailedResponse,
+ OrderDeliveryStatus,
+} from '@pharmatech/sdk';
+import { useAlert } from '../../components/AlertProvider'; // Importar el hook useAlert
+import * as Location from 'expo-location'; // Importar Location para obtener la ubicación actual
+import { Config } from '../../config'; // Importar la configuración de la API de Google Maps
+import {
+ initializeSocket,
+ disconnectSocket,
+} from '../../lib/deliverySocket/deliverySocket'; // Importar funciones de socket
+import { Socket } from 'socket.io-client'; // Importar el tipo Socket
+
+// Definir el tipo para un leg de la API de Google Maps Directions
+interface GoogleMapsLeg {
+ duration: {
+ value: number; // Duración en segundos
+ };
+}
+
+const calculateTravelTime = async (
+ origin: { latitude: number; longitude: number },
+ destination: { latitude: number; longitude: number },
+): Promise => {
+ try {
+ const response = await fetch(
+ `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.latitude},${origin.longitude}&destination=${destination.latitude},${destination.longitude}&key=${Config.googleMapsApiKey}`,
+ );
+ const data = await response.json();
+
+ if (data.routes && data.routes.length > 0) {
+ // Extraer el tiempo estimado en segundos
+ const durationInSeconds = data.routes[0].legs.reduce(
+ (total: number, leg: GoogleMapsLeg) => total + leg.duration.value,
+ 0,
+ );
+
+ // Convertir a minutos
+ return Math.ceil(durationInSeconds / 60);
+ } else {
+ console.error('No se pudo calcular el tiempo estimado.');
+ return 0;
+ }
+ } catch (error) {
+ console.error('Error al calcular el tiempo estimado:', error);
+ return 0;
+ }
+};
+
+// Extender el tipo OrderDeliveryDetailedResponse
+type ExtendedOrderDeliveryDetailedResponse = OrderDeliveryDetailedResponse & {
+ formattedEstimatedTime: string; // Nueva propiedad para el tiempo formateado
+};
+
+export default function DeliveryHomeScreen() {
+ const router = useRouter();
+ const { showAlert } = useAlert();
+
+ const [orders, setOrders] = useState(
+ [],
+ );
+ const [branchNames, setBranchNames] = useState<
+ Record
+ >({});
+ const [loading, setLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+ const [selectedOrder, setSelectedOrder] =
+ useState(null);
+ const [showDiscardPopup, setShowDiscardPopup] = useState(false);
+
+ const fetchAssignedOrders = async () => {
+ try {
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ const payload = JSON.parse(atob(jwt.split('.')[1]));
+ const employeeId = payload.sub;
+
+ // Obtener las órdenes asignadas
+ const response = await DeliveryService.getAssignedOrders(employeeId);
+
+ // Obtener las sucursales
+ const branches = await BranchService.findAll({ page: 1, limit: 100 });
+ const branchMap = branches.results.reduce(
+ (
+ acc: Record<
+ string,
+ { name: string; latitude: number; longitude: number }
+ >,
+ branch,
+ ) => {
+ acc[branch.id] = {
+ name: branch.name,
+ latitude: branch.latitude,
+ longitude: branch.longitude,
+ };
+ return acc;
+ },
+ {},
+ );
+ setBranchNames(branchMap);
+
+ // Procesar las órdenes con cálculo de tiempo estimado
+ const detailedOrders: ExtendedOrderDeliveryDetailedResponse[] =
+ await Promise.all(
+ response.results.map(async (order) => {
+ const details = await DeliveryService.getOrderDetails(order.id);
+
+ // Obtener la ubicación actual del repartidor
+ const location = await Location.getCurrentPositionAsync({});
+ const deliveryLocation = {
+ latitude: location.coords.latitude,
+ longitude: location.coords.longitude,
+ };
+
+ // Obtener la ubicación de la sucursal
+ const branchLocation = {
+ latitude: branchMap[details.branchId]?.latitude || 0,
+ longitude: branchMap[details.branchId]?.longitude || 0,
+ };
+
+ // Obtener la ubicación del cliente
+ const customerLocation = {
+ latitude: details.address.latitude,
+ longitude: details.address.longitude,
+ };
+
+ // Calcular tiempos de viaje
+ const timeToBranch = await calculateTravelTime(
+ deliveryLocation,
+ branchLocation,
+ );
+ const timeToCustomer = await calculateTravelTime(
+ branchLocation,
+ customerLocation,
+ );
+
+ // Tiempo total estimado en minutos
+ const totalEstimatedTime = timeToBranch + timeToCustomer;
+
+ // Formatear el tiempo estimado
+ const formattedEstimatedTime =
+ totalEstimatedTime < 60
+ ? `${totalEstimatedTime} minutos`
+ : `${Math.floor(totalEstimatedTime / 60)} horas ${
+ totalEstimatedTime % 60
+ } minutos`;
+
+ // Retornar la orden con el tiempo estimado formateado
+ return {
+ ...details,
+ estimatedTime: new Date(), // Mantener el tipo original como Date
+ formattedEstimatedTime, // Asignar el tiempo formateado
+ };
+ }),
+ );
+
+ setOrders(detailedOrders);
+ } catch (error) {
+ console.error('Error al obtener las órdenes asignadas:', error);
+ showAlert(
+ 'error',
+ 'Error',
+ 'No se pudieron cargar las órdenes asignadas.',
+ );
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ // Usar useFocusEffect para refrescar la pantalla al regresar
+ useFocusEffect(
+ useCallback(() => {
+ fetchAssignedOrders(); // Refrescar las órdenes asignadas
+ }, []),
+ );
+
+ useEffect(() => {
+ let socket: Socket;
+
+ const setupSocket = async () => {
+ try {
+ socket = await initializeSocket();
+ socket.connect();
+
+ socket.on('deliveryUpdated', (data: { id: string; status: string }) => {
+ setOrders((prevOrders) =>
+ prevOrders.map((order) =>
+ order.id === data.id
+ ? {
+ ...order,
+ deliveryStatus: data.status as OrderDeliveryStatus,
+ }
+ : order,
+ ),
+ );
+ });
+ } catch (error) {
+ console.error('Error configurando el WebSocket:', error);
+ }
+ };
+
+ setupSocket();
+
+ return () => {
+ if (socket) {
+ socket.off('deliveryUpdated');
+ disconnectSocket();
+ }
+ };
+ }, []);
+
+ const handleTakeOrder = (order: OrderDeliveryDetailedResponse) => {
+ const query = encodeURIComponent(JSON.stringify(order));
+ router.push(`/deliveryDetail/${order.id}?data=${query}`);
+ };
+
+ const handleDiscardOrder = (order: OrderDeliveryDetailedResponse) => {
+ setSelectedOrder(order);
+ setShowDiscardPopup(true);
+ };
+
+ const confirmDiscardOrder = async () => {
+ if (!selectedOrder) return;
+
+ try {
+ setLoading(true);
+ await DeliveryService.updateOrderStatus(
+ selectedOrder.id,
+ OrderDeliveryStatus.TO_ASSIGN,
+ );
+ showAlert('success', 'Éxito', 'La orden ha sido descartada.');
+ setOrders((prevOrders) =>
+ prevOrders.filter((order) => order.id !== selectedOrder.id),
+ );
+ } catch (error) {
+ console.error('Error al descartar la orden:', error);
+ showAlert('error', 'Error', 'No se pudo descartar la orden.');
+ } finally {
+ setLoading(false);
+ setShowDiscardPopup(false);
+ setSelectedOrder(null);
+ }
+ };
+
+ const cancelDiscardOrder = () => {
+ setShowDiscardPopup(false);
+ setSelectedOrder(null);
+ };
+
+ const calculateElapsedTime = (createdAt: string): string => {
+ const now = new Date();
+ const createdDate = new Date(createdAt);
+ const diffInMinutes = Math.floor(
+ (now.getTime() - createdDate.getTime()) / (1000 * 60),
+ );
+
+ if (diffInMinutes < 60) {
+ return `${diffInMinutes} minutos`;
+ } else if (diffInMinutes < 1440) {
+ const hours = Math.floor(diffInMinutes / 60);
+ return `${hours} horas`;
+ } else {
+ const days = Math.floor(diffInMinutes / 1440);
+ return `${days} días`;
+ }
+ };
+
+ const onRefresh = async () => {
+ setRefreshing(true);
+ await fetchAssignedOrders();
+ };
+
+ return (
+
+
+ }
+ >
+
+ Pedidos asignados
+
+ {loading ? (
+
+ ) : orders.length === 0 ? (
+
+ No tienes pedidos asignados.
+
+ ) : (
+ orders.map((order) => (
+ handleTakeOrder(order)}
+ onDiscardOrder={
+ order.deliveryStatus === 'assigned'
+ ? () => handleDiscardOrder(order)
+ : undefined
+ }
+ />
+ ))
+ )}
+
+
+
+ {/* Popup de confirmación para descartar orden */}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ padding: 16,
+ marginHorizontal: 4,
+ },
+ height: {
+ height: 64,
+ },
+ title: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 16,
+ },
+ noOrdersText: {
+ fontSize: FontSizes.b2.size,
+ color: Colors.textLowContrast,
+ textAlign: 'center',
+ marginTop: 16,
+ },
+});
diff --git a/src/screens/tab/BranchesScreen.tsx b/src/screens/tab/BranchesScreen.tsx
index bc40068..9d70be8 100644
--- a/src/screens/tab/BranchesScreen.tsx
+++ b/src/screens/tab/BranchesScreen.tsx
@@ -1,12 +1,150 @@
-import React from 'react';
-import { View, StyleSheet } from 'react-native';
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ Modal,
+ Pressable,
+} from 'react-native';
+import { useFocusEffect } from '@react-navigation/native';
+import { StateService } from '../../services/state';
+import { BranchService } from '../../services/branches';
+import { StateResponse, BranchResponse } from '@pharmatech/sdk';
+import BranchCard from '../../components/BranchCard';
+import { Colors, FontSizes } from '../../styles/theme';
+import { useRouter } from 'expo-router';
import PoppinsText from '../../components/PoppinsText';
-import { Colors } from '../../styles/theme';
export default function BranchesScreen() {
+ const [states, setStates] = useState([]);
+ const [branches, setBranches] = useState([]);
+ const [selectedState, setSelectedState] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [modalVisible, setModalVisible] = useState(false);
+ const router = useRouter();
+
+ // Cargar todos los estados al inicio
+ useEffect(() => {
+ const fetchStates = async () => {
+ setLoading(true);
+ const response = await StateService.getStates(1, 100);
+ if (response.success && response.data) setStates(response.data.results);
+ setLoading(false);
+ };
+ fetchStates();
+ }, []);
+
+ // Cargar sucursales al seleccionar un estado
+ useEffect(() => {
+ if (selectedState) {
+ const fetchBranches = async () => {
+ setLoading(true);
+ const response = await BranchService.findAll({
+ stateId: selectedState,
+ });
+ if (response.results) setBranches(response.results);
+ setLoading(false);
+ };
+ fetchBranches();
+ }
+ }, [selectedState]);
+
+ // Restablecer el estado del modal y el estado seleccionado al volver a la pantalla
+ useFocusEffect(
+ React.useCallback(() => {
+ setModalVisible(false); // Cerramos el modal
+ setSelectedState(null); // Restablecemos el estado seleccionado
+ }, []),
+ );
+
return (
- Pantalla Sucursales
+
+ Selecciona un estado
+
+ item.id}
+ renderItem={({ item }) => (
+ {
+ setSelectedState(item.id);
+ setModalVisible(true);
+ }}
+ >
+ {item.name}
+
+ )}
+ contentContainerStyle={styles.list}
+ ListEmptyComponent={
+ !loading ? (
+
+ No hay estados disponibles.
+
+ ) : null
+ }
+ />
+
+ {/* Modal para mostrar las sucursales */}
+ {
+ setModalVisible(false);
+ setSelectedState(null);
+ }}
+ >
+ {/* Overlay fijo */}
+
+
+ {/* Contenido del modal */}
+
+ {
+ setModalVisible(false);
+ setSelectedState(null);
+ }}
+ />
+
+ {/* Indicador de deslizamiento */}
+
+
+ {/* Encabezado del modal */}
+
+
+ Sucursales en el estado seleccionado
+
+
+
+ {/* Contenido del modal */}
+ item.id}
+ renderItem={({ item }) => (
+ {
+ router.push(`/branchDetail/${item.id}`);
+ }}
+ />
+ )}
+ contentContainerStyle={styles.list}
+ ListEmptyComponent={
+ !loading ? (
+
+ No hay sucursales disponibles.
+
+ ) : null
+ }
+ />
+
+
+
+
);
}
@@ -14,8 +152,80 @@ export default function BranchesScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
backgroundColor: Colors.bgColor,
+ padding: 16,
+ paddingTop: -16,
+ },
+ height: {
+ height: 64,
+ },
+ title: {
+ fontSize: FontSizes.s1.size,
+ lineHeight: FontSizes.s1.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 4,
+ },
+ list: {
+ marginTop: 8,
+ },
+ stateCard: {
+ backgroundColor: Colors.menuWhite,
+ borderRadius: 8,
+ padding: 16,
+ marginBottom: 10,
+ shadowColor: '#000',
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ stateName: {
+ fontSize: FontSizes.s2.size,
+ lineHeight: FontSizes.s2.lineHeight,
+ color: Colors.textMain,
+ },
+ emptyText: {
+ textAlign: 'center',
+ color: Colors.textLowContrast,
+ marginTop: 20,
+ },
+ modalOverlay: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ zIndex: 1,
+ },
+ modalContainer: {
+ flex: 1,
+ justifyContent: 'flex-end',
+ zIndex: 2,
+ },
+ modalDismissArea: {
+ flex: 1,
+ },
+ modalContent: {
+ backgroundColor: Colors.bgColor,
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ padding: 16,
+ maxHeight: '80%',
+ },
+ modalHandle: {
+ width: 40,
+ height: 4,
+ backgroundColor: Colors.textLowContrast,
+ borderRadius: 2,
+ alignSelf: 'center',
+ marginBottom: 16,
+ },
+ modalHeader: {
+ marginBottom: 8,
+ },
+ modalTitle: {
+ fontSize: FontSizes.s1.size,
+ lineHeight: FontSizes.s1.lineHeight,
+ color: Colors.textMain,
},
});
diff --git a/src/screens/tab/CategoriesScreen.tsx b/src/screens/tab/CategoriesScreen.tsx
index 1ecbcbc..67f7b1b 100644
--- a/src/screens/tab/CategoriesScreen.tsx
+++ b/src/screens/tab/CategoriesScreen.tsx
@@ -1,58 +1,136 @@
-import React, { useState } from 'react';
-import { View, StyleSheet } from 'react-native';
+import React, { useEffect, useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ ActivityIndicator,
+ Image,
+ ListRenderItem,
+} from 'react-native';
import PoppinsText from '../../components/PoppinsText';
-import Button from '../../components/Button';
-import { useRouter } from 'expo-router';
-import { Colors } from '../../styles/theme';
-import * as SecureStore from 'expo-secure-store';
-import Popup from '../../components/Popup';
+import { Colors, FontSizes } from '../../styles/theme';
+import Alert from '../../components/Alerts';
+import { CategoryService } from '../../services/category';
+import { CategoryResponse } from '@pharmatech/sdk';
+import CategoryDefaultImg from '../../assets/images/defaults/category.png';
+import { router } from 'expo-router';
export default function CategoriesScreen() {
- const router = useRouter();
- const [showPopup, setShowPopup] = useState(false);
+ const [categories, setCategories] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
- const handleLogout = async () => {
- try {
- await SecureStore.deleteItemAsync('auth_token');
- setShowPopup(false);
- router.replace('/login');
- } catch (error) {
- console.error('Error al cerrar sesión:', error);
- }
- };
+ useEffect(() => {
+ const fetchCategories = async () => {
+ setLoading(true);
+ try {
+ const categories = await CategoryService.getCategories(1, 100);
+ if (categories.success) setCategories(categories.data.results);
+ } catch (error) {
+ console.log(error);
+ setErrorMessage('Ocurrio un error');
+ }
+ setLoading(false);
+ };
+
+ fetchCategories();
+ }, []);
+
+ const renderItem: ListRenderItem = ({ item }) => (
+ {
+ router.push(`/categoryDetail/${item.id}`);
+ }}
+ >
+
+ {item.name}
+
+ );
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
return (
- {/* Popup */}
- setShowPopup(false),
- }}
- onClose={() => setShowPopup(false)}
- />
- Pantalla Categorías
- setShowPopup(true)}
- variant="secondary"
- mode="filled"
- size="large"
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ borderColor
+ />
+ )}
+
+
+ Categorías
+
+ item.id.toString()}
+ numColumns={2}
+ contentContainerStyle={styles.listContainer}
+ showsVerticalScrollIndicator={true}
+ alwaysBounceVertical={true}
/>
+
);
}
const styles = StyleSheet.create({
container: {
+ backgroundColor: Colors.bgColor,
+ flex: 1,
+ },
+ height: {
+ height: 64,
+ },
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -162,
+ top: 20,
+ right: 0,
+ zIndex: 1000,
+ },
+ title: {
+ margin: 5,
+ paddingHorizontal: 10,
+ fontSize: FontSizes.b1.size,
+ },
+ listContainer: {
+ marginHorizontal: 10,
+ },
+ gridItem: {
+ flex: 1,
+ margin: 10,
+ maxWidth: 150,
+ padding: 15,
+ borderRadius: 15,
+ backgroundColor: Colors.textWhite,
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: Colors.gray_100,
+ },
+ categoryText: {
+ marginTop: 10,
+ fontWeight: 'bold',
+ textAlign: 'center',
+ },
+ center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
diff --git a/src/screens/tab/EmailVerificationModal.tsx b/src/screens/tab/EmailVerificationModal.tsx
new file mode 100644
index 0000000..59688c2
--- /dev/null
+++ b/src/screens/tab/EmailVerificationModal.tsx
@@ -0,0 +1,377 @@
+import React, { useState, useRef, useCallback, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ TextInput,
+ Animated,
+ TouchableOpacity,
+ Text,
+ Modal,
+} from 'react-native';
+import Button from '../../components/Button';
+import PoppinsText from '../../components/PoppinsText';
+import Alert from '../../components/Alerts';
+import { Colors, FontSizes } from '../../styles/theme';
+import { AuthService } from '../../services/auth';
+import { XMarkIcon } from 'react-native-heroicons/outline';
+
+export default function EmailVerificationModal({
+ visible,
+ onClose,
+}: {
+ visible: boolean;
+ onClose: () => void;
+}) {
+ const [resending, setResending] = useState(false);
+ const [countdown, setCountdown] = useState(0);
+ const [loading, setLoading] = useState(false);
+ const [code, setCode] = useState(['', '', '', '', '', '']);
+ const codeRefs = useRef>([]);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [showVerifySuccessAlert, setShowVerifySuccessAlert] = useState(false);
+ const [showResendSuccessAlert, setShowResendSuccessAlert] = useState(false);
+ const [showInvalidCodeMessage, setShowInvalidCodeMessage] = useState(false);
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (resending && countdown > 0) {
+ timer = setInterval(() => {
+ setCountdown((prev) => prev - 1);
+ }, 1000);
+ } else if (countdown === 0) {
+ setResending(false);
+ }
+ return () => clearInterval(timer);
+ }, [resending, countdown]);
+
+ useEffect(() => {
+ if (showErrorAlert) {
+ const timer = setTimeout(() => setShowErrorAlert(false), 4000);
+ return () => clearTimeout(timer);
+ }
+ }, [showErrorAlert]);
+
+ useEffect(() => {
+ if (showVerifySuccessAlert) {
+ const timer = setTimeout(() => setShowVerifySuccessAlert(false), 4000);
+ return () => clearTimeout(timer);
+ }
+ }, [showVerifySuccessAlert]);
+
+ useEffect(() => {
+ if (showResendSuccessAlert) {
+ const timer = setTimeout(() => setShowResendSuccessAlert(false), 4000);
+ return () => clearTimeout(timer);
+ }
+ }, [showResendSuccessAlert]);
+
+ const handleVerifyCode = async () => {
+ const enteredCode = code.join('');
+ if (enteredCode.length !== 6) {
+ setErrorMessage('El código debe tener 6 dígitos');
+ setShowErrorAlert(true);
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const response = await AuthService.validateOtp(enteredCode);
+
+ if (response.success) {
+ setShowVerifySuccessAlert(true);
+ setCode(['', '', '', '', '', '']);
+ setShowInvalidCodeMessage(false);
+ setTimeout(() => onClose(), 2000);
+ } else {
+ setErrorMessage(
+ response.error ||
+ 'Error al verificar el código. Inténtalo nuevamente.',
+ );
+ if (response.error?.includes('I')) {
+ setShowInvalidCodeMessage(true);
+ }
+ setShowErrorAlert(true);
+ }
+ } catch {
+ setErrorMessage(
+ 'Error inesperado al verificar el código. Inténtalo nuevamente.',
+ );
+ setShowErrorAlert(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleResendCode = async () => {
+ if (resending) return;
+
+ setResending(true);
+ setCountdown(60);
+ try {
+ const response = await AuthService.resendOtp();
+ if (response.success) {
+ setShowResendSuccessAlert(true);
+ } else {
+ setErrorMessage(response.error || 'Error al reenviar el código.');
+ setShowErrorAlert(true);
+ }
+ } catch {
+ setErrorMessage('Error inesperado al reenviar el código.');
+ setShowErrorAlert(true);
+ }
+ };
+
+ const renderCodeInputs = useCallback(() => {
+ return (
+
+ {code.map((digit, index) => (
+ (codeRefs.current[index] = ref as TextInput)}
+ placeholder="•"
+ placeholderTextColor={Colors.textLowContrast}
+ style={styles.codeInput}
+ caretHidden={true}
+ keyboardType="number-pad"
+ maxLength={1}
+ value={digit}
+ onChangeText={(text) => {
+ const newCode = [...code];
+ const cleanedText = text.replace(/[^0-9]/g, '');
+ newCode[index] = cleanedText;
+ setCode(newCode);
+ setShowInvalidCodeMessage(false);
+ if (cleanedText && index < 5) {
+ codeRefs.current[index + 1]?.focus();
+ }
+ }}
+ onKeyPress={({ nativeEvent }) => {
+ if (nativeEvent.key === 'Backspace' && !digit && index > 0) {
+ const newCode = [...code];
+ newCode[index - 1] = '';
+ setCode(newCode);
+ setShowInvalidCodeMessage(false);
+ codeRefs.current[index - 1]?.focus();
+ }
+ }}
+ />
+ ))}
+
+ );
+ }, [code]);
+
+ return (
+ {
+ setCode(['', '', '', '', '', '']);
+ onClose();
+ }}
+ >
+
+
+ {
+ setCode(['', '', '', '', '', '']);
+ onClose();
+ }}
+ >
+
+
+
+
+
+ {showErrorAlert && (
+ setShowErrorAlert(false)}
+ />
+ )}
+ {showVerifySuccessAlert && (
+ setShowVerifySuccessAlert(false)}
+ />
+ )}
+ {showResendSuccessAlert && (
+ setShowResendSuccessAlert(false)}
+ />
+ )}
+
+
+
+
+ Confirma tu correo electrónico
+
+
+ Introduce el código enviado a tu correo para confirmarlo
+
+ {renderCodeInputs()}
+ {showInvalidCodeMessage && (
+
+ Código incorrecto
+
+ )}
+
+
+ Reenviar código
+
+ {resending && (
+
+ Puedes reenviar el código en {countdown} segundos
+
+ )}
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ modalContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ modalContent: {
+ height: 489,
+ width: 350,
+ backgroundColor: Colors.textWhite,
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: Colors.stroke,
+ paddingTop: 12,
+ paddingHorizontal: 20,
+ },
+ closeButton: {
+ position: 'absolute',
+ top: 10,
+ right: 20,
+ zIndex: 1000,
+ },
+ closeButtonIcon: {
+ width: 24,
+ height: 24,
+ color: Colors.textMain,
+ },
+ alertsContainer: {
+ position: 'absolute',
+ top: 20,
+ left: 0,
+ right: 0,
+ alignItems: 'center',
+ zIndex: 1000,
+ },
+ stepContainer: {
+ width: '100%',
+ marginVertical: 21,
+ marginTop: 70,
+ },
+ stepTitle: {
+ fontSize: FontSizes.h3.size,
+ lineHeight: FontSizes.h3.lineHeight,
+ color: Colors.textMain,
+ marginBottom: 24,
+ textAlign: 'center',
+ },
+ stepDescription: {
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ color: Colors.textLowContrast,
+ textAlign: 'center',
+ marginBottom: 24,
+ },
+ codeContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginHorizontal: 8,
+ marginBottom: 16,
+ },
+ codeInput: {
+ width: 42,
+ height: 48,
+ borderWidth: 1,
+ borderColor: Colors.stroke,
+ textAlign: 'center',
+ fontFamily: 'Poppins_400Regular',
+ color: Colors.textMain,
+ fontSize: FontSizes.b1.size,
+ lineHeight: FontSizes.b1.lineHeight,
+ borderRadius: 8,
+ backgroundColor: Colors.menuWhite,
+ includeFontPadding: false,
+ padding: 0,
+ textAlignVertical: 'center',
+ },
+ button: {
+ marginTop: 21,
+ width: '100%',
+ height: 50,
+ },
+ resendCodeButton: {
+ alignSelf: 'flex-end',
+ marginTop: 21,
+ },
+ resendCodeText: {
+ color: Colors.secondaryLight,
+ fontSize: FontSizes.b2.size,
+ lineHeight: FontSizes.b2.lineHeight,
+ fontFamily: 'Poppins_400Regular',
+ },
+ countdownText: {
+ marginTop: 8,
+ textAlign: 'right',
+ color: Colors.textLowContrast,
+ fontSize: FontSizes.b4.size,
+ fontFamily: 'Poppins_400Regular',
+ },
+ fadeAnim: {
+ opacity: 1,
+ },
+ disabledButton: {
+ opacity: 0.5,
+ },
+ invalidCodeMessage: {
+ color: Colors.semanticDanger,
+ fontSize: FontSizes.b2.size,
+ textAlign: 'left',
+ marginVertical: 8,
+ },
+ flexContainer: {
+ flex: 1,
+ },
+});
diff --git a/src/screens/tab/HomeScreen.tsx b/src/screens/tab/HomeScreen.tsx
index 3c85b4f..af1aa33 100644
--- a/src/screens/tab/HomeScreen.tsx
+++ b/src/screens/tab/HomeScreen.tsx
@@ -1,60 +1,243 @@
import React, { useState, useEffect } from 'react';
-import { View, StyleSheet, ScrollView } from 'react-native';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ ActivityIndicator,
+ RefreshControl,
+} from 'react-native';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import * as SecureStore from 'expo-secure-store';
+import { useCart } from '../../hooks/useCart';
import PoppinsText from '../../components/PoppinsText';
import { Colors, FontSizes } from '../../styles/theme';
import Carousel from '../../components/Carousel';
import { ProductService } from '../../services/products';
+import { Product } from '../../types/Product';
+import type { Promo } from '@pharmatech/sdk';
+import EmailVerificationModal from './EmailVerificationModal';
+import { decodeJWT } from '../../helper/jwtHelper';
+import { useNotifications } from '../../hooks/useNotifications';
export default function HomeScreen() {
- const [products, setProducts] = useState([{}]);
+ const [products, setProducts] = useState([]);
+ const [recommendedProducts, setRecommendedProducts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [loadingRecommendations, setLoadingRecommendations] = useState(true);
+ const [showEmailVerification, setShowEmailVerification] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+ const router = useRouter();
+ const { showEmailVerification: showEmailVerificationParam } =
+ useLocalSearchParams();
+ const { refreshNotifications } = useNotifications();
+ const { cartItems, updateCartQuantity, setCartUserId } = useCart();
+
+ const getItemQuantity = (productId: string) => {
+ const cartItem = cartItems.find((item) => item.id === productId.toString());
+ return cartItem ? cartItem.quantity : 0;
+ };
+
+ useEffect(() => {
+ const initialize = async () => {
+ setLoading(true);
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ router.replace('/login');
+ } else {
+ const decoded = decodeJWT(token);
+ if (decoded?.userId) {
+ setCartUserId(decoded.userId);
+ }
+ }
+
+ await obtainProducts();
+ await obtainRecommendedProducts();
+ setLoading(false);
+ };
+
+ initialize();
+ }, []);
+
+ useEffect(() => {
+ if (showEmailVerificationParam) {
+ const timer = setTimeout(() => {
+ setShowEmailVerification(true);
+ }, 2000);
+ return () => clearTimeout(timer);
+ }
+ }, [showEmailVerificationParam]);
const obtainProducts = async () => {
- const productsData = await ProductService.getProducts(1, 20);
+ setLoading(true);
+ try {
+ const productsData = await ProductService.getProducts(1, 20);
+ if (productsData.success) {
+ const pd = productsData.data.results;
+ // Filtra solo los que tengan stock > 0
+ const availableProducts = pd.filter((p) => p.stock > 0);
+ const carouselProducts = availableProducts.map((p) => {
+ // Usa el descuento real si hay promo, si no, no lo agregues
+ // @ts-expect-error: promo puede estar en p o en p.presentation
+ const promo: Promo | undefined = p.promo ?? p.presentation.promo;
+ const discount: Promo['discount'] | undefined = promo?.discount
+ ? Math.round(promo.discount * 100) / 100
+ : undefined;
+ return {
+ id: p.id,
+ presentationId: p.presentation.id,
+ productId: p.product.id,
+ imageUrl: p.product.images[0].url,
+ name:
+ p.product.name +
+ ' ' +
+ p.presentation.name +
+ ' ' +
+ p.presentation.quantity +
+ ' ' +
+ p.presentation.measurementUnit,
+ category: p.product.categories[0].name,
+ originalPrice: p.price,
+ ...(discount !== undefined ? { discount } : {}),
+ finalPrice: p.price,
+ quantity: getItemQuantity(p.id),
+ getQuantity: (quantity: number) => {
+ updateCartQuantity(p.id, quantity);
+ },
+ };
+ });
- if (productsData.success) {
- const pd = productsData.data.results;
- const carouselProducts: object[] = [];
+ setProducts(carouselProducts);
+ } else {
+ console.log(productsData.error);
+ }
+ } catch (error) {
+ console.error('Error fetching products:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
- pd.forEach((p) => {
- carouselProducts.push({
+ const obtainRecommendedProducts = async () => {
+ setLoadingRecommendations(true);
+ try {
+ const recommendations = await ProductService.getRecommendations();
+ const carouselRecommendations = recommendations.results.map((p) => {
+ // Usa el descuento real si hay promo, si no, no lo agregues
+ // @ts-expect-error: promo puede estar en p o en p.presentation
+ const promo: Promo | undefined = p.promo ?? p.presentation.promo;
+ const discount: Promo['discount'] | undefined = promo?.discount
+ ? Math.round(promo.discount * 100) / 100
+ : undefined;
+ return {
+ id: p.id,
+ presentationId: p.presentation.id,
+ productId: p.product.id,
imageUrl: p.product.images[0].url,
- name: p.product.genericName,
- category: p.product.categories[0].name,
+ name:
+ p.product.name +
+ ' ' +
+ p.presentation.name +
+ ' ' +
+ p.presentation.quantity +
+ ' ' +
+ p.presentation.measurementUnit,
+ category: p.product.categories[0]?.name || 'Sin categoría',
originalPrice: p.price,
- discount: 10,
- finalPrice: p.price - p.price * 0.1,
- getQuantity: () => console.log('producto'),
- });
+ ...(discount !== undefined ? { discount } : {}),
+ finalPrice: p.price,
+ quantity: getItemQuantity(p.id),
+ getQuantity: (quantity: number) => {
+ updateCartQuantity(p.id, quantity);
+ },
+ };
});
- setProducts(carouselProducts);
- } else console.log(productsData.error);
+ setRecommendedProducts(carouselRecommendations);
+ } catch (error) {
+ console.error('Error fetching recommended products:', error);
+ } finally {
+ setLoadingRecommendations(false);
+ }
};
- useEffect(() => {
- obtainProducts();
- }, []);
+ const onRefresh = async () => {
+ setRefreshing(true);
+ await Promise.all([
+ obtainProducts(),
+ obtainRecommendedProducts(),
+ refreshNotifications(), // <-- refresca notificaciones también
+ ]);
+ setRefreshing(false);
+ };
return (
-
+
+ }
+ >
Ofertas especiales
-
-
+
+ {loading ? (
+
+
+ Cargando ofertas...
+
+
+
+ ) : (
+
+ )}
Medicamentos
-
-
+
+ {loading ? (
+
+
+ Cargando medicamentos...
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ Recomendados para ti
+
+
+ {loadingRecommendations ? (
+
+
+ Cargando recomendaciones...
+
+
+
+ ) : (
+
+ )}
+
+ setShowEmailVerification(false)}
+ />
);
}
@@ -62,13 +245,30 @@ export default function HomeScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
- paddingVertical: 15,
backgroundColor: Colors.bgColor,
paddingLeft: 5,
},
+ height: {
+ height: 64,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.bgColor,
+ },
+ loadingText: {
+ marginTop: 10,
+ fontSize: FontSizes.b1.size,
+ color: Colors.textLowContrast,
+ },
title: {
fontSize: FontSizes.s1.size,
color: Colors.textMain,
paddingHorizontal: 15,
},
+ rowFullWidth: {
+ flexDirection: 'row',
+ width: '100%',
+ },
});
diff --git a/src/screens/tab/OffersScreen.tsx b/src/screens/tab/OffersScreen.tsx
index 9ca0574..adf3708 100644
--- a/src/screens/tab/OffersScreen.tsx
+++ b/src/screens/tab/OffersScreen.tsx
@@ -1,21 +1,172 @@
-import React from 'react';
-import { View, StyleSheet } from 'react-native';
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ FlatList,
+ StyleSheet,
+ ActivityIndicator,
+ ListRenderItemInfo,
+} from 'react-native';
import PoppinsText from '../../components/PoppinsText';
-import { Colors } from '../../styles/theme';
+import { Colors, FontSizes } from '../../styles/theme';
+import Card from '../../components/Card';
+import { ProductService } from '../../services/products';
+import type { Promo } from '@pharmatech/sdk';
export default function OffersScreen() {
+ const [offers, setOffers] = useState<
+ Array<{
+ id: string;
+ presentationId: string;
+ productId: string;
+ imageUrl: string;
+ name: string;
+ category: string;
+ originalPrice: number;
+ discount: number;
+ finalPrice: number;
+ quantity: number;
+ getQuantity: (quantity: number) => void;
+ }>
+ >([]);
+ const [loading, setLoading] = useState(false);
+
+ function hasPromo(item: {
+ [key: string]: unknown;
+ }): item is { promo?: Promo } {
+ return 'promo' in item && typeof item.promo !== 'undefined';
+ }
+
+ const loadOffers = async () => {
+ setLoading(true);
+ const res = await ProductService.getProducts(1, 50);
+ if (res.success) {
+ const promoItems = res.data.results.filter(
+ (item) => hasPromo(item) && Boolean(item.promo?.discount),
+ );
+
+ const mappedPromoItems = promoItems.map((p) => {
+ const promo: Promo | undefined = hasPromo(p) ? p.promo : undefined;
+ const discount: Promo['discount'] = promo?.discount ?? 0;
+
+ return {
+ id: p.id,
+ presentationId: p.presentation.id,
+ productId: p.product.id,
+ imageUrl:
+ p.product.images?.[0]?.url || 'https://via.placeholder.com/150',
+ name:
+ p.product.name +
+ ' ' +
+ p.presentation.name +
+ ' ' +
+ p.presentation.quantity +
+ ' ' +
+ p.presentation.measurementUnit,
+ category: p.product.categories?.[0]?.name || 'Sin categoría',
+ originalPrice: p.price,
+ discount,
+ finalPrice: p.price,
+ quantity: 0,
+ getQuantity: () => 0,
+ };
+ });
+ setOffers(mappedPromoItems);
+ } else {
+ console.error('Error al cargar productos:', res.error);
+ }
+ setLoading(false);
+ };
+
+ useEffect(() => {
+ loadOffers();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ const renderOffer = ({
+ item,
+ }: ListRenderItemInfo<{
+ id: string;
+ presentationId: string;
+ productId: string;
+ imageUrl: string;
+ name: string;
+ category: string;
+ originalPrice: number;
+ discount: number;
+ finalPrice: number;
+ quantity: number;
+ getQuantity: (quantity: number) => void;
+ }>) => {
+ return (
+
+ );
+ };
+
return (
- Pantalla Ofertas
+
+ Ofertas especiales
+
+ item.id}
+ numColumns={2}
+ contentContainerStyle={styles.list}
+ renderItem={renderOffer}
+ columnWrapperStyle={styles.columnWrapper}
+ onRefresh={loadOffers}
+ refreshing={loading}
+ />
+
);
}
const styles = StyleSheet.create({
container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ paddingHorizontal: 5,
+ },
+ title: {
+ fontSize: FontSizes.s1.size,
+ color: Colors.textMain,
+ paddingHorizontal: 15,
+ },
+ list: {
+ paddingBottom: 20,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ columnWrapper: {
+ justifyContent: 'space-between',
+ },
+ loaderContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: Colors.bgColor,
},
+ height: {
+ height: 64,
+ },
});
diff --git a/src/screens/tab/SupportScreen.tsx b/src/screens/tab/SupportScreen.tsx
deleted file mode 100644
index ee0cf25..0000000
--- a/src/screens/tab/SupportScreen.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import { View, StyleSheet } from 'react-native';
-import PoppinsText from '../../components/PoppinsText';
-import { Colors } from '../../styles/theme';
-
-export default function SupportScreen() {
- return (
-
- Pantalla Ofertas
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: Colors.bgColor,
- },
-});
diff --git a/src/services/auth.ts b/src/services/auth.ts
index 9db1b13..074e1d6 100644
--- a/src/services/auth.ts
+++ b/src/services/auth.ts
@@ -3,9 +3,16 @@ import * as SecureStore from 'expo-secure-store';
import { ServiceResponse, UserGender, SignUpResponse } from '../types/api.d';
import { validateEmail } from '../utils/validators';
import { extractErrorMessage } from '../utils/errorHandler';
+import { decodeJWT } from '../helper/jwtHelper';
+import { store } from '../redux/store';
+import { setUserId } from '../redux/slices/cartSlice';
+import { UserService } from './user';
export const AuthService = {
- login: async (email: string, password: string): Promise => {
+ login: async (
+ email: string,
+ password: string,
+ ): Promise> => {
try {
if (!validateEmail(email)) {
return { success: false, error: 'Correo electrónico inválido' };
@@ -17,8 +24,37 @@ export const AuthService = {
password: password.trim(),
});
+ // Guardar el token en SecureStore
await SecureStore.setItemAsync('auth_token', accessToken);
- return { success: true, data: undefined };
+
+ await SecureStore.deleteItemAsync('user_data');
+
+ const decoded = decodeJWT(accessToken);
+ store.dispatch(setUserId(decoded?.userId || null));
+
+ // Obtener el perfil del usuario para verificar el estado de isValidated
+ const profileResponse = await UserService.getProfile();
+ if (!profileResponse.success) {
+ return {
+ success: false,
+ error: 'Error al obtener el perfil del usuario',
+ };
+ }
+
+ const isValidated = profileResponse.data?.isValidated || false;
+
+ // Si el usuario no está validado, enviar automáticamente el OTP
+ if (!isValidated) {
+ const resendResponse = await AuthService.resendOtp();
+ if (!resendResponse.success) {
+ return {
+ success: false,
+ error: 'Error al enviar el OTP. Inténtalo nuevamente.',
+ };
+ }
+ }
+
+ return { success: true, data: { isValidated } };
} catch (error) {
return {
success: false,
@@ -67,6 +103,16 @@ export const AuthService = {
gender: mappedGender,
});
+ // Saving user data in SecureStore
+ await SecureStore.setItemAsync(
+ 'user_data',
+ JSON.stringify({
+ firstName: response.firstName,
+ lastName: response.lastName,
+ email: response.email,
+ }),
+ );
+
return { success: true, data: response };
} catch (error) {
return {
@@ -104,10 +150,15 @@ export const AuthService = {
return { success: true, data: accessToken };
} catch (error) {
- return {
- success: false,
- error: extractErrorMessage(error),
- };
+ // Manejar errores específicos del backend
+ const errorMessage = extractErrorMessage(error);
+ if (errorMessage.includes('invalid')) {
+ return {
+ success: false,
+ error: 'El código ingresado es incorrecto o ha expirado.',
+ };
+ }
+ return { success: false, error: errorMessage };
}
},
@@ -120,15 +171,151 @@ export const AuthService = {
return { success: false, error: 'Las contraseñas no coinciden' };
}
- const token = (await SecureStore.getItemAsync('reset_token')) || '';
- await api.auth.updatePassword(newPassword.trim(), token);
+ const token = (await SecureStore.getItemAsync('reset_token')) || ''; // Obtener el reset_token
+ if (!token) {
+ return { success: false, error: 'Token de recuperación no encontrado' };
+ }
+
+ // Realizar la solicitud sobrescribiendo el header Authorization
+ await api.client['client'].patch(
+ '/auth/password',
+ {
+ password: newPassword.trim(),
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`, // Sobrescribir el header con el reset_token
+ },
+ },
+ );
+
+ // Logout silencioso para limpiar la sesión
+ await AuthService.logout();
+
+ return { success: true, data: undefined };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ changePassword: async (
+ currentPassword: string,
+ newPassword: string,
+ ): Promise => {
+ try {
+ // Obtener el token de autenticación almacenado
+ const token = (await SecureStore.getItemAsync('auth_token')) || '';
+ if (!token) {
+ return {
+ success: false,
+ error: 'Token de autenticación no encontrado',
+ };
+ }
+
+ // Llamar al endpoint updateCurrentPassword del SDK
+ await api.auth.updateCurrentPassword(
+ currentPassword.trim(),
+ newPassword.trim(),
+ token,
+ );
return { success: true, data: undefined };
} catch (error) {
+ // Manejar errores específicos del backend
return {
success: false,
error: extractErrorMessage(error),
};
}
},
+
+ logout: async (): Promise => {
+ try {
+ // Eliminar el token del SecureStore
+ await SecureStore.deleteItemAsync('auth_token');
+ await SecureStore.deleteItemAsync('reset_token');
+
+ // Eliminar los interceptores configurados en el cliente HTTP
+ const interceptors = api.client['client'].interceptors.request;
+ interceptors.handlers = []; // Limpia todos los interceptores configurados
+
+ store.dispatch(setUserId(null)); // Limpia el carrito al cerrar sesión
+ } catch (error) {
+ console.error('Error al cerrar sesión:', error);
+ }
+ },
+
+ validateOtp: async (otp: string): Promise => {
+ try {
+ if (!/^\d{6}$/.test(otp)) {
+ return { success: false, error: 'El código debe tener 6 dígitos' };
+ }
+
+ const token = (await SecureStore.getItemAsync('auth_token')) || '';
+
+ if (!token) {
+ return {
+ success: false,
+ error: 'Token de autenticación no encontrado',
+ };
+ }
+
+ await api.auth.validateOtp(otp.trim(), token);
+
+ return { success: true, data: undefined };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ resendOtp: async (): Promise => {
+ try {
+ const token = (await SecureStore.getItemAsync('auth_token')) || '';
+
+ if (!token) {
+ return {
+ success: false,
+ error: 'Token de autenticación no encontrado',
+ };
+ }
+
+ await api.auth.resendOtp(token);
+
+ return { success: true, data: undefined };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ validateSession: async (): Promise<{
+ isValid: boolean;
+ userRole?: string;
+ }> => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) return { isValid: false };
+
+ const decoded = decodeJWT(token);
+ if (!decoded || !decoded.userId) return { isValid: false };
+
+ const profileResponse = await UserService.getProfile();
+ if (profileResponse.success) {
+ return { isValid: true, userRole: profileResponse.data.role };
+ }
+
+ return { isValid: false };
+ } catch (error) {
+ console.error('Error al validar la sesión:', error);
+ return { isValid: false };
+ }
+ },
};
diff --git a/src/services/branches.ts b/src/services/branches.ts
new file mode 100644
index 0000000..4200ea9
--- /dev/null
+++ b/src/services/branches.ts
@@ -0,0 +1,43 @@
+import { api } from '../lib/sdkConfig';
+import { Pagination, BranchResponse } from '@pharmatech/sdk';
+import { extractErrorMessage } from '../utils/errorHandler';
+import * as SecureStore from 'expo-secure-store';
+
+type FindAllParams = {
+ page?: number;
+ limit?: number;
+ q?: string;
+ stateId?: string;
+};
+
+export const BranchService = {
+ findAll: async ({
+ page = 1,
+ limit = 100,
+ q,
+ stateId,
+ }: FindAllParams): Promise> => {
+ try {
+ const response = await api.branch.findAll({ page, limit, q, stateId });
+ return response;
+ } catch (error) {
+ console.error('Error en BranchService.findAll:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+
+ getById: async (id: string): Promise => {
+ if (!id || typeof id !== 'string') {
+ throw new Error('El ID proporcionado no es válido.');
+ }
+
+ try {
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ const response = await api.branch.getById(id, jwt || '');
+ return response;
+ } catch (error) {
+ console.error('Error en BranchService.getById:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+};
diff --git a/src/services/category.ts b/src/services/category.ts
new file mode 100644
index 0000000..5a96d78
--- /dev/null
+++ b/src/services/category.ts
@@ -0,0 +1,39 @@
+import { Pagination, CategoryResponse } from '@pharmatech/sdk';
+import { api } from '../lib/sdkConfig';
+import { ServiceResponse } from '../types/api.d';
+import { extractErrorMessage } from '../utils/errorHandler';
+
+export const CategoryService = {
+ getCategories: async (
+ page: number,
+ limit: number,
+ ): Promise>> => {
+ try {
+ const categories = await api.category.findAll({
+ page: page,
+ limit: limit,
+ });
+
+ return { success: true, data: categories };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getById: async (id: string): Promise => {
+ if (!id || typeof id !== 'string') {
+ throw new Error('El ID proporcionado no es válido.');
+ }
+
+ try {
+ const response = await api.category.getById(id);
+ return response;
+ } catch (error) {
+ console.error('Error en CategoryService.getById:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+};
diff --git a/src/services/coupon.ts b/src/services/coupon.ts
new file mode 100644
index 0000000..fdd0843
--- /dev/null
+++ b/src/services/coupon.ts
@@ -0,0 +1,21 @@
+import * as SecureStore from 'expo-secure-store';
+import { api } from '../lib/sdkConfig';
+import { CouponResponse } from '@pharmatech/sdk';
+import { extractErrorMessage } from '../utils/errorHandler';
+
+export const CouponService = {
+ validateCoupon: async (code: string): Promise => {
+ try {
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ throw new Error('No se encontró el token de autenticación.');
+ }
+
+ const response = await api.coupon.getByCode(code, jwt);
+ return response;
+ } catch (error) {
+ console.error('Error en CouponService.validateCoupon:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+};
diff --git a/src/services/delivery.ts b/src/services/delivery.ts
new file mode 100644
index 0000000..7e90940
--- /dev/null
+++ b/src/services/delivery.ts
@@ -0,0 +1,144 @@
+import { api } from '../lib/sdkConfig';
+import * as SecureStore from 'expo-secure-store';
+import {
+ OrderDeliveryStatus,
+ OrderDeliveryResponse,
+ OrderDeliveryDetailedResponse,
+ Pagination,
+ OrderDeliveryPaginationRequest,
+ OrderDetailResponse,
+} from '@pharmatech/sdk';
+import { extractErrorMessage } from '../utils/errorHandler';
+
+export const DeliveryService = {
+ /**
+ * Listar órdenes asignadas al delivery.
+ * @param employeeId - ID del motorizado (delivery).
+ * @param statuses - Lista de estados de las órdenes a filtrar (por defecto: ['ASSIGNED', 'WAITING_CONFIRMATION', 'PICKED_UP', 'IN_ROUTE']).
+ * @returns Lista paginada de órdenes asignadas.
+ */
+ getAssignedOrders: async (
+ employeeId: string,
+ statuses: OrderDeliveryStatus[] = [
+ OrderDeliveryStatus.ASSIGNED,
+ OrderDeliveryStatus.WAITING_CONFIRMATION,
+ OrderDeliveryStatus.PICKED_UP,
+ OrderDeliveryStatus.IN_ROUTE,
+ ],
+ ): Promise> => {
+ try {
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ // Realizar múltiples llamadas al backend, una por cada estado
+ const responses = await Promise.all(
+ statuses.map((status) =>
+ api.deliveryService.findAll(
+ {
+ employeeId,
+ status,
+ page: 1,
+ limit: 10,
+ } as OrderDeliveryPaginationRequest,
+ jwt,
+ ),
+ ),
+ );
+
+ // Combinar los resultados de todas las llamadas
+ const combinedResults = responses.reduce(
+ (acc, response) => {
+ acc.results = acc.results.concat(response.results);
+ acc.count += response.count;
+ return acc;
+ },
+ {
+ results: [],
+ count: 0,
+ next: null,
+ previous: null,
+ } as Pagination,
+ );
+
+ return combinedResults;
+ } catch (error) {
+ console.error('Error al obtener las órdenes asignadas:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+
+ /**
+ * Obtener detalles de una orden específica.
+ * @param orderId - ID de la orden de delivery.
+ * @returns Detalles completos de la orden.
+ */
+ getOrderDetails: async (
+ orderId: string,
+ ): Promise => {
+ try {
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ const response = await api.deliveryService.getById(orderId, jwt);
+ return response;
+ } catch (error) {
+ console.error('Error al obtener los detalles de la orden:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+
+ /**
+ * Obtener los productos de una orden específica.
+ * @param orderId - ID de la orden.
+ * @returns Lista de productos de la orden.
+ */
+ getOrderProducts: async (orderId: string): Promise => {
+ try {
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ console.log('Fetching products for order ID:', orderId);
+ const orderDetails = await api.order.getById(orderId, jwt);
+ console.log('Order details fetched:', orderDetails);
+
+ return orderDetails.details; // Retornar solo los productos
+ } catch (error) {
+ console.error('Error al obtener los productos de la orden:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+
+ /**
+ * Actualizar el estado de una orden de delivery.
+ * @param orderId - ID de la orden de delivery.
+ * @param status - Nuevo estado de la orden.
+ * @returns Respuesta actualizada de la orden.
+ */
+ updateOrderStatus: async (
+ orderId: string,
+ status: OrderDeliveryStatus,
+ ): Promise => {
+ try {
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ const response = await api.deliveryService.update(
+ orderId,
+ { deliveryStatus: status },
+ jwt,
+ );
+ return response;
+ } catch (error) {
+ console.error('Error al actualizar el estado de la orden:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+};
diff --git a/src/services/images.ts b/src/services/images.ts
new file mode 100644
index 0000000..9774ed7
--- /dev/null
+++ b/src/services/images.ts
@@ -0,0 +1,62 @@
+import axios from 'axios';
+
+interface FileData {
+ uri: string;
+ type: string;
+ name: string;
+}
+
+export const ImageService = {
+ uploadImage: async (
+ imageUri: string,
+ ): Promise<{ secure_url: string } | undefined> => {
+ try {
+ console.log('Iniciando proceso de subida de imagen...');
+ console.log('URI de la imagen:', imageUri);
+
+ // Paso 1: Construir el FormData para la subida
+ console.log('Construyendo FormData para la subida...');
+ const formData = new FormData();
+ const file: FileData = {
+ uri: imageUri,
+ type: 'image/jpeg', // Cambia el tipo si es necesario
+ name: 'upload.jpg',
+ };
+
+ formData.append('file', file as unknown as Blob); // Cloudinary espera un Blob o File
+ formData.append('upload_preset', 'unsigned_preset'); // Cambia esto por el nombre de tu preset unsigned
+ formData.append('api_key', process.env.CLOUDINARY_API_KEY || '');
+ formData.append('timestamp', Math.floor(Date.now() / 1000).toString());
+
+ console.log('FormData construido:', formData);
+
+ // Paso 2: Subir la imagen a Cloudinary
+ console.log('Subiendo imagen a Cloudinary...');
+ const response = await axios.post(
+ `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload`,
+ formData,
+ {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ },
+ );
+
+ console.log('Respuesta de Cloudinary:', response.data);
+
+ return response.data; // Devuelve el objeto completo con secure_url
+ } catch (error: unknown) {
+ if (axios.isAxiosError(error)) {
+ console.error('Error al subir la imagen a Cloudinary:', error.message);
+ if (error.response) {
+ console.error('Detalles del error (response):', error.response.data);
+ } else if (error.request) {
+ console.error('Detalles del error (request):', error.request);
+ }
+ } else {
+ console.error('Error desconocido:', error);
+ }
+ return undefined;
+ }
+ },
+};
diff --git a/src/services/inventory.ts b/src/services/inventory.ts
new file mode 100644
index 0000000..b3733b8
--- /dev/null
+++ b/src/services/inventory.ts
@@ -0,0 +1,47 @@
+import { api } from '../lib/sdkConfig';
+import { ServiceResponse, Inventory } from '../types/api.d';
+import { Pagination, InventoryResponse } from '@pharmatech/sdk';
+import { extractErrorMessage } from '../utils/errorHandler';
+
+type GetBranchInventoryParams = {
+ page?: number;
+ limit?: number;
+ branchId: string;
+};
+
+export const InventoryService = {
+ getPresentationInventory: async (
+ page: number,
+ limit: number,
+ presentationId: string,
+ ): Promise>> => {
+ try {
+ const inventory = await api.inventory.findAll({
+ page: page,
+ limit: limit,
+ productPresentationId: presentationId,
+ });
+
+ return { success: true, data: inventory };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getBranchInventory: async ({
+ page = 1,
+ limit = 10,
+ branchId,
+ }: GetBranchInventoryParams): Promise> => {
+ try {
+ const response = await api.inventory.findAll({ page, limit, branchId });
+ return response;
+ } catch (error) {
+ console.error('Error en InventoryService.getBranchInventory:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+};
diff --git a/src/services/notifications.ts b/src/services/notifications.ts
new file mode 100644
index 0000000..a85d05b
--- /dev/null
+++ b/src/services/notifications.ts
@@ -0,0 +1,39 @@
+import * as SecureStore from 'expo-secure-store';
+import { api } from '../lib/sdkConfig';
+import { NotificationResponse } from '@pharmatech/sdk';
+import { ServiceResponse } from '../types/api';
+import { extractErrorMessage } from '../utils/errorHandler';
+
+export const NotificationService = {
+ getNotifications: async (): Promise<
+ ServiceResponse
+ > => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ const notifications = await api.notification.getNotifications(token);
+
+ return { success: true, data: notifications };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+ markAsRead: async (orderId: string): Promise => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ await api.notification.markAsRead(orderId, token);
+ } catch (error) {
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+};
diff --git a/src/services/order.ts b/src/services/order.ts
new file mode 100644
index 0000000..da3c1ae
--- /dev/null
+++ b/src/services/order.ts
@@ -0,0 +1,43 @@
+import { api } from '../lib/sdkConfig';
+import * as SecureStore from 'expo-secure-store';
+import { CreateOrder, OrderResponse } from '@pharmatech/sdk';
+import { extractErrorMessage } from '../utils/errorHandler';
+
+export const OrderService = {
+ create: async (order: CreateOrder): Promise => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ console.log('Enviando solicitud de creación de orden:', order);
+
+ const response = await api.order.create(order, token);
+
+ if (!response?.id) {
+ throw new Error('La respuesta no contiene un ID de orden válido');
+ }
+
+ console.log('Respuesta de creación de orden:', response);
+ return response;
+ } catch (error) {
+ console.error('Error en OrderService.create:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+
+ getById: async (orderId: string) => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+ const orderDetail = await api.order.getById(orderId, token);
+ return orderDetail;
+ } catch (error) {
+ console.error('Error en OrderService.getById:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+};
diff --git a/src/services/paymentConfirmation.ts b/src/services/paymentConfirmation.ts
new file mode 100644
index 0000000..de25636
--- /dev/null
+++ b/src/services/paymentConfirmation.ts
@@ -0,0 +1,53 @@
+import { api } from '../lib/sdkConfig';
+import * as SecureStore from 'expo-secure-store';
+import {
+ PaymentConfirmation,
+ PaymentConfirmationResponse,
+} from '../types/api.d';
+import { extractErrorMessage } from '../utils/errorHandler';
+
+export const PaymentConfirmationService = {
+ create: async (
+ paymentConfirmation: PaymentConfirmation,
+ ): Promise => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ console.log('Enviando confirmación de pago:', paymentConfirmation);
+
+ // Validar el payload
+ const requiredFields: (keyof PaymentConfirmation)[] = [
+ 'documentId',
+ 'phoneNumber',
+ 'bank',
+ 'reference',
+ 'orderId',
+ ];
+ for (const field of requiredFields) {
+ if (!paymentConfirmation[field]) {
+ throw new Error(`El campo ${field} es obligatorio`);
+ }
+ }
+
+ const response = await api.paymentConfirmation.create(
+ paymentConfirmation,
+ token,
+ );
+
+ if (!response?.id) {
+ throw new Error(
+ 'La respuesta no contiene un ID de confirmación válido',
+ );
+ }
+
+ console.log('Respuesta de confirmación de pago:', response);
+ return response;
+ } catch (error) {
+ console.error('Error en PaymentConfirmationService.create:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+};
diff --git a/src/services/paymentInformation.ts b/src/services/paymentInformation.ts
new file mode 100644
index 0000000..6016004
--- /dev/null
+++ b/src/services/paymentInformation.ts
@@ -0,0 +1,26 @@
+import { api } from '../lib/sdkConfig';
+import * as SecureStore from 'expo-secure-store';
+import { PaymentInfoResponse, PaymentMethod } from '@pharmatech/sdk';
+import { extractErrorMessage } from '../utils/errorHandler';
+
+export const PaymentInformationService = {
+ findAll: async (
+ paymentMethod?: PaymentMethod,
+ ): Promise => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ // Call the API method
+ const response = await api.paymentInformation.findAll(paymentMethod);
+
+ // Devolver la respuesta directamente (se asume que ya es un array)
+ return response;
+ } catch (error) {
+ console.error('Error en PaymentInformationService.findAll:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
+};
diff --git a/src/services/products.ts b/src/services/products.ts
index 2f7bf36..6cb98e8 100644
--- a/src/services/products.ts
+++ b/src/services/products.ts
@@ -1,20 +1,30 @@
import { api } from '../lib/sdkConfig';
+import * as SecureStore from 'expo-secure-store';
import {
- ServiceResponse,
Pagination,
ProductPresentation,
-} from '../types/api.d';
+ GenericProductResponse,
+ ProductPresentationResponse,
+ ProductImage,
+ ManufacturerResponse,
+ ProductPaginationRequest,
+ ProductPresentationDetailResponse,
+ PresentationResponse,
+} from '@pharmatech/sdk';
+import { ServiceResponse } from '../types/api';
import { extractErrorMessage } from '../utils/errorHandler';
export const ProductService = {
getProducts: async (
page: number,
limit: number,
+ params?: ProductPaginationRequest,
): Promise>> => {
try {
const products = await api.product.getProducts({
page: page,
limit: limit,
+ ...params,
});
return { success: true, data: products };
@@ -25,4 +35,122 @@ export const ProductService = {
};
}
},
+
+ getPresentation: async (
+ productId: string,
+ presentationId: string,
+ ): Promise> => {
+ try {
+ const presentation = await api.productPresentation.getByPresentationId(
+ productId,
+ presentationId,
+ );
+
+ return { success: true, data: presentation };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+ getPresentations: async (
+ page: number,
+ limit: number,
+ ): Promise>> => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+ const presentations = await api.presentation.findAll({ page, limit });
+
+ return { success: true, data: presentations };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+ getGenericProduct: async (
+ id: string,
+ ): Promise> => {
+ try {
+ const product = await api.genericProduct.getById(id);
+ console.log(product);
+ return { success: true, data: product };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getProductPresentations: async (
+ id: string,
+ ): Promise> => {
+ try {
+ const presentation = await api.productPresentation.getByProductId(id);
+
+ return { success: true, data: presentation };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getProductImages: async (
+ id: string,
+ ): Promise> => {
+ try {
+ const images = await api.productImage.getByProductId(id);
+
+ return { success: true, data: images };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getBrands: async (
+ page: number,
+ limit: number,
+ ): Promise>> => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ const response = await api.manufacturer.findAll({ page, limit });
+
+ return { success: true, data: response };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getRecommendations: async (): Promise> => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ const recommendations = await api.product.getRecommendations(token);
+ return recommendations;
+ } catch (error) {
+ console.error('Error en ProductService.getRecommendations:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
};
diff --git a/src/services/state.ts b/src/services/state.ts
new file mode 100644
index 0000000..7d26dc7
--- /dev/null
+++ b/src/services/state.ts
@@ -0,0 +1,81 @@
+import { api } from '../lib/sdkConfig';
+import { Pagination, StateResponse, CityResponse } from '@pharmatech/sdk';
+import { extractErrorMessage } from '../utils/errorHandler';
+
+export const StateService = {
+ getStates: async (
+ page: number,
+ limit: number,
+ ): Promise<{
+ success: boolean;
+ data?: Pagination;
+ error?: string;
+ }> => {
+ try {
+ const states = await api.state.findAll({
+ page: page,
+ limit: limit,
+ });
+
+ return { success: true, data: states };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getState: async (
+ id: string,
+ ): Promise<{ success: boolean; data?: StateResponse; error?: string }> => {
+ try {
+ const state = await api.state.getById(id);
+ return { success: true, data: state };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getCities: async (
+ page: number,
+ limit: number,
+ stateId: string,
+ ): Promise<{
+ success: boolean;
+ data?: Pagination;
+ error?: string;
+ }> => {
+ try {
+ const cities = await api.city.findAll({
+ page: page,
+ limit: limit,
+ stateId: stateId,
+ });
+
+ return { success: true, data: cities };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getCity: async (
+ id: string,
+ ): Promise<{ success: boolean; data?: CityResponse; error?: string }> => {
+ try {
+ const city = await api.city.getById(id);
+ return { success: true, data: city };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+};
diff --git a/src/services/user.ts b/src/services/user.ts
new file mode 100644
index 0000000..b719bf9
--- /dev/null
+++ b/src/services/user.ts
@@ -0,0 +1,273 @@
+import * as SecureStore from 'expo-secure-store';
+import { api } from '../lib/sdkConfig';
+import { ServiceResponse, UpdateUser } from '../types/api.d';
+import { extractErrorMessage } from '../utils/errorHandler';
+import { decodeJWT } from '../helper/jwtHelper';
+import {
+ UserList,
+ OrderResponse,
+ Pagination,
+ UserAddressResponse,
+ CreateUserAddressRequest,
+ OrderDetailedResponse,
+} from '@pharmatech/sdk';
+
+export const UserService = {
+ getProfile: async (): Promise> => {
+ try {
+ // Obtén el token JWT desde SecureStore
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ // Decodifica el JWT para obtener el userId
+ const decoded = decodeJWT(token);
+ if (!decoded || !decoded.userId) {
+ throw new Error('No se pudo decodificar el token de autenticación');
+ }
+
+ const userId = decoded.userId;
+
+ // Llama al método `getProfile` del SDK con el userId y el token JWT
+ const response = await api.user.getProfile(userId, token);
+ console.log('Profile Data:', response); // Verificar los datos del perfil
+ return { success: true, data: response };
+ } catch (error) {
+ console.error('Error fetching profile (se lleva al login):', error);
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ updateProfile: async (
+ userData: Partial,
+ ): Promise> => {
+ try {
+ // Obtén el token JWT desde SecureStore
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ // Decodifica el JWT para obtener el userId
+ const decoded = decodeJWT(token);
+ if (!decoded || !decoded.userId) {
+ throw new Error('No se pudo decodificar el token de autenticación');
+ }
+
+ const userId = decoded.userId;
+
+ // Llama al método `getProfile` del SDK con el userId y el token JWT
+ const response = await api.user.update(userId, userData, token);
+ return { success: true, data: response };
+ } catch (error) {
+ console.error('Error updating profile:', error);
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getUserDirections: async (): Promise<
+ ServiceResponse
+ > => {
+ try {
+ // Obtén el token JWT desde SecureStore
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ // Decodifica el JWT para obtener el userId
+ const decoded = decodeJWT(token);
+ if (!decoded || !decoded.userId) {
+ throw new Error('No se pudo decodificar el token de autenticación');
+ }
+
+ const userId = decoded.userId;
+ const response = await api.userAdress.getListAddresses(userId, token);
+ return { success: true, data: response };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getDirection: async (
+ id: string,
+ ): Promise> => {
+ try {
+ // Obtén el token JWT desde SecureStore
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ // Decodifica el JWT para obtener el userId
+ const decoded = decodeJWT(token);
+ if (!decoded || !decoded.userId) {
+ throw new Error('No se pudo decodificar el token de autenticación');
+ }
+
+ const userId = decoded.userId;
+ const response = await api.userAdress.getAddress(userId, id, token);
+ return { success: true, data: response };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ saveDirection: async (
+ userAddress: CreateUserAddressRequest,
+ ): Promise> => {
+ try {
+ // Obtén el token JWT desde SecureStore
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ // Decodifica el JWT para obtener el userId
+ const decoded = decodeJWT(token);
+ if (!decoded || !decoded.userId) {
+ throw new Error('No se pudo decodificar el token de autenticación');
+ }
+
+ const userId = decoded.userId;
+ const response = await api.userAdress.createAddress(
+ userId,
+ userAddress,
+ token,
+ );
+ return { success: true, data: response };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ updateDirection: async (
+ userAddress: CreateUserAddressRequest,
+ id: string,
+ ): Promise> => {
+ try {
+ // Obtén el token JWT desde SecureStore
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ // Decodifica el JWT para obtener el userId
+ const decoded = decodeJWT(token);
+ if (!decoded || !decoded.userId) {
+ throw new Error('No se pudo decodificar el token de autenticación');
+ }
+
+ const userId = decoded.userId;
+ const response = await api.userAdress.update(
+ userId,
+ userAddress,
+ id,
+ token,
+ );
+ return { success: true, data: response };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ deleteAddress: async (
+ id: string,
+ ): Promise<{ success: boolean; error?: string }> => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ const decoded = decodeJWT(token);
+ if (!decoded || !decoded.userId) {
+ throw new Error('No se pudo decodificar el token de autenticación');
+ }
+
+ const userId = decoded.userId;
+ await api.userAdress.deleteAddress(userId, id, token);
+ return { success: true };
+ } catch (error) {
+ console.error('Error al eliminar la dirección:', error);
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getUserOrders: async (): Promise<
+ ServiceResponse>
+ > => {
+ try {
+ // Obtén el token JWT desde SecureStore
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ // Decodifica el JWT para obtener el userId
+ const decoded = decodeJWT(token);
+ if (!decoded || !decoded.userId) {
+ throw new Error('No se pudo decodificar el token de autenticación');
+ }
+
+ const userId = decoded.userId;
+ const response = await api.order.findAll(
+ {
+ page: 1,
+ limit: 10,
+ userId: userId,
+ },
+ token, // Proporciona el token JWT como segundo argumento
+ );
+ return { success: true, data: response };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+
+ getOrder: async (
+ id: string,
+ ): Promise> => {
+ try {
+ // Obtén el token JWT desde SecureStore
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('No se encontró el token de autenticación');
+ }
+
+ // Llama al método `getById` del SDK con el id y el token JWT
+ const response = await api.order.getById(id, token); // Proporciona el token JWT como segundo argumento
+ return { success: true, data: response };
+ } catch (error) {
+ return {
+ success: false,
+ error: extractErrorMessage(error),
+ };
+ }
+ },
+};
diff --git a/src/styles/theme.ts b/src/styles/theme.ts
index 3630df4..c41121f 100644
--- a/src/styles/theme.ts
+++ b/src/styles/theme.ts
@@ -23,12 +23,29 @@ export const Colors = {
iconWhite: '#FFFFFF',
iconMainPrimary: '#1C2143',
iconMainSecondary: '#A3E4D7',
+ iconCancel: '#FF5959',
menuWhite: '#FFFFFF',
menuPrimary: '#1C2143',
toggleOn: '#1C2143',
toggleOff: '#D7D5D3',
+
+ yellow: '#FFBA2E0',
+
+ gray_100: '#E7E7E6',
+ gray_500: '#6E6D6C',
+
+ tertiary_600: '#247D74',
+ tertiary_500: '#319B8F',
+ tertiary_400: '#4AB7A8',
+
+ secondary_300: '#8BEAB3',
+
+ primary_300: '#A2BEEE',
+ primary_400: '#769CE4',
+ primary_600: '#415FCF',
+ primary_700: '#374CBE',
};
export const Fonts = {
@@ -63,8 +80,15 @@ export const FontSizes = {
btnTiny: { size: 10, lineHeight: 12 },
};
+export const ToggleSizes = {
+ small: { width: 40, height: 24, circleSize: 18, radius: 12 },
+ medium: { width: 55, height: 32, circleSize: 26, radius: 16 },
+ large: { width: 70, height: 40, circleSize: 34, radius: 20 },
+};
+
export default {
Colors,
Fonts,
FontSizes,
+ ToggleSizes,
};
diff --git a/src/types/Product.ts b/src/types/Product.ts
new file mode 100644
index 0000000..a759249
--- /dev/null
+++ b/src/types/Product.ts
@@ -0,0 +1,13 @@
+export type Product = {
+ id: string;
+ presentationId: string;
+ productId: string;
+ imageUrl: string;
+ name: string;
+ category: string;
+ originalPrice?: number;
+ discount?: number;
+ finalPrice: number;
+ quantity: number;
+ getQuantity: (quantity: number) => void;
+};
diff --git a/src/types/api.d.ts b/src/types/api.d.ts
index 3b09c30..2cf6ed0 100644
--- a/src/types/api.d.ts
+++ b/src/types/api.d.ts
@@ -1,3 +1,5 @@
+import { BaseModel } from '@pharmatech/sdk';
+
export interface ApiError extends Error {
response?: {
data?: {
@@ -52,3 +54,177 @@ export type ProductPresentation = BaseModel & {
presentation: Presentation;
product: Product;
};
+
+export interface ProfileResponse {
+ firstName: string;
+ lastName: string;
+ email: string;
+ documentId: string;
+ phoneNumber: string;
+ birthDate: Date;
+ gender: string;
+ profilePicture: string;
+ role: string;
+}
+
+type UserList = BaseModel & {
+ firstName: string;
+ lastName: string;
+ email: string;
+ documentId: string;
+ phoneNumber: string;
+ lastOrderDate: Date;
+ role: string;
+ isValidated: boolean;
+ profile: Profile;
+};
+
+type UpdateUser = {
+ firstName?: string;
+ lastName?: string;
+ phoneNumber?: string;
+ email: string;
+ profilePicture?: string;
+ birthDate: string;
+ gender?: UserGender;
+ role?: UserRole;
+};
+
+type UserAddressResponse = {
+ adress: string;
+ latitude: number | null;
+ longitude: number | null;
+ cityId: string;
+ id: string;
+ additionalInformation: string | null;
+ referencePoint: string | null;
+ nameCity: string;
+ nameState: string;
+ nameCountry: string;
+};
+
+type CreateUserAddressRequest = BaseModel & {
+ adress: string;
+ latitude?: number | null;
+ longitude?: number | null;
+ cityId?: string;
+ additionalInformation?: string | null;
+ referencePoint?: string | null;
+};
+
+type State = {
+ id: string;
+ name: string;
+};
+
+type CityResponse = {
+ id: string;
+ name: string;
+ state: StateResponse;
+};
+
+type OrderResponse = BaseModel & {
+ type: OrderType;
+ status: OrderStatus;
+ totalPrice: number;
+};
+
+type Inventory = BaseModel & {
+ stockQuantity: number;
+ branch: BranchResponse;
+ productPresentation: ProductPresentation;
+};
+
+export type Branch = {
+ name: string;
+ address: string;
+ latitude: number;
+ longitude: number;
+};
+
+export type CreateBranchRequest = Branch & {
+ cityId: string;
+};
+
+export type BranchResponse = Branch &
+ BaseModel & {
+ city: CityResponse;
+ };
+
+export type Coupon = {
+ code: string;
+ discount: number;
+ minPurchase: number;
+ maxUses: number;
+ expirationDate: Date;
+};
+
+export type CouponResponse = Coupon & BaseModel;
+
+export enum OrderType {
+ PICKUP = 'pickup',
+ DELIVERY = 'delivery',
+}
+
+export enum OrderStatus {
+ REQUESTED = 'requested',
+ APPROVED = 'approved',
+ READY = 'ready',
+ COMPLETED = 'completed',
+ CANCELED = 'canceled',
+}
+
+export interface CreateOrderDetail {
+ productPresentationId: string;
+ quantity: number;
+}
+
+export interface CreateOrder {
+ type: OrderType;
+ branchId?: string;
+ userAddressId?: string;
+ products: {
+ productPresentationId: string;
+ quantity: number;
+ }[];
+}
+
+export enum PaymentMethod {
+ CARD = 'card',
+ MOBILE_PAYMENT = 'mobile_payment',
+ BANK_TRANSFER = 'bank_transfer',
+ CASH = 'cash',
+}
+
+export interface PaymentConfirmation {
+ bank: string;
+ reference: string;
+ documentId: string;
+ phoneNumber: string;
+ orderId: string;
+}
+
+export interface PaymentConfirmationResponse extends BaseModel {
+ bank: string;
+ reference: string;
+ documentId: string;
+ phoneNumber: string;
+}
+
+export interface PaymentInformation {
+ bank: string;
+ accountType: string;
+ account: string;
+ documentId: string;
+ phoneNumber: string;
+ paymentMethod: PaymentMethod;
+}
+
+export interface PaymentInfoResponse extends BaseModel {
+ bank: string;
+ accountType: string;
+ account: string;
+ documentId: string;
+ phoneNumber: string;
+ paymentMethod: PaymentMethod;
+}
diff --git a/src/types/env.d.ts b/src/types/env.d.ts
index da78325..6c71cff 100644
--- a/src/types/env.d.ts
+++ b/src/types/env.d.ts
@@ -1,3 +1,7 @@
declare module '@env' {
export const PHARMATECH_DEV_MODE: string;
+ export const GOOGLE_MAPS_API_KEY: string;
+ export const NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: string;
+ export const CLOUDINARY_API_KEY: string;
+ export const CLOUDINARY_API_SECRET: string;
}
diff --git a/src/utils/commons.ts b/src/utils/commons.ts
index abde565..c92bf23 100644
--- a/src/utils/commons.ts
+++ b/src/utils/commons.ts
@@ -1,11 +1,22 @@
export const truncateString = (
text: string,
maxLength: number = 27,
+ optionalText: string = '...',
): string => {
if (text) {
if (text.length > maxLength) {
- return text.substring(0, maxLength) + '...';
+ return text.substring(0, maxLength) + optionalText;
}
}
return text;
};
+
+export const formatDate = (date: string): string => {
+ const options: Intl.DateTimeFormatOptions = {
+ year: 'numeric',
+ month: 'long',
+ day: '2-digit',
+ };
+ const dateObj = new Date(date);
+ return dateObj.toLocaleDateString('es-ES', options);
+};
diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts
index afc7a45..de6b2b9 100644
--- a/src/utils/errorHandler.ts
+++ b/src/utils/errorHandler.ts
@@ -1,17 +1,26 @@
-import type { ApiError } from '../types/api';
+import { isAPIError, APIError } from '@pharmatech/sdk';
-export const isApiError = (error: unknown): error is ApiError => {
- return error instanceof Error && 'response' in error;
+/**
+ * Verifica si el error es del tipo APIError proporcionado por el SDK.
+ */
+export const isApiError = (error: unknown): error is APIError => {
+ return isAPIError(error); // Usa el método del SDK directamente
};
+/**
+ * Extrae un mensaje de error legible para el usuario.
+ */
export const extractErrorMessage = (error: unknown): string => {
if (isApiError(error)) {
- return error.response?.data?.detail || error.message;
+ // Si es un APIError, toma el primer mensaje de la lista
+ return error.messages[0] || 'Ocurrió un error desconocido en el servidor.';
}
if (error instanceof Error) {
+ // Si es un error genérico de JavaScript
return error.message;
}
- return 'Error desconocido';
+ // Si no se puede identificar el error
+ return 'Ocurrió un error desconocido.';
};
diff --git a/src/utils/formatPrice.ts b/src/utils/formatPrice.ts
new file mode 100644
index 0000000..8e0c1e4
--- /dev/null
+++ b/src/utils/formatPrice.ts
@@ -0,0 +1,8 @@
+export function formatPrice(value: number, options?: Intl.NumberFormatOptions) {
+ // Divide entre 100 y muestra con dos decimales
+ return (value / 100).toLocaleString('es-VE', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ ...options,
+ });
+}
diff --git a/src/utils/validators.ts b/src/utils/validators.ts
index 86b014a..4067fc4 100644
--- a/src/utils/validators.ts
+++ b/src/utils/validators.ts
@@ -1,4 +1,34 @@
+// Validación de correo electrónico
export const validateEmail = (email: string): boolean => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
};
+
+// Validación de contraseña (mínimo 8 caracteres)
+export const validatePassword = (password: string): boolean => {
+ return password.length >= 8;
+};
+
+// Validación de coincidencia de contraseñas
+export const validatePasswordMatch = (
+ password: string,
+ confirmPassword: string,
+): boolean => {
+ return password === confirmPassword;
+};
+
+// Validación de campos vacíos
+export const validateRequiredFields = (fields: string[]): boolean => {
+ return fields.every((field) => field.trim() !== '');
+};
+
+// Validación de formato de fecha (YYYY-MM-DD)
+export const validateDateFormat = (date: string): boolean => {
+ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
+ return dateRegex.test(date);
+};
+
+// Validación de longitud del teléfono celular
+export const validatePhoneNumberLength = (phoneNumber: string): boolean => {
+ return phoneNumber.length >= 8 && phoneNumber.length <= 15;
+};
diff --git a/tsconfig.json b/tsconfig.json
index 6439257..e3905d3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,6 +12,11 @@
"strict": true,
"target": "esnext"
},
- "include": ["**/*.ts", "**/*.tsx", "src/types/**/*.d.ts"],
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ "src/types/**/*.d.ts",
+ "nativewind-env.d.ts"
+ ],
"exclude": ["node_modules"]
}