Compare commits

...
14 Commits
135 changed files with 16699 additions and 9347 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ yarn-error.log
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed. # screenshots whenever they are needed.
# For more information about the recommended setup visit: # For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/ # http://docs.fastlane.tools/best-practices/source-control/
**/fastlane/report.xml **/fastlane/report.xml
**/fastlane/Preview.html **/fastlane/Preview.html
+4
View File
@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
}
+7 -3
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { LogBox, StatusBar, View } from 'react-native'; import { LogBox, View } from 'react-native';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AlertNotificationRoot } from 'react-native-alert-notification'; import { AlertNotificationRoot } from 'react-native-alert-notification';
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
@@ -11,21 +11,24 @@ import { NavigationContainer } from '@react-navigation/native';
import colors from './src/Constant/Color'; import colors from './src/Constant/Color';
import { TextEncoder } from 'text-encoding'; import { TextEncoder } from 'text-encoding';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import AppStatusBar from './src/components/AppStatusBar';
LogBox.ignoreAllLogs(true); LogBox.ignoreAllLogs(true);
import { navigationRef } from './src/Navigation/NavigationService';
const App = () => { const App = () => {
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
global.TextEncoder = TextEncoder; global.TextEncoder = TextEncoder;
return ( return (
<NavigationContainer > <NavigationContainer ref={navigationRef}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<AlertNotificationRoot> <AlertNotificationRoot>
<Provider store={store}> <Provider store={store}>
<PersistGate persistor={persistor}> <PersistGate persistor={persistor}>
<View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}> <View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}>
<StatusBar barStyle="dark-content" /> <AppStatusBar />
<Routes /> <Routes />
</View> </View>
</PersistGate> </PersistGate>
@@ -38,3 +41,4 @@ const App = () => {
}; };
export default App; export default App;
+2 -2
View File
@@ -1,6 +1,6 @@
source 'https://rubygems.org' source 'http://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version # You may use http://rbenv.org/ or http://rvm.io/ to install and use this version
ruby ">= 2.6.10" ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures. # Exclude problematic versions of cocoapods and activesupport that causes build failures.
+12 -12
View File
@@ -1,8 +1,8 @@
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). This is a new [**React Native**](http://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](http://github.com/react-native-community/cli).
# Getting Started # Getting Started
> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. > **Note**: Make sure you have completed the [Set Up Your Environment](http://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
## Step 1: Start Metro ## Step 1: Start Metro
@@ -48,7 +48,7 @@ Then, and every time you update your native dependencies, run:
bundle exec pod install bundle exec pod install
``` ```
For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). For more information, please visit [CocoaPods Getting Started guide](http://guides.cocoapods.org/using/getting-started.html).
```sh ```sh
# Using npm # Using npm
@@ -66,7 +66,7 @@ This is one way to run your app — you can also build it directly from Android
Now that you have successfully run the app, let's make changes! Now that you have successfully run the app, let's make changes!
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](http://reactnative.dev/docs/fast-refresh).
When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
@@ -79,19 +79,19 @@ You've successfully run and modified your React Native App. :partying_face:
### Now what? ### Now what?
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). - If you want to add this new React Native code to an existing application, check out the [Integration guide](http://reactnative.dev/docs/integration-with-existing-apps).
- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). - If you're curious to learn more about React Native, check out the [docs](http://reactnative.dev/docs/getting-started).
# Troubleshooting # Troubleshooting
If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. If you're having issues getting the above steps to work, see the [Troubleshooting](http://reactnative.dev/docs/troubleshooting) page.
# Learn More # Learn More
To learn more about React Native, take a look at the following resources: To learn more about React Native, take a look at the following resources:
- [React Native Website](https://reactnative.dev) - learn more about React Native. - [React Native Website](http://reactnative.dev) - learn more about React Native.
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. - [Getting Started](http://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. - [Learn the Basics](http://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. - [Blog](http://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. - [`@facebook/react-native`](http://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
@@ -1,4 +1,4 @@
arguments=--init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\init\\init.gradle --init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\protobuf\\init.gradle arguments=--init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.54.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\init\\init.gradle --init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.54.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\protobuf\\init.gradle --init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.54.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\kotlin\\init.gradle --init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.54.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\groovy\\init.gradle --init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.54.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\scala\\init.gradle
auto.sync=false auto.sync=false
build.scans.enabled=false build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
Binary file not shown.
+2 -2
View File
@@ -40,7 +40,7 @@ react {
// entryFile = file("../js/MyApplication.android.js") // entryFile = file("../js/MyApplication.android.js")
// //
// A list of extra flags to pass to the 'bundle' commands. // A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle // See http://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = [] // extraPackagerArgs = []
/* Hermes Commands */ /* Hermes Commands */
@@ -99,7 +99,7 @@ android {
} }
release { release {
// Caution! In production, you need to generate your own keystore file. // Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android. // see http://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.procure",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 24
}
+2
View File
@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@@ -8,14 +8,12 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() { class MainActivity : ReactActivity() {
/** /**
* Returns the name of the main component registered from JavaScript. This is used to schedule * Returns the name of the main component registered from JavaScript.
* rendering of the component.
*/ */
override fun getMainComponentName(): String = "procure" override fun getMainComponentName(): String = "procure"
/** /**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] * Returns the instance of the [ReactActivityDelegate].
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/ */
override fun createReactActivityDelegate(): ReactActivityDelegate = override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
@@ -22,13 +22,13 @@
<selector> <selector>
<!-- <!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I). This file is a copy of abc_edit_text_material (http://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException. The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)' NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/> <item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR. For more info, see http://bit.ly/3CdLStv (react-native/pull/29452) and http://bit.ly/3nxOMoR.
--> -->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/> <item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/> <item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
+3 -3
View File
@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
@@ -19,13 +19,13 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# AndroidX package structure to make it clearer which packages are bundled with the # AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # http://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Use this property to specify which architecture you want to build. # Use this property to specify which architecture you want to build.
# You can also override it from the CLI using # You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64 # ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 reactNativeArchitectures=armeabi-v7a,arm64-v8a
# Use this property to enable support to the new architecture. # Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in # This will allow you to use TurboModules and the Fabric render in
+4 -4
View File
@@ -7,7 +7,7 @@
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# https://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
@@ -57,10 +57,10 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # http://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at http://github.com/gradle/gradle/.
# #
############################################################################## ##############################################################################
@@ -85,7 +85,7 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (http://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
+1 -1
View File
@@ -5,7 +5,7 @@
@rem you may not use this file except in compliance with the License. @rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at @rem You may obtain a copy of the License at
@rem @rem
@rem https://www.apache.org/licenses/LICENSE-2.0 @rem http://www.apache.org/licenses/LICENSE-2.0
@rem @rem
@rem Unless required by applicable law or agreed to in writing, software @rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS, @rem distributed under the License is distributed on an "AS IS" BASIS,
+204
View File
@@ -0,0 +1,204 @@
Diagnose log at 2026-05-08T08:22:50Z
AppPath: C:\Users\Lincpay\AppData\Local\Programs\Git\mingw64\bin\git-credential-manager.exe
InstallDir: C:\Users\Lincpay\AppData\Local\Programs\Git\mingw64\bin\
Version: 2.5.0+d34930736e131ad80e5690e5634ced1808aff3e2
------------
Diagnostic: Environment
Skipped: False
Success: True
Exception: None
Log:
OSType: Windows
OSVersion: 10.0 (build 26200)
Reading environment variables... OK
Variables:
HOMEPATH=\Users\Lincpay
DriverData=C:\Windows\System32\Drivers\DriverData
COMPUTERNAME=DESKTOP-TAFQRBL
GIT_TRACE2_PARENT_SID=3fbdeb66-dea1-404d-b65e-01414608f6f1
CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files
OneDrive=C:\Users\Lincpay\OneDrive
CODEX_THREAD_ID=019e0662-b081-7d10-a202-980cf9131ff1
SESSIONNAME=Console
TMP=C:\Users\Lincpay\AppData\Local\Temp
PROCESSOR_REVISION=8e0c
PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW;.CPL
USERNAME=Lincpay
GIT_EXEC_PATH=C:/Users/Lincpay/AppData/Local/Programs/Git/mingw64/libexec/git-core
TEMP=C:\Users\Lincpay\AppData\Local\Temp
LOCALAPPDATA=C:\Users\Lincpay\AppData\Local
MSYSTEM=MINGW64
TERM=xterm-256color
PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 142 Stepping 12, GenuineIntel
ProgramData=C:\ProgramData
windir=C:\WINDOWS
JAVA_HOME=C:\Program Files\Microsoft\jdk-17.0.14.7-hotspot\
USERDOMAIN=DESKTOP-TAFQRBL
PROCESSOR_LEVEL=6
NUMBER_OF_PROCESSORS=8
TMPDIR=C:\Users\Lincpay\AppData\Local\Temp
VSCODE_IPC_HOOK=\\.\pipe\547cb42d-1.107.0-main-sock
ProgramFiles(x86)=C:\Program Files (x86)
PUBLIC=C:\Users\Public
VSCODE_NLS_CONFIG={"userLocale":"en-us","osLocale":"en-in","resolvedLanguage":"en","defaultMessagesFile":"C:\\Users\\Lincpay\\AppData\\Local\\Programs\\Antigravity\\resources\\app\\out\\nls.messages.json","locale":"en-us","availableLanguages":{}}
CommonProgramFiles=C:\Program Files (x86)\Common Files
VSCODE_ESM_ENTRYPOINT=vs/workbench/api/node/extensionHostProcess
GRADLE_HOME=C:\ProgramData\chocolatey\lib\gradle\tools\gradle-8.12.1
ProgramFiles=C:\Program Files (x86)
PLINK_PROTOCOL=ssh
COLORTERM=truecolor
MSYS=enable_pcon
PATH=C:/Users/Lincpay/AppData/Local/Programs/Git/mingw64/libexec/git-core;C:\Users\Lincpay\AppData\Local\Programs\Git\mingw64\bin;C:\Users\Lincpay\AppData\Local\Programs\Git\usr\bin;C:\Users\Lincpay\bin;C:\Users\Lincpay\.codex\tmp\arg0\codex-arg0wLCfTN;C:\Python313\Scripts\;C:\Python313\;C:\Program Files\Microsoft\jdk-17.0.14.7-hotspot\bin;C:\Program Files\Java\jdk-17\bin;C:\Users\Lincpay\AppData\Local\Android\Sdk\platform-tools;C:\ProgramData\chocolatey\bin;C:\Users\DELL\AppData\Local\nvm;C:\nvm4w\nodejs;C:\Users\Lincpay\AppData\Local\Android\Sdk\emulator;C:\Program Files\nodejs\;C:\Windows\System32;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Users\Lincpay\.console-ninja\.bin;C:\Users\Lincpay\AppData\Local\Microsoft\WindowsApps;C:\Users\Lincpay\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\Lincpay\AppData\Local\Programs\Git\cmd;C:\Users\Lincpay\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\MongoDB Atlas CLI\;C:\Users\Lincpay\AppData\Local\Programs\Antigravity\bin;C:\Users\Lincpay\AppData\Local\Android\Sdk\emulator;C:\Users\Lincpay\AppData\Local\Android\Sdk\platform-tools;C:\Users\Lincpay\AppData\Local\Programs\cursor\resources\app\bin;c:\Users\Lincpay\.antigravity\extensions\openai.chatgpt-26.506.21252-win32-x64\bin\windows-x86_64
ChocolateyInstall=C:\ProgramData\chocolatey
DEBUG=release
VSCODE_L10N_BUNDLE_LOCATION=
VSCODE_CODE_CACHE_PATH=C:\Users\Lincpay\AppData\Roaming\Antigravity\CachedData\15487b3041e65228cae24980a3f796c905ef582c
ProgramW6432=C:\Program Files
NVM_SYMLINK=C:\nvm4w\nodejs
NVM_HOME=C:\Users\DELL\AppData\Local\nvm
HOME=C:\Users\Lincpay
VSCODE_HANDLES_UNCAUGHT_ERRORS=true
LC_CTYPE=C.UTF-8
CommonProgramW6432=C:\Program Files\Common Files
ZES_ENABLE_SYSMAN=1
LOGONSERVER=\\DESKTOP-TAFQRBL
VSCODE_CWD=C:\Users\Lincpay\AppData\Local\Programs\Antigravity
EFC_10100_1592913036=1
SystemRoot=C:\WINDOWS
CODEX_INTERNAL_ORIGINATOR_OVERRIDE=codex_vscode
USERDOMAIN_ROAMINGPROFILE=DESKTOP-TAFQRBL
APPDATA=C:\Users\Lincpay\AppData\Roaming
HOMEDRIVE=C:
RUST_LOG=warn
VSCODE_PID=14604
PSModulePath=C:\Users\Lincpay\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules
ELECTRON_RUN_AS_NODE=1
CHROME_CRASHPAD_PIPE_NAME=\\.\pipe\crashpad_14604_MTACWRXOBAGKDEGK
PROCESSOR_ARCHITEW6432=AMD64
PROCESSOR_ARCHITECTURE=x86
USERPROFILE=C:\Users\Lincpay
OS=Windows_NT
ComSpec=C:\WINDOWS\system32\cmd.exe
SystemDrive=C:
VSCODE_CRASH_REPORTER_PROCESS_TYPE=extensionHost
OneDriveConsumer=C:\Users\Lincpay\OneDrive
ALLUSERSPROFILE=C:\ProgramData
------------
Diagnostic: File system
Skipped: False
Success: True
Exception: None
Log:
Temporary directory is 'C:\Users\Lincpay\AppData\Local\Temp\'...
Checking basic file I/O...
Writing to temporary file 'C:\Users\Lincpay\AppData\Local\Temp\96ba4635bdd4d82432d7df06'... OK
Reading from temporary file 'C:\Users\Lincpay\AppData\Local\Temp\96ba4635bdd4d82432d7df06'... OK
Deleting temporary file 'C:\Users\Lincpay\AppData\Local\Temp\96ba4635bdd4d82432d7df06'... OK
Testing IFileSystem instance...
UserHomePath: C:\Users\Lincpay
UserDataDirectoryPath: C:\Users\Lincpay\.gcm
GetCurrentDirectory(): D:\ProcureLinc\Procurelinc
------------
Diagnostic: Networking
Skipped: False
Success: True
Exception: None
Log:
Checking networking and HTTP stack...
Creating HTTP client... OK
IsNetworkAvailable: True
Sending HEAD request to http://example.com...Sending HEAD request to https://example.com... OK
OK
Acquiring free TCP port... OK
Testing local HTTP loopback connections...
Creating new HTTP listener for http://localhost:55697/... OK
Waiting for loopback connection... OK
Writing response... OK
Waiting for response data... OK
Loopback connection data OK
------------
Diagnostic: Git
Skipped: False
Success: True
Exception: None
Log:
Getting Git version... OK
Git version is '2.45.2.windows.1'
Locating current repository...Git repository at 'D:/ProcureLinc/Procurelinc/.git'
OK
Listing all Git configuration... OK
Git configuration:
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig diff.astextplain.textconv=astextplain
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig filter.lfs.clean=git-lfs clean -- %f
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig filter.lfs.smudge=git-lfs smudge -- %f
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig filter.lfs.process=git-lfs filter-process
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig filter.lfs.required=true
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig http.sslbackend=openssl
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig http.sslcainfo=C:/Users/Lincpay/AppData/Local/Programs/Git/mingw64/etc/ssl/certs/ca-bundle.crt
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig core.autocrlf=true
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig core.fscache=true
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig core.symlinks=false
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig pull.rebase=false
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig credential.helper=manager
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig credential.https://dev.azure.com.usehttppath=true
file:C:/Users/Lincpay/AppData/Local/Programs/Git/etc/gitconfig init.defaultbranch=master
file:C:/Users/Lincpay/.gitconfig user.email=abintoppo96@gmail.com
file:C:/Users/Lincpay/.gitconfig user.name=Abin Toppo
file:C:/Users/Lincpay/.gitconfig credential.https://git.lincpay.in.provider=generic
file:C:/Users/Lincpay/.gitconfig credential.https://git.zeptic.in.provider=generic
file:C:/Users/Lincpay/.gitconfig credential.http://dev1.lincpay:3000.provider=generic
file:.git/config core.repositoryformatversion=0
file:.git/config core.filemode=false
file:.git/config core.bare=false
file:.git/config core.logallrefupdates=true
file:.git/config core.symlinks=false
file:.git/config core.ignorecase=true
file:.git/config remote.origin.url=http://dev1.lincpay:3000/rohit/Procurelinc.git
file:.git/config remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
file:.git/config branch.dev.remote=origin
file:.git/config branch.dev.merge=refs/heads/dev
file:.git/config branch.dev.vscode-merge-base=origin/main
file:.git/config branch.featureDSc/device.vscode-merge-base=origin/dev
------------
Diagnostic: Credential storage
Skipped: False
Success: True
Exception: None
Log:
ICredentialStore instance is of type: CredentialStore
Writing test credential... OK
Reading test credential... OK
Deleting test credential... OK
------------
Diagnostic: Microsoft authentication (AAD/MSA)
Skipped: False
Success: True
Exception: None
Log:
Broker is not enabled.
Flow type is: Auto
Gathering MSAL token cache data... OK
CacheDirectory: C:\Users\Lincpay\AppData\Local\.IdentityService
CacheFileName: msal.cache
CacheFilePath: C:\Users\Lincpay\AppData\Local\.IdentityService\msal.cache
Creating cache helper... OK
Verifying MSAL token cache persistence... OK
------------
Diagnostic: GitHub API
Skipped: False
Success: True
Exception: None
Log:
Using 'https://github.com/' as API target.
Querying '/meta' endpoint... OK
+1 -1
View File
@@ -24,7 +24,7 @@ target 'procure' do
) )
post_install do |installer| post_install do |installer|
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 # http://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
react_native_post_install( react_native_post_install(
installer, installer,
config[:reactNativePath], config[:reactNativePath],
+1 -1
View File
@@ -32,7 +32,7 @@
<true/> <true/>
</dict> </dict>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string/> <string>We need your location to securely bind this device for tender bidding verification.</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
+18 -2
View File
@@ -1,3 +1,4 @@
const path = require("path");
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config"); const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
const defaultConfig = getDefaultConfig(__dirname); const defaultConfig = getDefaultConfig(__dirname);
@@ -5,7 +6,7 @@ const { assetExts, sourceExts } = defaultConfig.resolver;
/** /**
* Metro configuration * Metro configuration
* https://reactnative.dev/docs/metro * http://reactnative.dev/docs/metro
* *
* @type {import('metro-config').MetroConfig} * @type {import('metro-config').MetroConfig}
*/ */
@@ -17,7 +18,22 @@ const config = {
}, },
resolver: { resolver: {
assetExts: assetExts.filter((ext) => ext !== "svg"), assetExts: assetExts.filter((ext) => ext !== "svg"),
sourceExts: [...sourceExts, "svg"] sourceExts: [...sourceExts, "svg"],
blockList: [
// Exclude native Android build artifacts that are created/deleted
// during Gradle builds, which crash Metro's file watcher (ENOENT)
/node_modules\/.*\/android\/\.cxx\/.*/,
/node_modules\/.*\/android\/build\/.*/,
/node_modules\/.*\/android\/\.gradle\/.*/,
]
},
watcher: {
additionalExclusions: [
// Also exclude from the watcher layer for safety
"**/android/.cxx/**",
"**/android/build/**",
"**/android/.gradle/**",
]
} }
}; };
+2921 -688
View File
File diff suppressed because it is too large Load Diff
+66 -57
View File
@@ -7,80 +7,89 @@
"ios": "react-native run-ios", "ios": "react-native run-ios",
"lint": "eslint .", "lint": "eslint .",
"start": "react-native start", "start": "react-native start",
"test": "jest" "test": "jest",
"create:users": "node scripts/create-users.js"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/blur": "^4.4.1", "@react-native-community/blur": "4.4.1",
"@react-native-documents/picker": "^11.0.3", "@react-native-documents/picker": "11.0.3",
"@react-native-masked-view/masked-view": "^0.3.2", "@react-native-masked-view/masked-view": "0.2.9",
"@react-native-vector-icons/material-icons": "^12.4.0", "@react-native-vector-icons/material-icons": "12.4.0",
"@react-navigation/bottom-tabs": "^7.9.0", "@react-navigation/bottom-tabs": "7.9.0",
"@react-navigation/native": "^7.1.26", "@react-navigation/drawer": "^7.8.1",
"@react-navigation/stack": "^7.6.13", "@react-navigation/native": "^7.1.28",
"@reduxjs/toolkit": "^2.11.2", "@react-navigation/stack": "7.6.13",
"@stomp/stompjs": "^7.2.1", "@reduxjs/toolkit": "2.11.2",
"@tanstack/react-query": "^5.90.12", "@stomp/stompjs": "7.2.1",
"axios": "^1.13.2", "@tanstack/react-query": "5.90.12",
"buffer": "^6.0.3", "axios": "1.13.2",
"eslint-config-prettier": "^10.1.8", "buffer": "6.0.3",
"eslint-plugin-react": "^7.37.5", "crypto-js": "^4.2.0",
"eslint-plugin-react-native": "^5.0.0", "eslint-config-prettier": "10.1.8",
"exceljs": "^4.4.0", "eslint-plugin-react": "7.37.5",
"formik": "^2.4.9", "eslint-plugin-react-native": "5.0.0",
"lottie-react-native": "^7.3.4", "exceljs": "4.4.0",
"lucide-react-native": "^0.562.0", "formik": "2.4.9",
"moment": "^2.30.1", "lottie-react-native": "7.3.4",
"lucide-react-native": "0.562.0",
"moment": "2.30.1",
"react": "19.0.0", "react": "19.0.0",
"react-native": "0.79.0", "react-native": "0.79.0",
"react-native-alert-notification": "^0.4.2", "react-native-alert-notification": "0.4.2",
"react-native-app-intro-slider": "^4.0.4", "react-native-app-intro-slider": "4.0.4",
"react-native-blob-util": "^0.24.6", "react-native-blob-util": "0.24.6",
"react-native-bouncy-checkbox": "^4.1.2", "react-native-bouncy-checkbox": "4.1.2",
"react-native-date-picker": "^5.0.13", "react-native-date-picker": "5.0.13",
"react-native-element-dropdown": "^2.12.4", "react-native-device-info": "^15.0.2",
"react-native-fs": "^2.20.0", "react-native-element-dropdown": "2.12.4",
"react-native-fs": "2.20.0",
"react-native-geolocation-service": "^5.3.1",
"react-native-gesture-handler": "^2.30.0", "react-native-gesture-handler": "^2.30.0",
"react-native-gifted-charts": "^1.4.70", "react-native-get-random-values": "1.11.0",
"react-native-haptic-feedback": "^2.3.3", "react-native-gifted-charts": "1.4.70",
"react-native-haptic-feedback": "2.3.3",
"react-native-linear-gradient": "^2.8.3", "react-native-linear-gradient": "^2.8.3",
"react-native-modal": "^14.0.0-rc.1", "react-native-modal": "^14.0.0-rc.1",
"react-native-permissions": "^5.4.4", "react-native-permissions": "5.4.4",
"react-native-reanimated": "^3.18.0", "react-native-reanimated": "^3.18.0",
"react-native-responsive-screen": "^1.4.2", "react-native-responsive-screen": "1.4.2",
"react-native-safe-area-context": "^5.6.2", "react-native-safe-area-context": "5.6.2",
"react-native-screens": "^4.12.0", "react-native-screens": "4.12.0",
"react-native-skeleton-placeholder": "^5.2.4", "react-native-skeleton-placeholder": "5.2.4",
"react-native-svg": "^15.12.0", "react-native-svg": "15.12.0",
"react-native-svg-transformer": "^1.5.1", "react-native-svg-transformer": "1.5.1",
"react-native-webview": "^13.16.0", "react-native-webview": "13.16.0",
"react-redux": "^9.2.0", "react-redux": "9.2.0",
"redux-persist": "^6.0.0", "redux-persist": "6.0.0",
"sockjs-client": "^1.6.1", "sockjs-client": "1.6.1",
"text-encoding": "^0.7.0", "text-encoding": "0.7.0",
"uuid": "13.0.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
"yup": "^1.7.1" "yup": "1.7.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "7.25.2",
"@babel/preset-env": "^7.25.3", "@babel/preset-env": "7.25.3",
"@babel/runtime": "^7.25.0", "@babel/runtime": "7.25.0",
"@react-native-community/cli": "^18.0.1", "@react-native-community/cli": "18.0.1",
"@react-native-community/cli-platform-android": "18.0.0", "@react-native-community/cli-platform-android": "^18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0", "@react-native-community/cli-platform-ios": "18.0.0",
"@react-native/babel-preset": "0.79.0", "@react-native/babel-preset": "0.79.0",
"@react-native/eslint-config": "0.79.0", "@react-native/eslint-config": "0.79.0",
"@react-native/metro-config": "0.79.0", "@react-native/metro-config": "0.79.0",
"@react-native/typescript-config": "0.79.0", "@react-native/typescript-config": "0.79.0",
"@types/jest": "^29.5.13", "@types/crypto-js": "^4.2.2",
"@types/react": "^19.0.0", "@types/jest": "29.5.13",
"@types/react-test-renderer": "^19.0.0", "@types/react": "19.0.0",
"@types/sockjs-client": "^1.5.4", "@types/react-test-renderer": "19.0.0",
"@types/text-encoding": "^0.0.40", "@types/sockjs-client": "1.5.4",
"eslint": "^8.19.0", "@types/text-encoding": "0.0.40",
"jest": "^29.6.3", "eslint": "^8.57.0",
"jest": "29.6.3",
"jsencrypt": "^3.5.4",
"prettier": "2.8.8", "prettier": "2.8.8",
"react-native-bundle-visualizer": "^3.1.3", "react-native-bundle-visualizer": "3.1.3",
"react-test-renderer": "19.0.0", "react-test-renderer": "19.0.0",
"typescript": "5.0.4" "typescript": "5.0.4"
}, },
Binary file not shown.
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env node
const axios = require('axios');
const { BASE_URL } = require('../src/Services/ApiCall/ApiUrls');
const REGISTER_URL =
`${BASE_URL}contractor/registerContractorStage1`;
const DESIGNATION_URL =
`${BASE_URL}contractor/getAllDesignationContractor`;
const DEFAULT_PASSWORD = 'P@ssword123';
function getArgValue(name, fallback) {
const prefix = `--${name}=`;
const arg = process.argv.find(item => item.startsWith(prefix));
if (!arg) return fallback;
return arg.slice(prefix.length);
}
function toPositiveInt(value, fallback) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) return fallback;
return parsed;
}
function buildMobile(seed, index) {
// Keep 10 digits and start with 9 to match app-side validation.
const dynamicPart = (seed * 1000 + index) % 999999999;
return String(9000000000 + dynamicPart).padStart(10, '9').slice(0, 10);
}
function toAlphabeticSuffix(index) {
let n = index;
let label = '';
while (n > 0) {
const rem = (n - 1) % 26;
label = String.fromCharCode(65 + rem) + label;
n = Math.floor((n - 1) / 26);
}
return label || 'A';
}
async function getDefaultDesignation() {
try {
const { data } = await axios.get(DESIGNATION_URL, { timeout: 15000 });
const list = data?.data;
if (Array.isArray(list) && list.length > 0) {
const firstId = Number(list[0]?.designationid);
if (Number.isFinite(firstId) && firstId > 0) return firstId;
}
} catch (error) {
const message = error?.response?.data?.message || error?.message || 'Unknown error';
console.warn(`Failed to fetch designation list, using fallback id=1. ${message}`);
}
return 1;
}
async function registerUser({
designation,
emailPrefix,
firstNamePrefix,
seed,
index,
password,
}) {
const serial = seed * 1000 + index;
const emailAdd = `${emailPrefix}${serial}@example.com`;
const alphaSuffix = toAlphabeticSuffix(index);
const payload = {
firstName: `${firstNamePrefix}${alphaSuffix}`,
middleName: '',
lastName: 'Auto',
dialCode: '+91',
mobNo: buildMobile(seed, index),
emailAdd,
contractorType: 'Indian',
designation,
password,
otherDesignation: null,
conPassword: password,
device: 'mobileApp',
};
try {
const { data } = await axios.post(REGISTER_URL, payload, { timeout: 20000 });
const status = String(data?.status ?? data?.statusCode ?? '');
const ok = status === '201' || status === '200' || status === 'SUCCESS';
return {
ok,
email: emailAdd,
mobile: payload.mobNo,
message: data?.message || (ok ? 'Success' : 'Unexpected response'),
rawStatus: status || 'unknown',
details: data,
};
} catch (error) {
const responseData = error?.response?.data;
const apiMessage =
responseData?.message ||
responseData?.error ||
error?.message ||
'Request failed';
return {
ok: false,
email: emailAdd,
mobile: payload.mobNo,
message: apiMessage,
rawStatus: String(error?.response?.status || 'network_error'),
details: responseData,
};
}
}
async function main() {
const count = toPositiveInt(getArgValue('count', '100'), 100);
const concurrency = toPositiveInt(getArgValue('concurrency', '5'), 5);
const startIndex = toPositiveInt(getArgValue('start', '1'), 1);
const password = getArgValue('password', DEFAULT_PASSWORD);
const emailPrefix = getArgValue('emailPrefix', 'bulkuser');
const firstNamePrefix = getArgValue('firstNamePrefix', 'BulkUser');
const seed = Number(String(Date.now()).slice(-6));
const designation = await getDefaultDesignation();
console.log(
`Creating ${count} users (start=${startIndex}, concurrency=${concurrency}, designation=${designation})...`,
);
const indexes = Array.from({ length: count }, (_, i) => i + startIndex);
let cursor = 0;
const results = [];
async function worker() {
while (true) {
const current = cursor;
cursor += 1;
if (current >= indexes.length) break;
const index = indexes[current];
const result = await registerUser({
designation,
emailPrefix,
firstNamePrefix,
seed,
index,
password,
});
results.push(result);
const marker = result.ok ? 'OK' : 'FAIL';
console.log(`[${marker}] #${index} ${result.email} -> ${result.message}`);
}
}
await Promise.all(Array.from({ length: concurrency }, () => worker()));
const success = results.filter(item => item.ok).length;
const failed = results.length - success;
console.log('\nSummary');
console.log(`Total: ${results.length}`);
console.log(`Success: ${success}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
console.log('\nFailed entries (up to 20):');
results
.filter(item => !item.ok)
.slice(0, 20)
.forEach(item => {
console.log(`- ${item.email} (${item.mobile}) [${item.rawStatus}] ${item.message}`);
if (item.details) {
console.log(` details: ${JSON.stringify(item.details)}`);
}
});
process.exitCode = 1;
}
}
main().catch(error => {
console.error('Bulk user creation crashed:', error?.message || error);
process.exit(1);
});
+78 -12
View File
@@ -1,6 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { setToken } from '../Redux/Slices/Token'; import { setToken } from '../Redux/Slices/Token';
import { store } from '../Redux/Store'; import { store } from '../Redux/Store';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { resetAndNavigate } from '../Navigation/NavigationService';
import NavigationString from '../Constant/NavigationString';
export const apiCall = async <R, D = {}>( export const apiCall = async <R, D = {}>(
method: 'get' | 'post' | 'patch' | 'delete' | 'put', method: 'get' | 'post' | 'patch' | 'delete' | 'put',
@@ -9,9 +13,28 @@ export const apiCall = async <R, D = {}>(
params?: any, params?: any,
contentType?: string contentType?: string
): Promise<R> => { ): Promise<R> => {
try {
const token: any = store.getState().token.token; const token: any = store.getState().token.token;
// 🔥 ONE MAIN GROUP FOR ENTIRE API CALL
console.group(
`%c🚀 API CALL → ${method.toUpperCase()} ${url}`,
'color:#0ea5e9;font-weight:bold'
);
// ===== REQUEST =====
console.group('📤 REQUEST');
console.log('%cMETHOD →', 'color:#22c55e;font-weight:bold', method);
console.log('%cURL →', 'color:#22c55e;font-weight:bold', url);
console.log('%cPARAMS →', 'color:#f59e0b;font-weight:bold', params);
console.log('%cPAYLOAD →', 'color:#a855f7;font-weight:bold', data);
console.log(
'%cTOKEN →',
'color:#ef4444;font-weight:bold',
token?.token ? '✔ PRESENT' : '❌ MISSING'
);
console.groupEnd();
try {
const response = await axios({ const response = await axios({
method, method,
url, url,
@@ -23,26 +46,69 @@ export const apiCall = async <R, D = {}>(
}, },
}); });
// ===== RESPONSE =====
console.group('📥 RESPONSE');
console.log(
'%cSTATUS →',
'color:#22c55e;font-weight:bold',
response.status
);
console.log(
'%cRESPONSE DATA →',
'color:#0ea5e9;font-weight:bold',
response.data
);
console.groupEnd();
console.groupEnd(); // 🔚 MAIN GROUP
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
const errorMessage = error?.response?.data?.error; // ===== ERROR =====
// console.log(error?.response?.data.error, '---------------------------api error message'); console.group('❌ ERROR');
console.error(
'%cSTATUS →',
'color:#ef4444;font-weight:bold',
error?.response?.status
);
console.error(
'%cMESSAGE →',
'color:#f97316;font-weight:bold',
error?.response?.data?.message || error.message
);
console.error(
'%cERROR DATA →',
'color:#fb7185;font-weight:bold',
error?.response?.data
);
console.error(
'%cFULL ERROR →',
'color:#7c2d12;font-weight:bold',
error
);
console.groupEnd();
if (error?.response?.statusCode === 401) { // ===== AUTH ERROR =====
if (error?.response?.status === 401) {
console.warn(
'%c⚠ JWT ERROR → Token expired or malformed',
'color:#eab308;font-weight:bold'
);
store.dispatch(setToken({})); store.dispatch(setToken({}));
console.groupEnd(); // 🔚 MAIN GROUP
return { return {
success: false, success: false,
message: 'JWT Error - Token expired or malformed', message: 'JWT Error - Token expired or malformed',
} as R; } as R;
} }
const data = {
data: {
console.groupEnd(); // 🔚 MAIN GROUP
return {
success: false, success: false,
message: error?.response?.data?.message || 'No Response From Server', message:
} error?.response?.data?.message || 'No Response From Server',
...(error?.response?.data || {})
} } as R;
return data as any
} }
}; };
+76 -51
View File
@@ -1,37 +1,8 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View, StyleProp, ViewStyle, TextStyle } from 'react-native'; import { StyleSheet, Text, View, StyleProp, ViewStyle, TextStyle } from 'react-native';
import { Dropdown } from 'react-native-element-dropdown'; import { Dropdown } from 'react-native-element-dropdown';
import { Shield } from 'lucide-react-native'; import { Shield, ChevronDown } from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
const colors = {
primary: '#F97316',
primaryLight: '#FDBA74',
primaryHighlight: '#f4f1efff',
secondary: '#1B4B66',
secondaryLight: '#639599',
success: '#22C55E',
error: '#EF4444',
warning: '#F59E0B',
info: '#3B82F6',
golden: '#FFD700',
white: '#FFFFFF',
black: '#000000',
darkText: '#2B2B2B',
grayText: '#6B7280',
lightText: '#9CA3AF',
border: '#E5E7EB',
lightGray: '#F9FAFB',
disabled: '#D1D5DB',
placeholder: '#9CA3AF',
backdrop: 'rgba(0,0,0,0.5)',
shadow: 'rgba(0,0,0,0.12)',
Orange: '#F97316',
OrangeLight: '#FDBA74',
OrangeSoft: '#FFF3E8',
darkGray: '#5f6368',
lightestGray: '#f1f3f4',
successLight: '#e6f4ea',
};
type DropdownItem = { type DropdownItem = {
[key: string]: any; [key: string]: any;
@@ -74,9 +45,22 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
dropdownStyle, dropdownStyle,
...rest ...rest
}) => { }) => {
const { colors, spacing, typography, shadows } = useTheme();
return ( return (
<View style={[{ zIndex: 4000 }, containerStyle]}> <View style={[{ zIndex: 4000, marginBottom: spacing.m }, containerStyle]}>
{label && <Text style={styles.label}>{label}</Text>} {label && (
<Text style={[styles.label, {
color: colors.text,
fontSize: typography.size.s,
fontWeight: '600',
letterSpacing: 0.5,
textTransform: 'uppercase',
marginBottom: spacing.xs
}]}>
{label}
</Text>
)}
<Dropdown <Dropdown
data={data} data={data}
labelField={labelField} labelField={labelField}
@@ -86,23 +70,70 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
onChange={onChange} onChange={onChange}
style={[ style={[
styles.dropdown, styles.dropdown,
{
backgroundColor: colors.surface,
borderColor: focusedField ? colors.primary : colors.border,
borderWidth: 1.5,
borderRadius: 12,
paddingHorizontal: spacing.m,
paddingVertical: 12,
height: 56,
},
focusedField && shadows.light,
style, style,
{ borderColor: focusedField ? colors.primary : '#e0e0e0' },
]} ]}
containerStyle={[styles.dropdownContainer, dropdownStyle]} containerStyle={[
selectedTextStyle={[styles.selectedText, selectedTextStyle]} styles.dropdownContainer,
placeholderStyle={[styles.placeholderText, placeholderStyle]} {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 12,
...shadows.medium,
},
dropdownStyle
]}
selectedTextStyle={[
styles.selectedText,
{
color: colors.text,
fontSize: typography.size.m,
fontWeight: '500'
},
selectedTextStyle
]}
placeholderStyle={[
styles.placeholderText,
{
color: colors.warmGray,
fontSize: typography.size.m
},
placeholderStyle
]}
itemTextStyle={{ color: colors.textSecondary }}
activeColor={colors.background} // Beige for active item bg
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
search search
searchPlaceholder="Search Class" searchPlaceholder="Search..."
inputSearchStyle={{
borderRadius: 8,
backgroundColor: colors.background,
borderColor: colors.border,
color: colors.text
}}
renderLeftIcon={() => ( renderLeftIcon={() => (
<Shield <Shield
style={{ marginRight: 10 }} style={{ marginRight: 10 }}
color={focusedField ? colors.primary : '#aaa'} color={focusedField ? colors.primary : colors.warmGray}
size={20} size={20}
/> />
)} )}
renderRightIcon={() => (
<ChevronDown
size={20}
color={focusedField ? colors.primary : colors.warmGray}
/>
)}
{...rest} {...rest}
/> />
</View> </View>
@@ -113,25 +144,19 @@ export default MyDropdown;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
label: { label: {
marginVertical: 10, // marginVertical: 10, handled inline
color: colors.darkText,
}, },
dropdown: { dropdown: {
borderWidth: 1, // handled inline with theme
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 12,
borderColor: colors.border,
backgroundColor: colors.white,
}, },
dropdownContainer: { dropdownContainer: {
borderRadius: 12, // handled inline
borderColor: colors.border, marginTop: 4,
}, },
selectedText: { selectedText: {
color: colors.darkText, // handled inline
}, },
placeholderText: { placeholderText: {
color: colors.placeholder, // handled inline
}, },
}); });
+22 -16
View File
@@ -2,10 +2,9 @@ import React from 'react';
import { View, Text, TouchableOpacity, Image, StyleSheet, Dimensions } from 'react-native'; import { View, Text, TouchableOpacity, Image, StyleSheet, Dimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ArrowLeft } from 'lucide-react-native'; import { ArrowLeft } from 'lucide-react-native';
import colors from '../../Constant/Color'; import { useTheme } from '../../Hooks/useTheme';
import ImagePath from '../../Constant/ImagePath'; import ImagePath from '../../Constant/ImagePath';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import FontsFamily from '../../Constant/FontsFamily';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@@ -18,6 +17,7 @@ interface HeaderProps {
const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => { const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
const navigation: any = useNavigation(); const navigation: any = useNavigation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { colors, spacing, typography, shadows } = useTheme();
return ( return (
<View <View
@@ -25,6 +25,8 @@ const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
styles.container, styles.container,
{ {
paddingTop: insets.top - 10, paddingTop: insets.top - 10,
backgroundColor: colors.background, // Off-white
...shadows.light, // Soft shadow
}, },
]} ]}
> >
@@ -36,13 +38,21 @@ const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
style={styles.backButton} style={styles.backButton}
activeOpacity={0.7} activeOpacity={0.7}
> >
<ArrowLeft size={26} color={colors.black} strokeWidth={2.5} /> <ArrowLeft size={24} color={colors.primary} strokeWidth={2.5} />
</TouchableOpacity> </TouchableOpacity>
)} )}
<Text style={styles.title}>{title}</Text> <Text style={[styles.title, {
color: colors.primary,
fontSize: typography.size.xl,
fontWeight: '600',
letterSpacing: 0.5
}]}>
{title}
</Text>
</View> </View>
{/* Optional: Add Logo or Right Action */}
<TouchableOpacity onPress={() => { navigation.navigate(NavigationString.Home) }}> <TouchableOpacity onPress={() => { navigation.navigate(NavigationString.Home) }}>
<Image <Image
source={ImagePath.Icon.Logo2} source={ImagePath.Icon.Logo2}
@@ -60,25 +70,21 @@ export default InsideHeader;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
width, width,
backgroundColor: colors.white, // IMPORTANT for shadow
shadowColor: '#000', // iOS shadow
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.12,
shadowRadius: 10,
elevation: 6,
zIndex: 10, zIndex: 10,
paddingBottom: 10,
}, },
row: { row: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 10, paddingHorizontal: 16,
}, },
leftRow: { leftRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
flex: 1,
}, },
backButton: { backButton: {
@@ -88,16 +94,16 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 8, marginRight: 8,
// backgroundColor: 'rgba(0,0,0,0.03)' // Optional touch target bg
}, },
title: { title: {
fontSize: 20, flex: 1,
color: colors.black,
fontFamily: FontsFamily.poppinsSemiBold,
}, },
logo: { logo: {
height: 60, height: 48, // Slightly smaller for cleaner look
width: 60, width: 48,
opacity: 0.9,
}, },
}); });
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView, StatusBar } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView } from 'react-native';
import LottieView from 'lottie-react-native'; import LottieView from 'lottie-react-native';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import { AlertCircle, Clock, RefreshCw, Activity } from 'lucide-react-native'; import { AlertCircle, Clock, RefreshCw, Activity } from 'lucide-react-native';
@@ -10,7 +10,6 @@ import FontsFamily from '../../Constant/FontsFamily';
const ServerDown = () => { const ServerDown = () => {
return ( return (
<> <>
<StatusBar barStyle="dark-content" />
<ScrollView <ScrollView
contentContainerStyle={styles.scrollContainer} contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}> showsVerticalScrollIndicator={false}>
+4 -12
View File
@@ -1,4 +1,4 @@
import { StyleSheet, Text, TouchableOpacity, ViewStyle } from 'react-native'; import { StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
import React from 'react'; import React from 'react';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import colors from '../../Constant/Color'; import colors from '../../Constant/Color';
@@ -13,16 +13,8 @@ type PrimaryButtonProps = {
const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, disabled }) => { const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, disabled }) => {
return ( return (
<LinearGradient <View
colors={[
colors.primary, // #F97316 (Orange)
colors.primaryLight, // #FDBA74 (OrangeLight)
colors.OrangeLight, // #FDBA74 (backup)
colors.OrangeSoft // #FFF3E8 (super soft orange end)
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
locations={[0.0, 0.4, 0.7, 1.0]}
style={[styles.primaryButton, style]} style={[styles.primaryButton, style]}
> >
<TouchableOpacity <TouchableOpacity
@@ -33,7 +25,7 @@ const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, di
> >
<Text style={styles.buttonText}>{title}</Text> <Text style={styles.buttonText}>{title}</Text>
</TouchableOpacity> </TouchableOpacity>
</LinearGradient> </View>
); );
}; };
+39 -34
View File
@@ -7,7 +7,7 @@ import Animated, {
withTiming, withTiming,
interpolateColor, interpolateColor,
} from "react-native-reanimated"; } from "react-native-reanimated";
import colors from "../../Constant/Color"; import { useTheme } from "../../Hooks/useTheme";
export interface SearchType { export interface SearchType {
placeholder?: string; placeholder?: string;
@@ -15,7 +15,7 @@ export interface SearchType {
label?: string; label?: string;
filter?: boolean; filter?: boolean;
filterPress?: () => void; filterPress?: () => void;
debounceDelay?: number; // 👈 optional debounceDelay?: number;
} }
const AnimatedView = Animated.createAnimatedComponent(View); const AnimatedView = Animated.createAnimatedComponent(View);
@@ -26,8 +26,9 @@ export const SearchInput: React.FC<SearchType> = ({
label, label,
filter = true, filter = true,
filterPress, filterPress,
debounceDelay = 500, // 👈 default debounce debounceDelay = 500,
}) => { }) => {
const { colors, shadows } = useTheme();
const isFocused = useSharedValue(0); const isFocused = useSharedValue(0);
const [text, setText] = useState(''); const [text, setText] = useState('');
const debounceRef = useRef<NodeJS.Timeout | null>(null); const debounceRef = useRef<NodeJS.Timeout | null>(null);
@@ -36,18 +37,17 @@ export const SearchInput: React.FC<SearchType> = ({
const borderColor = interpolateColor( const borderColor = interpolateColor(
isFocused.value, isFocused.value,
[0, 1], [0, 1],
[colors.grayText + '50', colors.grayText] [colors.overlay, colors.primary] // Use theme overlay/primary
); );
return { return {
borderColor, borderColor,
borderWidth: withTiming(isFocused.value ? 1.5 : 1), borderWidth: withTiming(isFocused.value ? 1 : 1), // Constant thin border
transform: [{ scale: withTiming(isFocused.value ? 1.01 : 1) }], transform: [{ scale: withTiming(isFocused.value ? 1.01 : 1) }],
shadowOpacity: withTiming(isFocused.value ? 0.1 : 0),
}; };
}); });
// 🔥 Debounce logic // Debounce logic
useEffect(() => { useEffect(() => {
if (debounceRef.current) { if (debounceRef.current) {
clearTimeout(debounceRef.current); clearTimeout(debounceRef.current);
@@ -66,34 +66,47 @@ export const SearchInput: React.FC<SearchType> = ({
return ( return (
<View style={styles.container}> <View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>} {label && <Text style={[styles.label, { color: colors.text }]}>{label}</Text>}
<View style={styles.row}> <View style={styles.row}>
<AnimatedView style={[styles.inputWrapper, inputContainerStyle]}> <AnimatedView style={[
styles.inputWrapper,
{
backgroundColor: colors.glass,
...shadows.light // Use theme shadow
},
inputContainerStyle
]}>
<Search <Search
size={18} size={18}
color={colors.grayText} color={colors.primary} // Theme primary
style={styles.searchIcon} style={styles.searchIcon}
/> />
<TextInput <TextInput
style={styles.input} style={[styles.input, { color: colors.text }]}
placeholder={placeholder} placeholder={placeholder}
value={text} value={text}
onChangeText={setText} // 👈 update local state only onChangeText={setText}
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
onFocus={() => (isFocused.value = withTiming(1))} onFocus={() => (isFocused.value = withTiming(1))}
onBlur={() => (isFocused.value = withTiming(0))} onBlur={() => (isFocused.value = withTiming(0))}
selectionColor={colors.grayText} selectionColor={colors.primary}
/> />
</AnimatedView> </AnimatedView>
{filter && ( {filter && (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
style={styles.filterButton} style={[
styles.filterButton,
{
backgroundColor: colors.surface,
borderColor: colors.overlay
}
]}
onPress={filterPress} onPress={filterPress}
> >
<Filter size={22} color={colors.black} /> <Filter size={20} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
@@ -110,7 +123,6 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
marginBottom: 8, marginBottom: 8,
color: colors.black,
letterSpacing: 0.3, letterSpacing: 0.3,
}, },
row: { row: {
@@ -122,39 +134,32 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
height: 48, height: 50, // Slightly taller pill
backgroundColor: '#F9FAFB', borderRadius: 25, // Full Pill
borderRadius: 12, paddingHorizontal: 16,
paddingHorizontal: 12,
borderWidth: 1, borderWidth: 1,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowRadius: 4,
elevation: 2,
}, },
searchIcon: { searchIcon: {
marginRight: 8, marginRight: 10,
}, },
input: { input: {
flex: 1, flex: 1,
height: '100%', height: '100%',
fontSize: 15, fontSize: 15,
color: colors.black, fontWeight: '500',
paddingVertical: Platform.OS === 'ios' ? 0 : 8, paddingVertical: Platform.OS === 'ios' ? 0 : 8,
}, },
filterButton: { filterButton: {
height: 48, height: 50,
width: 48, width: 50,
backgroundColor: '#FFFFFF',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderRadius: 12, borderRadius: 25, // Circle
borderWidth: 1, borderWidth: 1,
borderColor: colors.grayText + '30',
elevation: 2, elevation: 2,
shadowColor: "#000", shadowColor: "#000",
shadowOffset: { width: 0, height: 1 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1, shadowOpacity: 0.1,
shadowRadius: 2, shadowRadius: 4,
}, },
}); });
-1
View File
@@ -73,7 +73,6 @@ const SideDrawer: React.FC<Props> = ({ visible, onClose }) => {
{/* ================= Header / Profile ================= */} {/* ================= Header / Profile ================= */}
<View style={styles.profileHeader}> <View style={styles.profileHeader}>
{/* <TouchableOpacity style={styles.closeBtn} onPress={() => { {/* <TouchableOpacity style={styles.closeBtn} onPress={() => {
console.log('sdfadsfdsf');
}}> }}>
<X size={20} color="#111827" /> <X size={20} color="#111827" />
</TouchableOpacity> */} </TouchableOpacity> */}
+97 -21
View File
@@ -1,34 +1,110 @@
import { View, Dimensions } from 'react-native'; import React, { useEffect, useRef } from 'react';
import React from 'react'; import { View, Animated, StyleSheet, Dimensions } from 'react-native';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; import { useTheme } from '../../Hooks/useTheme';
const Skeleton = () => { // 1. Create a Reusable "Bone" Component
const { width } = Dimensions.get('screen'); const SkeletonBone = ({ width, height, style, borderRadius = 8 }) => {
const cardWidth = width - 30; // full width with some padding const { isDarkMode } = useTheme();
const opacity = useRef(new Animated.Value(0.3)).current;
// Animation Loop
useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 1000,
useNativeDriver: true,
}),
])
);
animation.start();
return () => animation.stop();
}, [opacity]);
// Dynamic colors based on theme
const boneColor = isDarkMode ? '#3E4145' : '#E1E9EE';
return ( return (
<View style={{ padding: 10 }}> <Animated.View
style={[
{
width: width,
height: height,
backgroundColor: boneColor,
borderRadius: borderRadius,
opacity: opacity,
},
style,
]}
/>
);
};
// 2. Main Skeleton Layout
const Skeleton = () => {
const { colors } = useTheme();
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
{[...Array(6)].map((_, index) => ( {[...Array(6)].map((_, index) => (
<SkeletonPlaceholder key={index}> <View
<SkeletonPlaceholder.Item key={index}
width={cardWidth} style={{
marginBottom={20} marginBottom: 16,
borderRadius={12} marginHorizontal: 16,
overflow="hidden" borderRadius: 20,
backgroundColor: colors.surface,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
}}
> >
{/* Image */} {/* Header Row: Tag + Price */}
<SkeletonPlaceholder.Item width={cardWidth} height={150} borderRadius={12} /> <View style={styles.row}>
<SkeletonBone width={80} height={24} borderRadius={12} />
<SkeletonBone width={60} height={24} borderRadius={4} />
</View>
{/* Title */} {/* Title (2 lines) */}
<SkeletonPlaceholder.Item marginTop={10} width={cardWidth * 0.7} height={18} borderRadius={6} /> <View style={styles.section}>
<SkeletonBone width="90%" height={18} borderRadius={4} style={{ marginBottom: 6 }} />
<SkeletonBone width="60%" height={18} borderRadius={4} />
</View>
{/* Subtitle / Price */} {/* Info Grid (2 cols) */}
<SkeletonPlaceholder.Item marginTop={6} width={cardWidth * 0.5} height={14} borderRadius={6} /> <View style={styles.row}>
</SkeletonPlaceholder.Item> <SkeletonBone width="45%" height={40} borderRadius={8} />
</SkeletonPlaceholder> <SkeletonBone width="45%" height={40} borderRadius={8} />
</View>
{/* Action Row */}
<View style={styles.row}>
<SkeletonBone width="35%" height={40} borderRadius={14} />
<SkeletonBone width="60%" height={40} borderRadius={14} />
</View>
</View>
))} ))}
</View> </View>
); );
}; };
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
section: {
marginBottom: 16,
},
});
export default Skeleton; export default Skeleton;
+105 -67
View File
@@ -1,12 +1,14 @@
import React, { useRef, useEffect } from 'react'; import React, { useMemo } from 'react';
import { import { View, StyleSheet } from 'react-native';
View, import Animated, {
Text, useSharedValue,
TouchableOpacity, useAnimatedStyle,
StyleSheet, withSpring,
Animated, withTiming,
} from 'react-native'; } from 'react-native-reanimated';
import colors from '../Constant/Color'; import { useTheme } from '../Hooks/useTheme';
import AppText from '../components/AppText';
import { Check } from 'lucide-react-native';
type StepProgressBarProps = { type StepProgressBarProps = {
currentStep: number; currentStep: number;
@@ -14,96 +16,132 @@ type StepProgressBarProps = {
onStepPress?: (stepIndex: number) => void; onStepPress?: (stepIndex: number) => void;
}; };
const STEP_LABELS = ['Info', 'Register', 'KYC', 'Address', 'Docs'];
const StepProgressBar: React.FC<StepProgressBarProps> = ({ const StepProgressBar: React.FC<StepProgressBarProps> = ({
currentStep, currentStep,
steps, steps,
onStepPress = () => { }, onStepPress = () => { },
}) => { }) => {
const animations = useRef<Animated.Value[]>( const { colors, spacing, typography, shadows } = useTheme();
steps.map(() => new Animated.Value(1)) const styles = useMemo(() => makeStyles(colors, spacing), [colors, spacing]);
).current; const totalSteps = steps.length;
useEffect(() => {
animations.forEach((anim, index) => {
Animated.timing(anim, {
toValue: index === currentStep ? 1.2 : 1,
duration: 300,
useNativeDriver: true,
}).start();
});
}, [currentStep]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
{steps.map((label, index) => { {/* Progress line background */}
<View style={styles.lineContainer}>
<View style={[styles.lineBackground, { backgroundColor: colors.border }]} />
{/* Animated fill line */}
<Animated.View
style={[
styles.lineFill,
{
backgroundColor: colors.primary,
width: `${(currentStep / (totalSteps - 1)) * 100}%`,
},
]}
/>
</View>
{/* Step circles */}
<View style={styles.stepsRow}>
{steps.map((_, index) => {
const isActive = index === currentStep; const isActive = index === currentStep;
const isCompleted = index < currentStep; const isCompleted = index < currentStep;
return ( return (
<TouchableOpacity <View key={index} style={styles.stepItem}>
key={index} <View
onPress={() => onStepPress(index)}
style={styles.stepContainer}
activeOpacity={0.8}
>
<Animated.View
style={[ style={[
styles.circle, styles.circle,
isCompleted && styles.completedCircle,
isActive && styles.activeCircle,
{ {
transform: [{ scale: animations[index] }], backgroundColor: isCompleted
? colors.primary
: isActive
? colors.primary
: colors.surface,
borderColor: isCompleted || isActive
? colors.primary
: colors.border,
transform: [{ scale: isActive ? 1.15 : 1 }],
}, },
]} isActive && { ...shadows.light },
/>
<Text
style={[
styles.label,
isCompleted && { color: '#4caf50' },
isActive && { color: '#2196f3' },
]} ]}
> >
{label} {isCompleted ? (
</Text> <Check size={14} color={colors.buttonText} strokeWidth={3} />
</TouchableOpacity> ) : (
<AppText
variant="caption"
color={isActive ? colors.buttonText : colors.textSecondary}
style={{ fontWeight: '800', fontSize: 11 }}
>
{index + 1}
</AppText>
)}
</View>
<AppText
variant="caption"
color={isActive ? colors.primary : isCompleted ? colors.primary : colors.textSecondary}
style={{
marginTop: 4,
fontWeight: isActive || isCompleted ? '700' : '500',
fontSize: typography.size.xs,
}}
>
{STEP_LABELS[index] || ''}
</AppText>
</View>
); );
})} })}
</View> </View>
</View>
); );
}; };
export default StepProgressBar; export default StepProgressBar;
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any) => StyleSheet.create({
container: { container: {
flexDirection: 'row', paddingVertical: spacing.m,
alignItems: 'center', paddingHorizontal: spacing.s,
paddingVertical: 20,
flexWrap: 'wrap',
justifyContent: 'center',
}, },
stepContainer: { lineContainer: {
position: 'absolute',
top: spacing.m + 14, // Center with circle
left: 40,
right: 40,
height: 3,
zIndex: 0,
},
lineBackground: {
position: 'absolute',
height: 3,
width: '100%',
borderRadius: 2,
},
lineFill: {
position: 'absolute',
height: 3,
borderRadius: 2,
},
stepsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
zIndex: 1,
},
stepItem: {
alignItems: 'center', alignItems: 'center',
width: 70, width: 52,
}, },
circle: { circle: {
width: 50, width: 28,
height: 5, height: 28,
borderRadius: 25, borderRadius: 14,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.primary, borderWidth: 2,
},
completedCircle: {
backgroundColor: '#4caf50',
},
activeCircle: {
backgroundColor: '#2196f3',
},
label: {
// marginTop: 6,
// fontSize: 12,
textAlign: 'center',
color: '#555',
}, },
}); });
@@ -1,14 +1,20 @@
import React, { ReactNode } from 'react'; import React, { ReactNode, useMemo } from 'react';
import { View, Text, Modal, ScrollView, TouchableOpacity, StyleSheet, Dimensions } from 'react-native'; import { View, Modal, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
import { CheckCircle, X } from 'lucide-react-native'; // Updated icons import { CheckCircle, X, FileText, Shield, Users, Brain, Lock, RefreshCw, Scale } from 'lucide-react-native';
import colors from '../../Constant/Color'; import Animated, { FadeInUp, FadeInDown, FadeIn } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
type ModalProps = { type ModalProps = {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;
onChecked: () => void onChecked: () => void;
} };
const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChecked }) => { const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChecked }) => {
const { colors, spacing, shadows, typography } = useTheme();
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
return ( return (
<Modal <Modal
animationType="slide" animationType="slide"
@@ -17,185 +23,282 @@ const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChe
onRequestClose={onClose} onRequestClose={onClose}
> >
<View style={styles.container}> <View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer}> {/* ═══ Header ═══ */}
<View style={styles.header}> <Animated.View
<Text style={styles.title}>Terms & Conditions</Text> entering={FadeInDown.duration(400).springify()}
<Text style={styles.subtitle}>Last updated: {new Date().toLocaleDateString()}</Text> style={styles.header}
>
<View style={styles.headerRow}>
<View style={{ flex: 1 }}>
<AppText variant="h3" color={colors.buttonText} style={{ fontWeight: '800' }}>
Terms & Conditions
</AppText>
<AppText variant="caption" color={`${colors.buttonText}CC`} style={{ marginTop: 4 }}>
Last updated: {new Date().toLocaleDateString()}
</AppText>
</View> </View>
<TouchableOpacity
onPress={onClose}
style={styles.closeButton}
activeOpacity={0.7}
>
<X size={20} color={colors.buttonText} />
</TouchableOpacity>
</View>
</Animated.View>
<View style={styles.content}> {/* ═══ Content ═══ */}
<Section title="1. Acceptance of Terms"> <ScrollView
<Text style={styles.paragraph}> contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<Section
title="1. Acceptance of Terms"
icon={<FileText size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={0}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
By accessing or using our services, you agree to be bound by these Terms and Conditions. By accessing or using our services, you agree to be bound by these Terms and Conditions.
If you do not agree with any part of these terms, you must not use our services. If you do not agree with any part of these terms, you must not use our services.
</Text> </AppText>
</Section> </Section>
<Section title="2. User Responsibilities"> <Section
<Text style={styles.paragraph}> title="2. User Responsibilities"
icon={<Users size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={1}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
You are responsible for maintaining the confidentiality of your account information and You are responsible for maintaining the confidentiality of your account information and
for all activities that occur under your account. You agree to notify us immediately for all activities that occur under your account. You agree to notify us immediately
of any unauthorized use of your account. of any unauthorized use of your account.
</Text> </AppText>
</Section> </Section>
<Section title="3. Intellectual Property"> <Section
<Text style={styles.paragraph}> title="3. Intellectual Property"
icon={<Brain size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={2}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
All content included on our platform, such as text, graphics, logos, and software, is All content included on our platform, such as text, graphics, logos, and software, is
the property of our company and protected by intellectual property laws. You may not the property of our company and protected by intellectual property laws. You may not
reproduce, distribute, or create derivative works without our express permission. reproduce, distribute, or create derivative works without our express permission.
</Text> </AppText>
</Section> </Section>
<Section title="4. Limitation of Liability"> <Section
<Text style={styles.paragraph}> title="4. Limitation of Liability"
icon={<Shield size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={3}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
Our company shall not be liable for any indirect, incidental, special, or consequential Our company shall not be liable for any indirect, incidental, special, or consequential
damages resulting from the use or inability to use our services, even if we have been damages resulting from the use or inability to use our services, even if we have been
advised of the possibility of such damages. advised of the possibility of such damages.
</Text> </AppText>
</Section> </Section>
<Section title="5. Privacy Policy"> <Section
<Text style={styles.paragraph}> title="5. Privacy Policy"
icon={<Lock size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={4}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
Your use of our services is also governed by our Privacy Policy, which explains how we Your use of our services is also governed by our Privacy Policy, which explains how we
collect, use, and protect your personal information. Please review our Privacy Policy collect, use, and protect your personal information. Please review our Privacy Policy
to understand our practices. to understand our practices.
</Text> </AppText>
</Section> </Section>
<Section title="6. Changes to Terms"> <Section
<Text style={styles.paragraph}> title="6. Changes to Terms"
icon={<RefreshCw size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={5}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
We reserve the right to modify these terms at any time. We will notify you of any changes We reserve the right to modify these terms at any time. We will notify you of any changes
by posting the new Terms and Conditions on this page. Your continued use of the service by posting the new Terms and Conditions on this page. Your continued use of the service
after such modifications constitutes your acceptance of the new terms. after such modifications constitutes your acceptance of the new terms.
</Text> </AppText>
</Section> </Section>
<Section title="7. Governing Law"> <Section
<Text style={styles.paragraph}> title="7. Governing Law"
icon={<Scale size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={6}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
These Terms shall be governed by and construed in accordance with the laws of the These Terms shall be governed by and construed in accordance with the laws of the
jurisdiction in which our company is registered, without regard to its conflict of law provisions. jurisdiction in which our company is registered, without regard to its conflict of law provisions.
</Text> </AppText>
</Section> </Section>
<Text style={styles.footerText}> <Animated.View entering={FadeInUp.delay(500).springify()}>
<AppText variant="caption" color={colors.textSecondary} style={styles.footerText}>
If you have any questions about these Terms, please contact us at legal@company.com. If you have any questions about these Terms, please contact us at legal@company.com.
</Text> </AppText>
</View> </Animated.View>
{/* Accept Button */} {/* Spacer for button */}
<View style={{ height: 100 }} />
</ScrollView>
{/* ═══ Accept Button ═══ */}
<Animated.View
entering={FadeInDown.delay(300).springify()}
style={styles.acceptButtonWrapper}
>
<TouchableOpacity <TouchableOpacity
style={styles.acceptButton} style={styles.acceptButton}
onPress={() => { onPress={() => {
onClose(); onClose();
onChecked(); onChecked();
}} }}
activeOpacity={0.85}
> >
<CheckCircle color={colors.white} size={28} /> <CheckCircle color={colors.buttonText} size={22} strokeWidth={2.5} />
<Text style={styles.acceptButtonText}>I Accept</Text> <AppText variant="button" color={colors.buttonText} style={{ marginLeft: 10 }}>
I Accept
</AppText>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </Animated.View>
</View> </View>
</Modal> </Modal>
); );
}; };
// Reusable Section component
// ── Section sub-component ─────────────────────────────────────────────
type SectionProps = { type SectionProps = {
title: string; title: string;
icon: ReactNode;
children: ReactNode; children: ReactNode;
styles: any;
colors: any;
spacing: any;
index: number;
}; };
const Section: React.FC<SectionProps> = ({ title, children }) => ( const Section: React.FC<SectionProps> = ({ title, icon, children, styles, colors, spacing, index }) => (
<View style={styles.section}> <Animated.View
<Text style={styles.sectionTitle}>{title}</Text> entering={FadeInUp.delay(100 + index * 60).springify()}
{children} style={styles.section}
>
<View style={styles.sectionHeader}>
{icon}
<AppText variant="bodySmall" color={colors.text} style={styles.sectionTitle}>
{title}
</AppText>
</View> </View>
{children}
</Animated.View>
); );
const styles = StyleSheet.create({
export default TermsAndConditionsModal;
// ── Styles ────────────────────────────────────────────────────────────
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.lightGray, // Updated with your colors backgroundColor: colors.background,
},
scrollContainer: {
paddingBottom: 120, // Space for accept button
flexGrow: 1,
}, },
header: { header: {
backgroundColor: colors.primary, // Orange header backgroundColor: colors.primary,
paddingVertical: 32, paddingTop: spacing.xl,
paddingHorizontal: 24, paddingBottom: spacing.l,
borderBottomLeftRadius: 24, paddingHorizontal: spacing.l,
borderBottomRightRadius: 24, borderBottomLeftRadius: 20,
marginBottom: 24, borderBottomRightRadius: 20,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.25,
shadowRadius: 12,
elevation: 8,
}, },
title: { headerRow: {
fontSize: 28, flexDirection: 'row',
fontWeight: '800', alignItems: 'flex-start',
color: colors.white, justifyContent: 'space-between',
marginBottom: 4,
}, },
subtitle: { closeButton: {
fontSize: 14, width: 36,
color: 'rgba(255,255,255,0.85)', height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255,255,255,0.2)',
justifyContent: 'center',
alignItems: 'center',
}, },
content: { scrollContainer: {
paddingHorizontal: 24, padding: spacing.l,
paddingBottom: 40, paddingBottom: 40,
}, },
section: { section: {
marginBottom: 28, backgroundColor: colors.surface,
borderRadius: 14,
padding: spacing.m,
marginBottom: spacing.m,
borderWidth: 1,
borderColor: colors.border,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: spacing.s,
paddingBottom: spacing.s,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
}, },
sectionTitle: { sectionTitle: {
fontSize: 20,
fontWeight: '700', fontWeight: '700',
color: colors.darkText, fontSize: typography.size.s,
marginBottom: 12,
}, },
paragraph: { paragraph: {
fontSize: 16,
lineHeight: 25,
color: colors.grayText,
},
footerText: {
fontSize: 15,
fontStyle: 'italic',
color: colors.lightText,
marginTop: 24,
textAlign: 'center',
lineHeight: 22, lineHeight: 22,
}, },
acceptButton: { footerText: {
fontStyle: 'italic',
textAlign: 'center',
lineHeight: 20,
marginTop: spacing.m,
},
acceptButtonWrapper: {
position: 'absolute', position: 'absolute',
bottom: 40, bottom: 0,
left: 24, left: 0,
right: 24, right: 0,
padding: spacing.m,
paddingBottom: spacing.xl,
backgroundColor: colors.surface,
borderTopWidth: 1,
borderTopColor: colors.border,
},
acceptButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
borderRadius: 16, borderRadius: 14,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 18, paddingVertical: 16,
paddingHorizontal: 20,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 16,
elevation: 12,
},
acceptButtonText: {
color: colors.white,
fontSize: 18,
fontWeight: '700',
marginLeft: 12,
letterSpacing: 0.5,
}, },
}); });
export default TermsAndConditionsModal;
+7 -1
View File
@@ -6,6 +6,7 @@ import {
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
Pressable, Pressable,
ActivityIndicator,
} from 'react-native'; } from 'react-native';
import colors from '../Color'; import colors from '../Color';
@@ -17,6 +18,7 @@ type ConfirmDialogProps = {
cancelText?: string; cancelText?: string;
onConfirm: () => void; onConfirm: () => void;
onClose: () => void; onClose: () => void;
loading:boolean
}; };
const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
@@ -27,6 +29,7 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
cancelText = 'Cancel', cancelText = 'Cancel',
onConfirm, onConfirm,
onClose, onClose,
loading,
}) => { }) => {
return ( return (
<Modal <Modal
@@ -59,7 +62,10 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
onClose(); onClose();
}} }}
> >
<Text style={styles.confirmText}>{confirmText}</Text> { !loading ?<Text style={styles.confirmText}>{confirmText}</Text>:<ActivityIndicator
color={colors.primary}
size={30}
/>}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</Pressable> </Pressable>
+8 -1
View File
@@ -5,6 +5,9 @@ import Diamond from '../Images/diamond.svg'
import Hill from '../Images/Svg/Hill.svg' import Hill from '../Images/Svg/Hill.svg'
const Logo = require('../Images/logo.png') const Logo = require('../Images/logo.png')
const Logo2 = require('../Images/logo2.png') const Logo2 = require('../Images/logo2.png')
const Icon = { const Icon = {
Logo, Logo,
Logo2 Logo2
@@ -15,7 +18,11 @@ const ImagePath = {
OnBoard2, OnBoard2,
OnBoard3, OnBoard3,
Diamond, Diamond,
Hill Hill,
DarkBg: require('../Images/Backgrounds/darkBackground.png'),
LightBg: require('../Images/Backgrounds/lightBackground.png'),
DarkBg2: require('../Images/Backgrounds/darkBackground2.png'),
LightBg2: require('../Images/Backgrounds/lightBackground2.png'),
} }
export default ImagePath; export default ImagePath;
+6 -2
View File
@@ -21,7 +21,11 @@ const NavigationString = {
LiveTenderDetail:'LiveTenderDetail', LiveTenderDetail:'LiveTenderDetail',
AuctionDetail: 'AuctionDetail', AuctionDetail: 'AuctionDetail',
Profile:'Profile', Profile:'Profile',
ViewTenderDetail:'ViewTenderDetail', ResetPassword:'ResetPassword',
ResetPassword:'ResetPassword' KycWebViewScreen:'KycWebViewScreen',
StageThreeScreen:'StageThree',
DecryptionScreen:'DecryptionScreen',
DeviceBinding: 'DeviceBinding',
DscBinding: 'DscBinding'
} }
export default NavigationString export default NavigationString
+15
View File
@@ -0,0 +1,15 @@
export const generateTransactionId = () => {
const now = new Date();
const dateTime =
now.getFullYear().toString() +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0');
const randomPart = Math.floor(1000 + Math.random() * 9000); // 4-digit random
return `TNX${dateTime}${randomPart}`;
};
+20
View File
@@ -0,0 +1,20 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useCreate = <T, V>(
url: string,
options?: UseMutationOptions<T, Error, V>
) => {
const queryClient = useQueryClient();
return useMutation<T, Error, V>({
mutationFn: async (variables: V) => {
const { data } = await apiClient.post<T>(url, variables);
return data;
},
onSuccess: () => {
// Invalidate queries if needed, handled by component or passed in options
},
...options,
});
};
+17
View File
@@ -0,0 +1,17 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useDelete = <T>(
url: string,
options?: UseMutationOptions<T, Error, void>
) => {
const queryClient = useQueryClient();
return useMutation<T, Error, void>({
mutationFn: async () => {
const { data } = await apiClient.delete<T>(url);
return data;
},
...options,
});
};
+17
View File
@@ -0,0 +1,17 @@
import { useQuery, UseQueryOptions, QueryKey } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useFetch = <T>(
key: QueryKey,
url: string,
options?: UseQueryOptions<T, Error>
) => {
return useQuery<T, Error>({
queryKey: key,
queryFn: async () => {
const { data } = await apiClient.get<T>(url);
return data;
},
...options,
});
};
+32
View File
@@ -0,0 +1,32 @@
import { useColorScheme } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { lightColors, darkColors, spacing, typography, shadows } from '../theme';
import { selectThemeMode, setThemeMode } from '../Redux/Slices/ThemeSlice';
export const useTheme = () => {
const systemScheme = useColorScheme();
const themeMode = useSelector(selectThemeMode);
const dispatch = useDispatch();
const isDarkMode =
themeMode === 'system'
? systemScheme === 'dark'
: themeMode === 'dark';
const themeColors = isDarkMode ? darkColors : lightColors;
const toggleTheme = (mode: 'light' | 'dark' | 'system') => {
dispatch(setThemeMode(mode));
};
return {
colors: themeColors,
isDarkMode,
themeMode,
toggleTheme,
spacing,
typography,
shadows,
};
};
+17
View File
@@ -0,0 +1,17 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useUpdate = <T, V>(
url: string,
options?: UseMutationOptions<T, Error, V>
) => {
const queryClient = useQueryClient();
return useMutation<T, Error, V>({
mutationFn: async (variables: V) => {
const { data } = await apiClient.put<T>(url, variables);
return data;
},
...options,
});
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+156 -81
View File
@@ -1,124 +1,173 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect } from 'react';
import { import {
View, View,
Text, Text,
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
Dimensions, Dimensions,
Animated, Platform,
} from 'react-native'; } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import * as LucideIcons from 'lucide-react-native'; import * as LucideIcons from 'lucide-react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
interpolate,
Extrapolation
} from 'react-native-reanimated';
import Screens from '../Screens'; import Screens from '../Screens';
import colors from '../Constant/Color';
import NavigationString from '../Constant/NavigationString'; import NavigationString from '../Constant/NavigationString';
import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview'; import { useTheme } from '../Hooks/useTheme';
import { typography } from '../theme/typography';
// import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const TAB_BAR_HEIGHT = 60; const TAB_BAR_HEIGHT = 70; // Slightly taller for premium feel
const TAB_BUTTON_WIDTH = width / 4; const TAB_BUTTON_WIDTH = width / 4;
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
const TabButton = ({ route, isFocused, onPress }: any) => { const TabButton = ({ route, isFocused, onPress }: any) => {
const icons: Record<string, { outline: any; filled: any }> = { const { colors } = useTheme();
Home: { outline: "Home", filled: "Home" }, const icons: Record<string, { outline: keyof typeof LucideIcons; filled: keyof typeof LucideIcons }> = {
Home: { outline: "House", filled: "House" },
Auction: { outline: "Gavel", filled: "Gavel" }, Auction: { outline: "Gavel", filled: "Gavel" },
Tender: { outline: "FileText", filled: "FileText" }, Tender: { outline: "FileText", filled: "FileText" },
Profile: { outline: "User", filled: "User" }, Profile: { outline: "User", filled: "User" },
}; };
const IconName = LucideIcons[isFocused ? icons[route.name].filled : icons[route.name].outline]; // Lucide icons don't always have 'filled' variants in the set, so we might use the same icon
const scaleAnim = useRef(new Animated.Value(1)).current; // or rely on color/strokeWidth to indicate state.
const translateYAnim = useRef(new Animated.Value(0)).current; const iconKey = icons[route.name]?.outline || "HelpCircle"; // Fallback
const opacityAnim = useRef(new Animated.Value(1)).current; const IconName = LucideIcons[iconKey] as React.ElementType;
const scale = useSharedValue(0);
const translateY = useSharedValue(0);
useEffect(() => { useEffect(() => {
if (isFocused) { if (isFocused) {
Animated.parallel([ scale.value = withSpring(1, { damping: 12, stiffness: 100 });
Animated.sequence([ translateY.value = withSpring(-4, { damping: 12, stiffness: 100 });
Animated.timing(translateYAnim, {
toValue: -8,
duration: 120,
useNativeDriver: true,
}),
Animated.spring(translateYAnim, {
toValue: 0,
friction: 3,
tension: 80,
useNativeDriver: true,
}),
]),
Animated.spring(scaleAnim, {
toValue: 1.3,
friction: 5,
tension: 150,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
]).start();
} else { } else {
Animated.parallel([ scale.value = withTiming(0, { duration: 200 });
Animated.spring(scaleAnim, { translateY.value = withSpring(0, { damping: 12, stiffness: 100 });
toValue: 1,
friction: 5,
tension: 150,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0.6,
duration: 150,
useNativeDriver: true,
}),
]).start();
} }
}, [isFocused]); }, [isFocused]);
const animatedPillStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
opacity: interpolate(scale.value, [0, 1], [0, 1], Extrapolation.CLAMP),
};
});
const animatedIconStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: interpolate(scale.value, [0, 1], [1, 0.9]) }], // Slight shrink on press
};
});
const animatedTextStyle = useAnimatedStyle(() => {
const opacity = interpolate(scale.value, [0, 1], [0, 1]);
const translateX = interpolate(scale.value, [0, 1], [20, 0]); // Slide in from right
return {
opacity,
transform: [{ translateX }],
display: scale.value === 0 ? 'none' : 'flex', // Hide when inactive
marginLeft: 4, // Reduced space
};
});
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={onPress} onPress={onPress}
style={[styles.tabButton, { width: TAB_BUTTON_WIDTH }]} style={styles.tabButton}
activeOpacity={0.8} activeOpacity={0.8}
> >
<Animated.View <View style={styles.tabContent}>
style={[ <Animated.View style={animatedIconStyle}>
styles.iconContainer,
{
transform: [{ scale: scaleAnim }, { translateY: translateYAnim }],
opacity: opacityAnim,
},
]}
>
{IconName && ( {IconName && (
<IconName <IconName
size={20} size={24}
color={isFocused ? colors.primary : '#888'} color={isFocused ? colors.primary : colors.text}
strokeWidth={2} strokeWidth={isFocused ? 2.5 : 2}
/> />
)} )}
</Animated.View> </Animated.View>
<Text {isFocused && (
<Animated.Text
style={[ style={[
styles.label, styles.label,
animatedTextStyle,
{ {
color: isFocused ? colors.primary : '#888', color: colors.primary,
fontWeight: isFocused ? '600' : '400', fontFamily: typography.fonts.medium,
fontWeight: undefined, // Remove fontWeight as we use fontFamily
}, },
]} ]}
numberOfLines={1}
> >
{route.name === 'BrandDay' ? 'Coupons' : route.name} {route.name === 'BrandDay' ? 'Coupons' : route.name}
</Text> </Animated.Text>
)}
</View>
</TouchableOpacity> </TouchableOpacity>
); );
}; };
const CustomTabBar = ({ state, descriptors, navigation }: any) => { const CustomTabBar = ({ state, descriptors, navigation }: any) => {
const { colors, shadows } = useTheme();
const { width: windowWidth } = Dimensions.get('window');
const TAB_BAR_WIDTH = windowWidth - 40; // 20 margin each side
const TAB_PADDING_H = 10; // 5 padding left + 5 padding right
const TAB_WIDTH = (TAB_BAR_WIDTH - TAB_PADDING_H) / state.routes.length;
const translateX = useSharedValue(0);
useEffect(() => {
translateX.value = withSpring(state.index * TAB_WIDTH, {
damping: 15,
stiffness: 150,
mass: 0.5,
});
}, [state.index]);
const animatedIndicatorStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }],
};
});
return ( return (
<View style={styles.tabBar}> <View style={[
styles.tabBar,
{
backgroundColor: colors.background, // Use transparent to allow shadow to show
shadowColor: colors.shadow, // Dynamic shadow color
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.2, // Consistent opacity
shadowRadius: 10,
borderColor: colors.border, // Subtle border for definition
borderWidth: 3,
elevation: 20,
}
]}>
<Animated.View
style={[
styles.slidingIndicator,
{
width: TAB_WIDTH,
backgroundColor: colors.primary,
opacity: 0.3,
elevation: 10, // Subtle elevation for the indicator
},
animatedIndicatorStyle,
]}
/>
{state.routes.map((route: any, index: number) => { {state.routes.map((route: any, index: number) => {
const isFocused = state.index === index; const isFocused = state.index === index;
@@ -136,8 +185,9 @@ const CustomTabBar = ({ state, descriptors, navigation }: any) => {
}; };
export default function BottomTab() { export default function BottomTab() {
const { colors } = useTheme();
return ( return (
<View style={styles.container}> <View style={[styles.container, { backgroundColor: colors.background }]}>
<Tab.Navigator <Tab.Navigator
tabBar={(props) => <CustomTabBar {...props} />} tabBar={(props) => <CustomTabBar {...props} />}
screenOptions={{ screenOptions={{
@@ -159,29 +209,54 @@ export default function BottomTab() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#fff',
}, },
tabBar: { tabBar: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
flexDirection: 'row', flexDirection: 'row',
height: TAB_BAR_HEIGHT, height: 70,
backgroundColor: '#fff', borderRadius: 35,
borderTopWidth: 1, elevation: 20, // stronger Android elevation
borderTopColor: '#eee', shadowColor: '#000', // iOS shadow color
elevation: 5, shadowOffset: { width: 0, height: 10 },
shadowOpacity: Platform.OS === 'ios' ? 0.25 : 0.2,
shadowRadius: 12,
overflow: 'visible', // allow shadow to render outside rounded corners
zIndex: 9999,
paddingBottom: 0,
justifyContent: 'space-around',
alignItems: 'center',
paddingHorizontal: 5,
}, },
tabButton: { tabButton: {
flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
height: '100%', height: '100%',
}, },
iconContainer: { tabContent: {
marginBottom: 4, flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 4, // Drastically reduced from 12
paddingVertical: 4,
borderRadius: 20,
},
slidingIndicator: {
position: 'absolute',
top: 12,
left: 5,
height: 40,
borderRadius: 30,
zIndex: 0,
}, },
label: { label: {
fontSize: 11, fontSize: 11, // Slightly smaller text
textAlign: 'center', textAlign: 'left',
includeFontPadding: false, textTransform: 'capitalize',
letterSpacing: 0.2, // Tighter spacing
}, },
}); });
+100
View File
@@ -0,0 +1,100 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, Switch, Image } from 'react-native';
import { DrawerContentScrollView, DrawerItemList } from '@react-navigation/drawer';
import { useTheme } from '../Hooks/useTheme';
import AppText from '../components/AppText';
import { Moon, Sun, Monitor } from 'lucide-react-native';
import ImagePath from '../Constant/ImagePath';
const CustomDrawer = (props: any) => {
const { colors, isDarkMode, themeMode, toggleTheme } = useTheme();
const renderThemeOption = (mode: 'light' | 'dark' | 'system', label: string, Icon: any) => {
const isActive = themeMode === mode;
return (
<TouchableOpacity
style={[
styles.themeOption,
isActive && { backgroundColor: colors.primary + '20' } // 20% opacity
]}
onPress={() => toggleTheme(mode)}
>
<Icon size={20} color={isActive ? colors.primary : colors.textSecondary} />
<AppText variant="medium" style={{ marginLeft: 10, color: isActive ? colors.primary : colors.textSecondary }}>
{label}
</AppText>
</TouchableOpacity>
);
};
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<DrawerContentScrollView {...props} contentContainerStyle={{ paddingTop: 0 }}>
{/* Header */}
<View style={[styles.header, { backgroundColor: colors.surface, borderBottomColor: colors.border }]}>
<Image source={ImagePath.Icon.Logo} style={styles.logo} resizeMode="contain" />
<AppText variant="bold" size={20} style={{ color: colors.text }}>ProcureLinc</AppText>
<AppText variant="regular" size={12} style={{ color: colors.textSecondary }}>Smart Procurement</AppText>
</View>
<View style={{ flex: 1, paddingTop: 10 }}>
<DrawerItemList {...props} />
</View>
{/* Theme Switch Section */}
<View style={[styles.section, { borderTopColor: colors.border }]}>
<AppText variant="bold" style={{ marginLeft: 16, marginBottom: 10, color: colors.text }}>Running Theme</AppText>
<View style={styles.themeRow}>
{renderThemeOption('light', 'Light', Sun)}
{renderThemeOption('dark', 'Dark', Moon)}
{renderThemeOption('system', 'System', Monitor)}
</View>
</View>
</DrawerContentScrollView>
<View style={[styles.footer, { borderTopColor: colors.border }]}>
<AppText variant="regular" size={12} style={{ color: colors.textSecondary, textAlign: 'center' }}>
Version 1.0.0
</AppText>
</View>
</View>
);
};
const styles = StyleSheet.create({
header: {
padding: 20,
paddingTop: 50, // safe area approximation
alignItems: 'center',
borderBottomWidth: 1,
},
logo: {
width: 80,
height: 80,
borderRadius: 40,
marginBottom: 10,
},
section: {
padding: 10,
borderTopWidth: 1,
marginTop: 'auto',
marginBottom: 20
},
themeRow: {
flexDirection: 'row',
justifyContent: 'space-around',
},
themeOption: {
alignItems: 'center',
justifyContent: 'center',
padding: 10,
borderRadius: 12,
flexDirection: 'row'
},
footer: {
padding: 20,
borderTopWidth: 1,
}
});
export default CustomDrawer;
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import { createDrawerNavigator } from '@react-navigation/drawer';
import BottomTab from './Bottom';
import CustomDrawer from './CustomDrawer';
import { useTheme } from '../Hooks/useTheme';
import NavigationString from '../Constant/NavigationString';
import { Dimensions } from 'react-native';
const Drawer = createDrawerNavigator();
const { width } = Dimensions.get('window');
const DrawerStack = () => {
const { colors } = useTheme();
return (
<Drawer.Navigator
drawerContent={(props) => <CustomDrawer {...props} />}
screenOptions={{
headerShown: false,
drawerType: 'slide',
drawerStyle: {
backgroundColor: colors.background,
width: width * 0.75,
},
drawerActiveTintColor: colors.primary,
drawerInactiveTintColor: colors.textSecondary,
overlayColor: 'rgba(0,0,0,0.5)',
}}
>
<Drawer.Screen
name="MainTabs"
component={BottomTab}
options={{ title: 'Home' }}
/>
{/* Add other drawer screens here that are NOT in the bottom tab */}
</Drawer.Navigator>
);
};
export default DrawerStack;
+9 -19
View File
@@ -4,35 +4,20 @@ import NavigationString from '../Constant/NavigationString';
import Screens from '../Screens'; import Screens from '../Screens';
import colors from '../Constant/Color'; import colors from '../Constant/Color';
import BottomTab from './Bottom'; import BottomTab from './Bottom';
import Login from '../Screens/Login/Login';
export default function MainStack() { export default function MainStack() {
const Stack = createStackNavigator(); const Stack = createStackNavigator();
const option = { headerShown: false }; const option = { headerShown: false };
const fadeTransition = { // ...
gestureDirection: 'horizontal',
transitionSpec: {
open: { animation: 'timing', config: { duration: 300 } },
close: { animation: 'timing', config: { duration: 300 } },
},
cardStyleInterpolator: ({ current }) => ({
cardStyle: {
opacity: current.progress,
},
}),
};
return ( return (
<Stack.Navigator <Stack.Navigator
screenOptions={{ // ...
cardStyle: { backgroundColor: colors.white },
...fadeTransition
}}
> >
<Stack.Screen name={NavigationString.Splash} component={Screens.Splash} options={option} /> <Stack.Screen name={NavigationString.Splash} component={Screens.Splash} options={option} />
<Stack.Screen name={NavigationString.Signup} component={Screens.Signup} options={option} /> <Stack.Screen name={NavigationString.Signup} component={Screens.Signup} options={option} />
<Stack.Screen name={NavigationString.Onboarding} component={Screens.Onboarding} options={option} /> <Stack.Screen name={NavigationString.Onboarding} component={Screens.Onboarding} options={option} />
<Stack.Screen name={NavigationString.Login} component={Screens.Login} options={option} /> <Stack.Screen name={NavigationString.Login} component={Login} options={option} />
<Stack.Screen name={NavigationString.Home} component={BottomTab} options={option} /> <Stack.Screen name={NavigationString.Home} component={BottomTab} options={option} />
<Stack.Screen name={NavigationString.PurchaseList} component={Screens.PurchaseList} options={option} /> <Stack.Screen name={NavigationString.PurchaseList} component={Screens.PurchaseList} options={option} />
<Stack.Screen name={NavigationString.Emd} component={Screens.Emd} options={option} /> <Stack.Screen name={NavigationString.Emd} component={Screens.Emd} options={option} />
@@ -47,6 +32,11 @@ export default function MainStack() {
<Stack.Screen name={NavigationString.Partnership} component={Screens.Partnership} options={option} /> <Stack.Screen name={NavigationString.Partnership} component={Screens.Partnership} options={option} />
<Stack.Screen name={NavigationString.LiveTenderDetail} component={Screens.LiveTenderDetail} options={option} /> <Stack.Screen name={NavigationString.LiveTenderDetail} component={Screens.LiveTenderDetail} options={option} />
<Stack.Screen name={NavigationString.ResetPassword} component={Screens.ResetPassword} options={option} /> <Stack.Screen name={NavigationString.ResetPassword} component={Screens.ResetPassword} options={option} />
<Stack.Screen name={NavigationString.KycWebViewScreen} component={Screens.KycWebViewScreen} options={option} />
<Stack.Screen name={NavigationString.StageThreeScreen} component={Screens.StageThreeScreen} options={option} />
<Stack.Screen name={NavigationString.DecryptionScreen} component={Screens.DecryptionScreen} options={option} />
<Stack.Screen name={NavigationString.DeviceBinding} component={Screens.DeviceBinding} options={option} />
<Stack.Screen name={NavigationString.DscBinding} component={Screens.DscBinding} options={option} />
</Stack.Navigator> </Stack.Navigator>
); );
} }
+20
View File
@@ -0,0 +1,20 @@
import { createNavigationContainerRef, CommonActions } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef<any>();
export function navigate(name: string, params?: any) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
}
}
export function resetAndNavigate(name: string) {
if (navigationRef.isReady()) {
navigationRef.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name }],
})
);
}
}
+27
View File
@@ -0,0 +1,27 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type ThemeMode = 'light' | 'dark' | 'system';
interface ThemeState {
mode: ThemeMode;
}
const initialState: ThemeState = {
mode: 'system',
};
export const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
setThemeMode: (state, action: PayloadAction<ThemeMode>) => {
state.mode = action.payload;
},
},
});
export const { setThemeMode } = themeSlice.actions;
export const selectThemeMode = (state: any) => state.theme.mode;
export default themeSlice.reducer;
+1
View File
@@ -17,4 +17,5 @@ export const tokenSlice = createSlice({
}, },
}); });
export const { setToken, removeToken } = tokenSlice.actions; export const { setToken, removeToken } = tokenSlice.actions;
export const getToken = (state: any) => state.token.token;
export default tokenSlice.reducer; export default tokenSlice.reducer;
+3 -1
View File
@@ -7,6 +7,7 @@ import {auctionSlice} from './Slices/SelectedAuction';
import selectPurchaseAuctionReducer from './Slices/selectPurchaseAuction' import selectPurchaseAuctionReducer from './Slices/selectPurchaseAuction'
import selectPurchaseTenderReducer from './Slices/SelectedPurchaseTender'; import selectPurchaseTenderReducer from './Slices/SelectedPurchaseTender';
import { tenderSlice } from './Slices/SelectedTender'; import { tenderSlice } from './Slices/SelectedTender';
import { themeSlice } from './Slices/ThemeSlice';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
token: tokenSlice.reducer, token: tokenSlice.reducer,
@@ -14,11 +15,12 @@ const rootReducer = combineReducers({
selectPurchaseAuction: selectPurchaseAuctionReducer, selectPurchaseAuction: selectPurchaseAuctionReducer,
selectPurchaseTender: selectPurchaseTenderReducer, selectPurchaseTender: selectPurchaseTenderReducer,
tender: tenderSlice.reducer, tender: tenderSlice.reducer,
theme: themeSlice.reducer,
}); });
const persistConfig = { const persistConfig = {
key: 'root', key: 'root',
storage: AsyncStorage, storage: AsyncStorage,
whitelist: ['token'], whitelist: ['token', 'theme'],
}; };
const persistReducers = persistReducer(persistConfig, rootReducer); const persistReducers = persistReducer(persistConfig, rootReducer);
export const store = configureStore({ export const store = configureStore({
+198 -87
View File
@@ -1,26 +1,34 @@
import React, { useState, useRef, useMemo } from 'react'; import React, { useState, useRef, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Animated, Dimensions } from 'react-native'; import {
View,
StyleSheet,
TouchableOpacity,
Animated,
Dimensions,
} from 'react-native';
import { InsideHeader } from '../../Component/Index'; import { InsideHeader } from '../../Component/Index';
import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/BBPS/Hooks'; import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
import colors from '../../Constant/Color';
import LiveAuction from './LiveAuction'; import LiveAuction from './LiveAuction';
import PurchaseAuction from './PurchaseAuction'; import PurchaseAuction from './PurchaseAuction';
import { ShoppingCart } from 'lucide-react-native'; import { ShoppingCart, Lock, Radio, Package } from 'lucide-react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import FontsFamily from '../../Constant/FontsFamily';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
const HORIZONTAL_PADDING = 32; const HORIZONTAL_PADDING = 32;
const SWITCH_INSETS = 2; const SWITCH_INSETS = 3;
const TAB_HEIGHT = 44; const TAB_HEIGHT = 48;
const Auction = () => { const Auction = () => {
const { colors, spacing, shadows } = useTheme();
const Navigation = useNavigation() as any; const Navigation = useNavigation() as any;
const { data: liveAuctionData = {} }: any = useFetchLiveAuction(); const { data: liveAuctionData = {} }: any = useFetchLiveAuction();
const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction(); const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction();
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const reduxToken = useSelector((state: any) => state.token?.token);
const liveCount = Array.isArray(liveAuctionData?.result) const liveCount = Array.isArray(liveAuctionData?.result)
? liveAuctionData?.pages?.[0]?.totalCount ? liveAuctionData?.pages?.[0]?.totalCount
@@ -30,15 +38,14 @@ const Auction = () => {
? purchaseAuctionData.data.length ? purchaseAuctionData.data.length
: 0; : 0;
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('live');
const translateX = useRef(new Animated.Value(0)).current;
const TAB_WIDTH = useMemo(() => { const TAB_WIDTH = useMemo(() => {
const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2; const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2;
return availableWidth / 2.4; return availableWidth / 2;
}, []); }, []);
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('purchase');
const translateX = useRef(new Animated.Value(TAB_WIDTH)).current;
const onTabPress = (tab: 'live' | 'purchase', index: number) => { const onTabPress = (tab: 'live' | 'purchase', index: number) => {
setSelectedTab(tab); setSelectedTab(tab);
Animated.spring(translateX, { Animated.spring(translateX, {
@@ -48,147 +55,251 @@ const Auction = () => {
}).start(); }).start();
}; };
const cartCount = SelectedPurchaseList.length;
const styles = makeStyles(colors, spacing, shadows, TAB_WIDTH, TAB_HEIGHT);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<InsideHeader title="Auction" showArrow /> <InsideHeader title="Auction" showArrow />
{/* Tab Row */}
<View style={styles.topRow}> <View style={styles.topRow}>
{/* Segmented Control */}
<View <View
style={[ style={[
styles.switchWrapper, styles.switchWrapper,
{ { width: TAB_WIDTH * 2 + SWITCH_INSETS * 2 },
width: TAB_WIDTH * 2 + SWITCH_INSETS * 2,
},
]} ]}
> >
{/* Sliding pill */}
<Animated.View <Animated.View
pointerEvents="none" pointerEvents="none"
style={[ style={[
styles.activeTab, styles.activeTab,
{ { width: TAB_WIDTH, transform: [{ translateX }] },
width: TAB_WIDTH,
transform: [{ translateX }],
},
]} ]}
/> />
{/* Live Tab */}
<TouchableOpacity <TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]} style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8} activeOpacity={0.8}
onPress={() => onTabPress('live', 0)} onPress={() => onTabPress('live', 0)}
> >
<Text <Radio
size={13}
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
style={[ style={[
styles.tabText, styles.tabText,
selectedTab === 'live' && styles.activeText, selectedTab === 'live' && (styles.activeTabText as any),
]} ]}
> >
Live ({liveCount}) Live {liveCount ? `(${liveCount})` : ''}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
{/* Purchase Tab */}
<TouchableOpacity <TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]} style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8} activeOpacity={0.8}
onPress={() => onTabPress('purchase', 1)} onPress={() => onTabPress('purchase', 1)}
> >
<Text <Package
size={13}
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
style={[ style={[
styles.tabText, styles.tabText,
selectedTab === 'purchase' && styles.activeText, selectedTab === 'purchase' && (styles.activeTabText as any),
]} ]}
> >
Purchased ({purchaseCount}) Purchased {purchaseCount ? `(${purchaseCount})` : ''}
</Text> </AppText>
</TouchableOpacity>
</View>
<View>
{SelectedPurchaseList.length ? (
<View
style={{
borderWidth: 2,
borderColor: colors.white,
backgroundColor: colors.primaryLight,
position: 'absolute',
borderRadius: 50,
zIndex: 20,
right: 0,
top: -10,
paddingHorizontal: 2,
}}
>
<Text style={{ fontSize: 12, color: colors.black }}>
{SelectedPurchaseList.length}
</Text>
</View>
) : null}
<TouchableOpacity
style={{
padding: 10,
backgroundColor: colors.primaryLight,
borderRadius: 20,
}}
onPress={() => {
Navigation.navigate(NavigationString.PurchaseList);
}}
>
<ShoppingCart size={22} color={colors.black} strokeWidth={2} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
{/* Content */}
{selectedTab === 'live' ? <LiveAuction /> : <PurchaseAuction />} {selectedTab === 'live' ? <LiveAuction /> : <PurchaseAuction />}
{/* Cart Floating Button */}
<TouchableOpacity
style={styles.cartFloatingButton}
activeOpacity={0.9}
onPress={() => Navigation.navigate(NavigationString.PurchaseList)}
>
<ShoppingCart size={24} color={colors.buttonText} strokeWidth={2} />
{cartCount > 0 && (
<View style={styles.cartBadge}>
<AppText variant="caption" color={colors.primary} style={styles.cartBadgeText as any}>
{cartCount}
</AppText>
</View>
)}
</TouchableOpacity>
</View> </View>
); );
}; };
export default Auction; export default Auction;
const styles = StyleSheet.create({ /* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any, TAB_WIDTH: number, TAB_HEIGHT: number) =>
StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.white, backgroundColor: colors.background,
}, },
/* ── Tab Row ── */
topRow: { topRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 16, justifyContent: 'center',
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
}, },
switchWrapper: { switchWrapper: {
flexDirection: 'row', flexDirection: 'row',
marginVertical: 14, backgroundColor: colors.surface,
backgroundColor: '#f1f5f9', borderRadius: 16,
borderRadius: 18, padding: SWITCH_INSETS,
padding: 2,
height: TAB_HEIGHT, height: TAB_HEIGHT,
...shadows.light,
}, },
tab: { tab: {
height: TAB_HEIGHT - 4, height: TAB_HEIGHT - SWITCH_INSETS * 2,
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 5,
zIndex: 2, zIndex: 2,
}, },
tabText: { tabText: {
fontSize: 13, fontWeight: '600',
color: '#64748b', letterSpacing: 0.3,
lineHeight: 18, } as any,
fontFamily: FontsFamily.poppinsSemiBold, activeTabText: {
}, fontWeight: '700',
activeText: { } as any,
color: colors.primary,
},
activeTab: { activeTab: {
position: 'absolute', position: 'absolute',
top: 2, top: SWITCH_INSETS,
left: 2, left: SWITCH_INSETS,
height: TAB_HEIGHT - 4, height: TAB_HEIGHT - SWITCH_INSETS * 2,
backgroundColor: '#fff', backgroundColor: colors.background,
borderRadius: 16, borderRadius: 13,
shadowColor: '#000', ...shadows.medium,
shadowOpacity: 0.1,
shadowRadius: 6,
elevation: 3,
}, },
/* ── Cart Floating Button ── */
cartFloatingButton: {
position: 'absolute',
bottom: spacing.xl + 80,
right: spacing.l,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
...shadows.strong,
zIndex: 999,
},
cartBadge: {
position: 'absolute',
top: 0,
right: 0,
minWidth: 20,
height: 20,
borderRadius: 10,
backgroundColor: colors.background,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 4,
borderWidth: 1.5,
borderColor: colors.primary,
},
cartBadgeText: {
fontSize: 10,
fontWeight: '800',
lineHeight: 13,
} as any,
/* ── Restricted Access ── */
restrictedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
backgroundColor: colors.background,
},
restrictedCard: {
backgroundColor: colors.surface,
borderRadius: 28,
padding: spacing.l,
alignItems: 'center',
width: '100%',
maxWidth: 400,
...shadows.strong,
},
lockRing: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: `${colors.primary}12`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.l,
borderWidth: 1.5,
borderColor: `${colors.primary}25`,
},
lockInner: {
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: `${colors.primary}18`,
alignItems: 'center',
justifyContent: 'center',
},
restrictedTitle: {
marginBottom: spacing.s + 4,
} as any,
restrictedMessage: {
marginBottom: spacing.s,
lineHeight: 24,
} as any,
hintBox: {
backgroundColor: colors.overlay,
borderRadius: 14,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
marginBottom: spacing.l,
width: '100%',
},
loginButton: {
backgroundColor: colors.buttonPrimary,
paddingHorizontal: spacing.l,
paddingVertical: 14,
borderRadius: 16,
width: '100%',
alignItems: 'center',
...shadows.medium,
},
loginBtnText: {
letterSpacing: 1.2,
} as any,
}); });
+116 -90
View File
@@ -1,69 +1,76 @@
import React, { useCallback, useState, useEffect, useRef } from 'react'; import React, { useCallback, useState, useEffect, useRef } from 'react';
import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native'; import {
View,
FlatList,
ActivityIndicator,
RefreshControl,
StyleSheet,
} from 'react-native';
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
import { RefreshCw } from 'lucide-react-native';
import LiveAuctionTile from './LiveAuctionTile'; import LiveAuctionTile from './LiveAuctionTile';
import { useFetchLiveAuction } from '../../Services/BBPS/Hooks'; import { useFetchLiveAuction } from '../../Services/ApiCall/Hooks';
import { SearchInput } from '../../Component/Serach/SeacrhInput'; import { SearchInput } from '../../Component/Serach/SeacrhInput';
import { Skelton } from '../../Component/Index'; import { Skelton } from '../../Component/Index';
import colors from '../../Constant/Color'; import SkeltonList from '../../Component/Skelton/SkeltonList';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction'; import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { apiCall } from '../../Axios/Axios'; import { useTheme } from '../../Hooks/useTheme';
import { GET_MY_AUCTION } from '../../Services/BBPS/ApiUrls'; import AppText from '../../components/AppText';
import SkeltonList from '../../Component/Skelton/SkeltonList';
import Skeleton from '../../Component/Skelton/Skelton';
const LiveAuction = () => { const LiveAuction = () => {
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const user = useSelector((state: any) => state.token?.token);
const onEndReachedCalledDuringMomentum = useRef(true); const onEndReachedCalledDuringMomentum = useRef(true);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const [showRefreshBanner, setShowRefreshBanner] = useState(false); const [showRefreshBanner, setShowRefreshBanner] = useState(false);
const [allBoolean, setAllBoolean] = useState({ liveAuctionData: [], isLoading: false })
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch, isPending }: any = useFetchLiveAuction();
// My Methods const {
data,
const getMyAuction = async () => { fetchNextPage,
try { hasNextPage,
setAllBoolean(prev => ({ ...prev, isLoading: true })) isFetchingNextPage,
const res = await apiCall<any>('get', `${GET_MY_AUCTION}`,); isLoading,
if (res?.statusCode === 200) { isRefetching,
const purchasedIds = new Set(res.data.map((item: any) => item.auctionId)); refetch,
const filteredLive = data?.result.map((item: any) => ({ ...item, active: !purchasedIds.has(item.auctionId) })); isPending,
setAllBoolean(prev => ({ ...prev, liveAuctionData: filteredLive })); }: any = useFetchLiveAuction();
}
} catch (error) {
console.error('getMyAuction error', error);
} finally {
setAllBoolean(prev => ({ ...prev, isLoading: false }));
}
};
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: any) => { ({ item, index }: any) => {
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.auctionNumber === item.auctionNumber) const isPurchase = SelectedPurchaseList.findIndex(
return <LiveAuctionTile (prev: any) => prev.auctionNumber === item.auctionNumber,
);
return (
<LiveAuctionTile
data={item} data={item}
index={index} index={index}
isPurchasedByContractor={item.contractorId?.includes(
String(user.contractorId),
)}
auctionId={item.auctionId} auctionId={item.auctionId}
auctionDispatchDate={item.auctionDispatchDate} auctionDispatchDate={item.auctionDispatchDate}
isPurchased={isPurchase !== -1 ? true : false} isPurchased={isPurchase !== -1}
purchaseEnd={item.purchaseEnd} purchaseEnd={item.purchaseEnd}
auctionOfWork={item.auctionOfWork} auctionOfWork={item.auctionOfWork}
auctionFees={item.auctionFees} auctionFees={item.auctionFees}
auctionNumber={item.auctionNumber} auctionNumber={item.auctionNumber}
onPurchasePress={() => { Dispatch(togglePurchaseAuction(item)) }} onPurchasePress={() => {
console.log('[LiveAuction] purchase toggled:', item?.auctionNumber);
Dispatch(togglePurchaseAuction(item));
}}
onDetailsPress={() => {}} onDetailsPress={() => {}}
/> />
);
}, },
[SelectedPurchaseList] [SelectedPurchaseList],
); );
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
if (onEndReachedCalledDuringMomentum.current) return; if (onEndReachedCalledDuringMomentum.current) return;
if (!hasNextPage || isFetchingNextPage) return; if (!hasNextPage || isFetchingNextPage) return;
console.log('[LiveAuction] loadMore: fetching next page');
fetchNextPage(); fetchNextPage();
onEndReachedCalledDuringMomentum.current = true; onEndReachedCalledDuringMomentum.current = true;
}, [hasNextPage, isFetchingNextPage, fetchNextPage]); }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
@@ -71,91 +78,80 @@ const LiveAuction = () => {
const renderFooter = useCallback(() => { const renderFooter = useCallback(() => {
if (!isFetchingNextPage) return null; if (!isFetchingNextPage) return null;
return ( return (
<View style={{ paddingVertical: 12 }}> <View style={styles.footerLoader}>
<ActivityIndicator size="small" color={colors.primary} /> <ActivityIndicator size="small" color={colors.primary} />
<AppText variant="caption" color={colors.textSecondary}>
Loading more…
</AppText>
</View> </View>
); );
}, [isFetchingNextPage]); }, [isFetchingNextPage, colors]);
useEffect(() => { useEffect(() => {
if (isRefetching) { if (isRefetching) {
console.log('[LiveAuction] refetch started');
setShowRefreshBanner(true); setShowRefreshBanner(true);
} else { } else {
const timer = setTimeout(() => { console.log('[LiveAuction] refetch completed');
setShowRefreshBanner(false); const timer = setTimeout(() => setShowRefreshBanner(false), 1000);
}, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isRefetching]); }, [isRefetching]);
useEffect(() => { useEffect(() => {
if (data?.result?.length > 0) { console.log('[LiveAuction] data updated:', {
getMyAuction() count: data?.result?.length ?? 0,
} hasNextPage,
}, [data]) isFetchingNextPage,
isPending,
});
}, [data, hasNextPage, isFetchingNextPage, isPending]);
if (isLoading) { if (isLoading) {
return <Skelton />; return <Skelton />;
} }
return ( return (
<View style={{ flex: 1 }}> <View style={styles.root}>
<SearchInput onChange={() => { }} placeholder="Search...." filter={false} /> {/* Search */}
<SearchInput onChange={() => {}} placeholder="Search auctions…" filter={false} />
{/* Refresh Banner */}
{showRefreshBanner && ( {showRefreshBanner && (
<Animated.View <Animated.View
entering={FadeInDown.duration(200)} entering={FadeInDown.duration(200)}
exiting={FadeOutUp.duration(500)} exiting={FadeOutUp.duration(400)}
style={{ style={[
alignSelf: 'center', styles.refreshBanner,
flexDirection: 'row', {
alignItems: 'center', backgroundColor: colors.surface,
gap: 8, ...shadows.medium,
paddingVertical: 8, },
paddingHorizontal: 14, ]}
marginTop: 6,
borderRadius: 20,
backgroundColor: '#FFFFFF',
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 8,
shadowOffset: { width: 0, height: 3 },
elevation: 4,
zIndex: 10, // Ensure it stays on top
position: 'absolute',
top: 60, // Adjust based on your SearchInput height
}}
> >
<ActivityIndicator size="small" color={colors.primary} /> <ActivityIndicator size="small" color={colors.primary} />
<Text <AppText variant="caption" color={colors.text} style={styles.refreshText as any}>
style={{ Refreshing auctions…
fontSize: 13, </AppText>
color: '#374151',
fontWeight: '500',
}}
>
Refreshing auctions
</Text>
</Animated.View> </Animated.View>
)} )}
{
isPending || allBoolean.isLoading ? <Skeleton /> :
<FlatList
data={allBoolean.liveAuctionData ?? []}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 50, paddingTop: 10 }}
{/* List */}
{isPending ? (
<SkeltonList />
) : (
<FlatList
data={data?.result ?? []}
renderItem={renderItem}
keyExtractor={(item: any) => item?.auctionId?.toString()}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContent}
onEndReached={loadMore} onEndReached={loadMore}
onEndReachedThreshold={0.1} onEndReachedThreshold={0.1}
onMomentumScrollBegin={() => { onMomentumScrollBegin={() => {
onEndReachedCalledDuringMomentum.current = false; onEndReachedCalledDuringMomentum.current = false;
}} }}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={isRefetching} refreshing={isRefetching}
@@ -164,18 +160,48 @@ const LiveAuction = () => {
colors={[colors.primary]} colors={[colors.primary]}
/> />
} }
initialNumToRender={6} initialNumToRender={6}
maxToRenderPerBatch={6} maxToRenderPerBatch={6}
windowSize={7} windowSize={7}
removeClippedSubviews removeClippedSubviews
/> />
} )}
</View> </View>
); );
}; };
export default LiveAuction; export default LiveAuction;
const styles = StyleSheet.create({
root: {
flex: 1,
},
listContent: {
paddingHorizontal: 16,
paddingBottom: 60,
paddingTop: 10,
},
refreshBanner: {
alignSelf: 'center',
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 8,
paddingHorizontal: 16,
marginTop: 6,
borderRadius: 20,
zIndex: 10,
position: 'absolute',
top: 60,
},
refreshText: {
fontWeight: '500',
} as any,
footerLoader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 16,
},
});
+231 -196
View File
@@ -1,13 +1,26 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import moment from 'moment'; import moment from 'moment';
import colors from '../../Constant/Color';
import { LiveAuctionTileProps } from './types/types'; import { LiveAuctionTileProps } from './types/types';
import AuctionDetail from './Modal/AuctionDetail'; import AuctionDetail from './Modal/AuctionDetail';
import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native'; import {
Tag,
Calendar,
Clock,
ShoppingCart,
Trash2,
CheckCircle2,
XCircle,
} from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import NavigationString from '../../Constant/NavigationString';
import { useNavigation } from '@react-navigation/native';
const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({ const LiveAuctionTile: React.FC<LiveAuctionTileProps & { index?: number }> = ({
data, data,
index = 0,
isPurchased = false, isPurchased = false,
onPurchasePress, onPurchasePress,
auctionDispatchDate, auctionDispatchDate,
@@ -15,230 +28,252 @@ const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({
purchaseEnd, purchaseEnd,
auctionFees, auctionFees,
auctionNumber, auctionNumber,
isPurchasedByContractor,
}) => { }) => {
const { colors, spacing, shadows } = useTheme();
const [showDetail, setShowDetail] = React.useState(false); const [showDetail, setShowDetail] = React.useState(false);
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss')); const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss'));
const deadlineFormatted = moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY');
const Navigation = useNavigation() as any;
const getStatus = () => {
if (isPurchasedByContractor) return 'purchased';
if (purchaseEnded) return 'expired';
if (isPurchased) return 'in_cart';
return 'active';
};
const status = getStatus();
const styles = makeStyles(colors, spacing, shadows);
if (showDetail) {
return ( return (
<View style={styles.cardWrapper}> <View style={styles.card}>
{ <AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
!showDetail ?
<>
<View
style={[
styles.statusTag,
{ backgroundColor: isPurchased ? colors.error : purchaseEnded ? '#94a3b8' : '#4ade80' },
]}
/>
<View
style={styles.container}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity style={styles.badge} onPress={() => setShowDetail(true)}>
<Tag size={14} color={colors.primary} />
<Text style={styles.badgeText}>#{auctionNumber}</Text>
</TouchableOpacity>
<Text style={styles.priceText}>₹{auctionFees}</Text>
</View> </View>
);
{/* Work Title */}
<Text style={styles.workTitle} numberOfLines={2}>
{auctionOfWork ?? 'General Auction Work'}
</Text>
{/* Info Grid */}
<View style={styles.infoGrid}>
<View style={styles.infoItem}>
<Calendar size={16} color="#64748b" />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Start</Text>
<Text style={styles.infoValue}>{auctionDispatchDate ?? '-'}</Text>
</View>
</View>
<View style={styles.infoItem}>
<Calendar size={16} color={purchaseEnded ? "#ef4444" : "#22c55e"} />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Deadline</Text>
<Text style={[styles.infoValue, purchaseEnded && { color: '#ef4444' }]}>
{moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY')}
</Text>
</View>
</View>
</View>
{/* Action Bar */}
<View style={styles.actionBar}>
<TouchableOpacity style={styles.iconBtn} onPress={() => setShowDetail(true)}>
<Info size={18} color={colors.primary} />
<Text style={styles.iconBtnText}>Details</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.mainButton,
purchaseEnded
? styles.btnDisabled
: !data.active ? { backgroundColor: 'lightgray' } : isPurchased
? styles.btnPurchased
: styles.btnActive,
]}
onPress={onPurchasePress}
disabled={purchaseEnded ? true : data.active ? false : true}
>
<Text
style={[
styles.mainButtonText,
isPurchased && !purchaseEnded ? { color: colors.white } : { color: '#fff' },
]}
>
{purchaseEnded ? 'Expired' : !data.active ? 'Purchased' : isPurchased ? 'Remove' : 'Add to Cart'}
</Text>
{!purchaseEnded && (
<ArrowRight size={16} color={isPurchased ? colors.white : '#fff'} />
)}
</TouchableOpacity>
</View>
{/* Auction Detail Modal */}
</View>
</>
: <AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
} }
return (
<Animated.View
entering={FadeInDown.delay(index * 60).springify()}
style={styles.card}
>
{/* ── Row 1: Number + Fee ── */}
<View style={styles.topRow}>
<View style={styles.numberRow}>
<Tag size={14} color={colors.primary} strokeWidth={2} />
<AppText variant="bodySmall" color={colors.primary} style={styles.auctionNo as any}>
#{auctionNumber}
</AppText>
</View> </View>
<AppText variant="h4" color={colors.primary} style={styles.fee as any}>
₹{auctionFees}
</AppText>
</View>
{/* ── Row 2: Title ── */}
<AppText variant="h4" color={colors.text} numberOfLines={2} style={styles.title as any}>
{auctionOfWork ?? 'General Auction Work'}
</AppText>
{/* ── Row 3: Date strip ── */}
<View style={[styles.dateStrip, { backgroundColor: colors.overlay }]}>
{/* Start */}
<View style={styles.dateCol}>
<View style={styles.dateHeader}>
<Calendar size={13} color={colors.textSecondary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary} style={styles.dateLabel as any}>
Start Date
</AppText>
</View>
<AppText variant="caption" color={colors.text} style={styles.dateValue as any}>
{auctionDispatchDate ?? '—'}
</AppText>
</View>
{/* Separator */}
<View style={[styles.separator, { backgroundColor: colors.border }]} />
{/* Deadline */}
<View style={styles.dateCol}>
<View style={styles.dateHeader}>
<Clock
size={13}
color={purchaseEnded ? colors.error : colors.success}
strokeWidth={2}
/>
<AppText variant="caption" color={colors.textSecondary} style={styles.dateLabel as any}>
Deadline
</AppText>
</View>
<AppText
variant="caption"
color={purchaseEnded ? colors.error : colors.text}
style={styles.dateValue as any}
>
{deadlineFormatted}
</AppText>
</View>
</View>
{/* ── Row 4: Actions ── */}
<View style={styles.actionRow}>
{/* Ghost Details button */}
<TouchableOpacity
style={[styles.detailsBtn, { borderColor: colors.border }]}
onPress={() => setShowDetail(true)}
activeOpacity={0.7}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.detailsText as any}>
View Details
</AppText>
</TouchableOpacity>
{/* CTA */}
{status === 'active' && (
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.primary }]}
onPress={onPurchasePress}
activeOpacity={0.85}
>
<ShoppingCart size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
ADD TO CART
</AppText>
</TouchableOpacity>
)}
{status === 'in_cart' && (
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.success }]}
onPress={() => { Navigation.navigate(NavigationString.PurchaseList) }}
activeOpacity={0.85}
>
<ShoppingCart size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
GO TO CART
</AppText>
</TouchableOpacity>
)}
{status === 'purchased' && (
<View style={[styles.ctaBtn, { backgroundColor: colors.secondary }]}>
<CheckCircle2 size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
PURCHASED
</AppText>
</View>
)}
{status === 'expired' && (
<View style={[styles.ctaBtn, { backgroundColor: colors.secondary }]}>
<XCircle size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
EXPIRED
</AppText>
</View>
)}
</View>
</Animated.View>
); );
}; };
export default memo(LiveAuctionTile); export default memo(LiveAuctionTile);
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, shadows: any) =>
cardWrapper: { StyleSheet.create({
marginVertical: 10, card: {
marginHorizontal: 16, backgroundColor: colors.surface,
backgroundColor: '#fff', borderRadius: 18,
borderRadius: 24, borderWidth: 1,
flexDirection: 'row', borderColor: colors.border,
overflow: 'hidden', marginVertical: 7,
padding: spacing.m,
...shadows.medium,
},
// Modern shadow /* Top row */
shadowColor: '#000', topRow: {
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.08,
shadowRadius: 20,
elevation: 8,
},
statusTag: {
width: 6,
height: '100%',
},
container: {
flex: 1,
padding: 20,
},
header: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: spacing.s,
}, },
badge: { numberRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.primary + '25', gap: 5,
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 12,
gap: 6,
}, },
badgeText: { auctionNo: {
fontSize: 12,
fontWeight: '700', fontWeight: '700',
color: colors.primary, } as any,
}, fee: {
priceText: {
fontSize: 20,
fontWeight: '800', fontWeight: '800',
color: '#0f172a', } as any,
},
workTitle: { /* Title */
fontSize: 16, title: {
fontWeight: '600', fontWeight: '700',
color: '#334155',
lineHeight: 24, lineHeight: 24,
marginBottom: 16, marginBottom: spacing.m,
}, } as any,
infoGrid: {
flexDirection: 'row', /* Date strip */
justifyContent: 'space-between', dateStrip: {
backgroundColor: '#f0f4f8',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 16,
marginBottom: 18,
},
infoItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 10, borderRadius: 12,
paddingVertical: spacing.s + 2,
paddingHorizontal: spacing.m,
marginBottom: spacing.m,
}, },
infoTextGroup: { dateCol: {
flexDirection: 'column',
},
infoLabel: {
fontSize: 10,
color: '#94a3b8',
textTransform: 'uppercase',
fontWeight: '700',
},
infoValue: {
fontSize: 13,
fontWeight: '700',
color: '#1e293b',
},
actionBar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
},
iconBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 10,
backgroundColor: '#e2e8f0',
},
iconBtnText: {
fontSize: 14,
fontWeight: '700',
color: colors.primary,
},
mainButton: {
flex: 1, flex: 1,
height: 46, },
borderRadius: 16, dateHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginBottom: 3,
},
dateLabel: {
fontWeight: '500',
} as any,
dateValue: {
fontWeight: '700',
} as any,
separator: {
width: StyleSheet.hairlineWidth,
height: 36,
marginHorizontal: spacing.m,
},
/* Actions */
actionRow: {
flexDirection: 'row',
gap: spacing.s,
alignItems: 'center',
},
detailsBtn: {
flex: 1,
paddingVertical: 10,
borderWidth: 1,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
detailsText: {
fontWeight: '600',
} as any,
ctaBtn: {
flex: 2,
height: 44,
borderRadius: 12,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 8, gap: 6,
}, },
btnActive: { ctaText: {
backgroundColor: '#4ade80', // light green
},
btnPurchased: {
backgroundColor: colors.error,
borderWidth: 2,
borderColor: colors.error,
},
btnDisabled: {
backgroundColor: '#e2e8f0',
},
mainButtonText: {
fontSize: 14,
fontWeight: '800', fontWeight: '800',
}, letterSpacing: 0.8,
} as any,
}); });
+324 -273
View File
@@ -1,63 +1,66 @@
import React, { memo, useEffect, useState } from 'react'; import React, { memo, useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, StatusBar, Modal } from 'react-native'; import {
import { Briefcase, Calendar, FileText, Download, Eye, Layers, ArrowLeft, Clock } from 'lucide-react-native'; View,
import { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/BBPS/Hooks'; StyleSheet,
TouchableOpacity,
ScrollView,
Modal,
ActivityIndicator,
} from 'react-native';
import {
Briefcase,
Calendar,
FileText,
Download,
Eye,
Layers,
ArrowLeft,
Clock,
IndianRupee,
Wrench,
Mail,
} from 'lucide-react-native';
import { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/BBPS/ApiUrls'; import { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
import ReactNativeBlobUtil from 'react-native-blob-util'; import ReactNativeBlobUtil from 'react-native-blob-util';
import AuctionItemDetail from './AuctionItemDetail'; import AuctionItemDetail from './AuctionItemDetail';
import { Loader } from '../../../Component/Index'; import { useTheme } from '../../../Hooks/useTheme';
import colors from '../../../Constant/Color'; import AppText from '../../../components/AppText';
import moment from 'moment'; import moment from 'moment';
interface Props { visible: boolean; onClose: () => void; data?: any } interface Props { visible: boolean; onClose: () => void; data?: any }
const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => { const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets() const inset = useSafeAreaInsets();
const { data: auctionData, isLoading } = useGetAuciton({ auctionId: data?.auctionId }) const { colors, spacing, shadows } = useTheme();
const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData)
const [allBoolean, setAllBoolean] = useState({ showItemDetail: false, selectedItem: {}, fileLoading: false }); const { data: auctionData } = useGetAuciton({ auctionId: data?.auctionId });
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId }) const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData);
const { mutate } = useAuctionItem() const [allBoolean, setAllBoolean] = useState({
showItemDetail: false,
selectedItem: {},
fileLoading: false,
});
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId });
const { mutate } = useAuctionItem();
const formatDate = (date?: string) => { const formatDate = (date?: string) => {
if (!date) return '-'; if (!date) return '—';
return moment(date, 'DD-MM-YYYY HH:mm:ss').format('DD MMM YYYY, hh:mm A'); return moment(date, 'DD-MM-YYYY HH:mm:ss').format('DD MMM YYYY, hh:mm A');
}; };
const isLive = data?.purchaseEnd ? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss')) : false; const isLive = data?.purchaseEnd
? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss'))
const Section = ({ title, icon: Icon, children }: any) => ( : false;
<View style={styles.sectionContainer}>
<View style={styles.sectionHeader}>
<Icon size={20} color={colors.primary} />
<Text style={styles.sectionTitle}>{title}</Text>
</View>
{children}
</View>
);
const InfoBox = ({ label, value, full = false }: any) => (
<View style={[styles.infoBox, full ? { width: '100%' } : { width: '48%' }]}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value || '-'}</Text>
</View>
);
const handleDownload = async (id: number, fileName: string) => { const handleDownload = async (id: number, fileName: string) => {
try { try {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: true })); setAllBoolean(prev => ({ ...prev, fileLoading: true }));
const { dirs } = ReactNativeBlobUtil.fs; const { dirs } = ReactNativeBlobUtil.fs;
const path = `${dirs.DownloadDir}/${fileName}.pdf`; const path = `${dirs.DownloadDir}/${fileName}.pdf`;
// console.log('Saving to:', path);
const downloadUrl = `${AUCTION_DOCUMENT_DOWNLOAD}?documentId=${id}&isAmendment=false`; const downloadUrl = `${AUCTION_DOCUMENT_DOWNLOAD}?documentId=${id}&isAmendment=false`;
// console.log('Download URL:', downloadUrl); await ReactNativeBlobUtil.config({
const response = await ReactNativeBlobUtil.config({
fileCache: false, fileCache: false,
addAndroidDownloads: { addAndroidDownloads: {
useDownloadManager: true, useDownloadManager: true,
@@ -68,292 +71,340 @@ const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
description: 'Downloading document', description: 'Downloading document',
mediaScannable: true, mediaScannable: true,
}, },
}).fetch('GET', downloadUrl, { }).fetch('GET', downloadUrl, { Accept: 'application/pdf' });
Accept: 'application/pdf',
});
const stat = await ReactNativeBlobUtil.fs.stat(path);
console.log('File size:', stat.size);
} catch (error: any) { } catch (error: any) {
console.log('Error:', error?.message || error); console.log('Error:', error?.message || error);
} finally { } finally {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: false })); setAllBoolean(prev => ({ ...prev, fileLoading: false }));
} }
}; };
useEffect(() => { useEffect(() => {
mutate({ auctionNumber: data?.auctionNumber, status: data?.auctionPattern }, { mutate(
onSuccess: (res) => { { auctionNumber: data?.auctionNumber, status: data?.auctionPattern },
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments })) {
}, onError: (err) => { onSuccess: (res: any) => {
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments }));
},
onError: (err: any) => {
console.log(err, '-----auction item error'); console.log(err, '-----auction item error');
} },
}); },
);
}, [data.auctionNumber, auctionDocuments]); }, [data.auctionNumber, auctionDocuments]);
const isAmended = auctionData?.auctionStatus === 'Amended';
const kd = (key: any, amended: any) => isAmended ? amended : key;
const styles = makeStyles(colors, spacing, shadows);
return ( return (
<Modal <Modal
visible={visible} visible={visible}
transparent={false} transparent={false}
animationType="none" animationType="slide"
statusBarTranslucent statusBarTranslucent
onRequestClose={onClose} onRequestClose={onClose}
> >
<View style={styles.safeArea}> <View style={styles.root}>
<StatusBar barStyle="dark-content" /> {/* ── Header ── */}
<View style={[styles.header, { paddingTop: inset.top + 6 }]}>
{/* Full Header */} <TouchableOpacity onPress={onClose} style={styles.backBtn} activeOpacity={0.7}>
<View style={[styles.header, { paddingTop: inset.top }]}> <ArrowLeft size={22} color={colors.text} strokeWidth={2.5} />
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" />
</TouchableOpacity> </TouchableOpacity>
<View style={styles.headerTitleContainer}>
<Text style={styles.headerTitleText}>Auction Overview</Text> <View style={styles.headerCenter}>
<Text style={styles.headerIdText}>Ref: {data?.auctionNumber || 'N/A'}</Text> <AppText variant="h4" color={colors.text} style={styles.headerTitle as any}>
Auction Overview
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Ref: {data?.auctionNumber ?? 'N/A'}
</AppText>
</View> </View>
<View style={styles.liveIndicator}>
<View style={[styles.dot, { backgroundColor: isLive ? '#22c55e' : '#94a3b8' }]} /> <View style={[styles.statusChip, { borderColor: isLive ? `${colors.success}50` : colors.border }]}>
<Text style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</Text> <View style={[styles.statusDot, { backgroundColor: isLive ? colors.success : colors.textSecondary }]} />
<AppText variant="caption" color={isLive ? colors.success : colors.textSecondary} style={styles.statusText as any}>
{isLive ? 'LIVE' : 'CLOSED'}
</AppText>
</View> </View>
</View> </View>
{/* Scrollable Body */} {/* ── Body ── */}
<ScrollView <ScrollView
style={styles.scrollView} showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={true} style={{ backgroundColor: colors.background }}
> >
{/* 1. Basic Info Grid */}
<Section title="Details" icon={Layers}>
<View style={styles.grid}>
<InfoBox label="Auction No." value={data?.auctionNumber} />
<InfoBox label="Pattern" value={data?.auctionPattern || 'Forward'} />
<InfoBox label="Mode" value={data?.auctionMode || 'Open'} />
<InfoBox label="Category" value={data?.categoryOfAuction || 'General'} />
<InfoBox label="EMD Type" value={data?.emd || 'Online'} />
<InfoBox label="EMD Figure" value={data?.emdFigure || 'Item Wise'} />
</View>
</Section>
{/* 2. Work Description */} {/* BASIC DETAILS */}
<Section title="Auction Details" icon={Briefcase}> <SectionCard title="Basic Details" icon={Layers} colors={colors} spacing={spacing} shadows={shadows}>
<View style={styles.grid}> <InfoRow label="Auction No." value={data?.auctionNumber} colors={colors} />
<InfoBox label="Auction Name" value={auctionData?.auctionOfWork} /> <InfoRow label="Pattern" value={data?.auctionPattern || 'Forward'} colors={colors} />
<InfoBox label="Category Of Auction " value={auctionData?.auctionCategories} /> <InfoRow label="Mode" value={data?.auctionMode || 'Open'} colors={colors} />
<InfoBox label="Auction Fees (In Figure) " value={auctionData?.auctionFees} /> <InfoRow label="Category" value={data?.categoryOfAuction || 'General'} colors={colors} />
<InfoBox label="Auction Fees (In Words)" value={auctionData?.auctionFeesInWord} /> <InfoRow label="EMD Type" value={data?.emd || 'Online'} colors={colors} last />
<InfoBox label="Auction Keyword" value={auctionData?.auctionKeywordName} /> </SectionCard>
</View>
</Section>
{/* 3. Timeline Section */} {/* AUCTION DETAILS */}
<Section title="Key Dates" icon={Calendar}> <SectionCard title="Auction Details" icon={Briefcase} colors={colors} spacing={spacing} shadows={shadows}>
<View style={styles.timelineContainer}> <InfoRow label="Name" value={auctionData?.auctionOfWork} colors={colors} multiline />
<View style={styles.timelineRow}> <InfoRow label="Category" value={auctionData?.auctionCategories} colors={colors} />
<View style={styles.iconCircle}><Clock size={16} color="#64748b" /></View> <InfoRow label="Fees" value={auctionData?.auctionFees ? `₹ ${auctionData.auctionFees}` : '—'} colors={colors} valueColor={colors.primary} />
<View> <InfoRow label="Fees (Words)" value={auctionData?.auctionFeesInWord} colors={colors} multiline />
<Text style={styles.label}>Auction Purchase (Start)</Text> <InfoRow label="Keyword" value={auctionData?.auctionKeywordName} colors={colors} last />
<Text style={styles.value}>{formatDate(data?.auctionDispatchDate)}</Text> </SectionCard>
</View>
</View>
<View style={styles.timelineConnector} />
<View style={styles.timelineRow}>
<View style={[styles.iconCircle, { backgroundColor: '#fee2e2' }]}><Clock size={16} color="#ef4444" /></View>
<View>
<Text style={styles.label}>Purchase Deadline (End)</Text>
<Text style={styles.value}>{formatDate(data?.purchaseEnd)}</Text>
</View>
</View>
</View>
</Section>
{/* 4. Item Table Section */} {/* KEY DATES */}
<Section title="Auction Items" icon={FileText}> <SectionCard title="Key Dates" icon={Calendar} colors={colors} spacing={spacing} shadows={shadows}>
<View style={styles.tableWrapper}> <InfoRow label="Purchase Start" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.startDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.startDateTime))} colors={colors} />
<View style={styles.tableHead}> <InfoRow label="Purchase End" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.endDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.endDateTime))} colors={colors} valueColor={colors.error} />
<Text style={[styles.tableLabel, { flex: 2 }]}>Item</Text> <InfoRow label="Bidding Start" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.startDateTime, auctionData?.amendedkeyDates?.auctionBidding?.startDateTime))} colors={colors} />
<Text style={styles.tableLabel}>Time</Text> <InfoRow label="Bidding End" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.endDateTime, auctionData?.amendedkeyDates?.auctionBidding?.endDateTime))} colors={colors} valueColor={colors.error} last />
<Text style={[styles.tableLabel, { textAlign: 'right' }]}>View</Text> </SectionCard>
{/* AUCTION ITEMS */}
{aucitonDetail?.auctionItems?.length > 0 && (
<SectionCard title="Auction Items" icon={FileText} colors={colors} spacing={spacing} shadows={shadows}>
{aucitonDetail.auctionItems.map((item: any, index: number) => (
<View
key={index}
style={[
styles.itemRow,
{ borderBottomColor: colors.border },
index === aucitonDetail.auctionItems.length - 1 && { borderBottomWidth: 0 },
]}
>
<View style={styles.itemLeft}>
<AppText variant="bodySmall" color={colors.text} style={styles.itemName as any}>
{item.productName}
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
{item?.categoryName} · {item.timeDurationType}
</AppText>
</View> </View>
{/* Dummy Item Row */} <TouchableOpacity
{aucitonDetail?.auctionItems?.map((item: any, index: number) => ( style={[styles.eyeBtn, { backgroundColor: `${colors.primary}15` }]}
<View style={styles.tableRow}> onPress={() => setAllBoolean(prev => ({ ...prev, showItemDetail: true, selectedItem: item }))}
<View style={{ flex: 2 }}> >
<Text style={styles.itemMain}>{item.productName}</Text> <Eye size={16} color={colors.primary} strokeWidth={2} />
<Text style={styles.itemSub}>Category: {item?.categoryName}</Text>
</View>
<Text style={styles.itemQty}>{item.timeDurationType}</Text>
<TouchableOpacity style={styles.actionIcon} onPress={() => { setAllBoolean((prev: any) => ({ ...prev, showItemDetail: true, selectedItem: item })) }}>
<Eye size={18} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
))} ))}
</View> </SectionCard>
</Section>
{/* 5. Downloads */}
<Section title="Downloads" icon={Download}>
{aucitonDetail?.auctionDocuments?.map((doc: any, index: number) => (
<TouchableOpacity style={styles.fileCard} onPress={() => {
handleDownload(
doc.auctionDocumentId,
doc.fileName.replace('.pdf', '')
)
}}>
<View style={styles.fileIcon}><FileText size={20} color="#ef4444" /></View>
<View style={{ flex: 1 }}>
<Text style={styles.fileName}>{doc.fileName}</Text>
</View>
{allBoolean.fileLoading ? (
<View >
<Loader size='small' color='#94a3b8' />
</View>
) : (
<Download size={20} color="#94a3b8" />
)} )}
{/* DOCUMENTS */}
{aucitonDetail?.auctionDocuments?.length > 0 && (
<SectionCard title="Documents" icon={Download} colors={colors} spacing={spacing} shadows={shadows}>
{aucitonDetail.auctionDocuments.map((doc: any, index: number) => (
<TouchableOpacity
key={index}
style={[
styles.docRow,
{ borderBottomColor: colors.border },
index === aucitonDetail.auctionDocuments.length - 1 && { borderBottomWidth: 0 },
]}
activeOpacity={0.7}
onPress={() => handleDownload(doc.auctionDocumentId, doc.fileName.replace('.pdf', ''))}
>
<View style={[styles.docIcon, { backgroundColor: `${colors.error}15` }]}>
<FileText size={16} color={colors.error} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} style={styles.docName as any} numberOfLines={1}>
{doc.fileName}
</AppText>
{allBoolean.fileLoading
? <ActivityIndicator size="small" color={colors.primary} />
: <Download size={18} color={colors.textSecondary} strokeWidth={2} />
}
</TouchableOpacity> </TouchableOpacity>
))} ))}
</Section> </SectionCard>
)}
</ScrollView> </ScrollView>
{allBoolean.showItemDetail && <AuctionItemDetail data={allBoolean.selectedItem} onClose={() => { setAllBoolean((prev: any) => ({ ...prev, showItemDetail: false })) }} visible={allBoolean.showItemDetail} />} {allBoolean.showItemDetail && (
<AuctionItemDetail
data={allBoolean.selectedItem}
onClose={() => setAllBoolean(prev => ({ ...prev, showItemDetail: false }))}
visible={allBoolean.showItemDetail}
/>
)}
</View> </View>
</Modal> </Modal>
); );
}; };
const styles = StyleSheet.create({ export default memo(AuctionFullDetail);
fullModal: {
margin: 0, /* ─── Sub-components ─── */
backgroundColor: '#fff',
const SectionCard = ({ title, icon: Icon, children, colors, spacing, shadows }: any) => {
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Icon size={18} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.text} style={styles.cardTitle as any}>
{title.toUpperCase()}
</AppText>
</View>
<View style={[styles.cardDivider, { backgroundColor: colors.border }]} />
{children}
</View>
);
};
const InfoRow = ({ label, value, colors, last = false, multiline = false, valueColor }: any) => (
<View style={[
{
flexDirection: 'row',
alignItems: multiline ? 'flex-start' : 'center',
justifyContent: 'space-between',
paddingVertical: 13,
borderBottomWidth: last ? 0 : StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.06)',
gap: 12,
}, },
safeArea: { ]}>
<AppText variant="bodySmall" color={colors.textSecondary} style={{ flex: 1 } as any}>
{label}
</AppText>
<AppText
variant="bodySmall"
color={valueColor || colors.text}
style={{ flex: 1, textAlign: 'right', fontWeight: '600' } as any}
numberOfLines={multiline ? 4 : 1}
>
{value || '—'}
</AppText>
</View>
);
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
root: {
flex: 1, flex: 1,
backgroundColor: '#fff', backgroundColor: colors.background,
}, },
/* Header */
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: 16, paddingHorizontal: spacing.m,
borderBottomWidth: 1, paddingBottom: spacing.m,
borderBottomColor: '#f1f5f9', backgroundColor: colors.surface,
backgroundColor: '#fff', borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
}, },
backBtn: { backBtn: {
padding: 8, width: 36,
marginRight: 8, height: 36,
}, borderRadius: 10,
headerTitleContainer: {
flex: 1,
},
headerTitleText: {
fontSize: 18,
fontWeight: '800',
color: '#0f172a',
},
headerIdText: {
fontSize: 13,
color: '#64748b',
},
liveIndicator: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f8fafc',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
gap: 6,
borderWidth: 1,
borderColor: '#e2e8f0',
},
dot: { width: 8, height: 8, borderRadius: 4 },
liveText: { fontSize: 11, fontWeight: '800', color: '#475569' },
scrollView: { flex: 1, backgroundColor: '#f8fafc' },
scrollContent: { padding: 16, paddingBottom: 30 },
sectionContainer: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: '#eef2f6',
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 20,
paddingBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#f8fafc',
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: '#1e293b',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
infoBox: { marginBottom: 16 },
label: { fontSize: 12, color: '#94a3b8', fontWeight: '600', marginBottom: 4 },
value: { fontSize: 15, fontWeight: '700', color: '#334155' },
spacer: { height: 12 },
timelineContainer: { paddingLeft: 8 },
timelineRow: { flexDirection: 'row', alignItems: 'center', gap: 15 },
iconCircle: { width: 34, height: 34, borderRadius: 10, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
timelineConnector: { width: 2, height: 20, backgroundColor: '#f1f5f9', marginLeft: 16 },
tableWrapper: { backgroundColor: '#f8fafc', borderRadius: 12, padding: 12 },
tableHead: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#e2e8f0', paddingBottom: 8, marginBottom: 12 },
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 },
tableRow: { flexDirection: 'row', alignItems: 'center' },
itemMain: { fontSize: 14, fontWeight: '700', color: '#1e293b' },
itemSub: { fontSize: 12, color: '#64748b' },
itemQty: { flex: 1, fontSize: 14, fontWeight: '700', color: '#334155' },
actionIcon: { padding: 6 },
fileCard: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 },
fileIcon: { width: 40, height: 40, borderRadius: 8, backgroundColor: '#fee2e2', alignItems: 'center', justifyContent: 'center' },
fileName: { fontSize: 14, fontWeight: '700', color: '#334155' },
fileSize: { fontSize: 12, color: '#94a3b8' },
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#fff',
padding: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1,
borderTopColor: '#f1f5f9',
},
mainBtn: {
backgroundColor: colors.primary,
height: 56,
borderRadius: 16,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
}, },
btnDisabled: { backgroundColor: '#cbd5e1' }, headerCenter: {
mainBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' }, flex: 1,
}); },
headerTitle: {
fontWeight: '800',
} as any,
statusChip: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.s + 4,
paddingVertical: 6,
borderRadius: 20,
borderWidth: 1.2,
gap: 5,
},
statusDot: {
width: 7,
height: 7,
borderRadius: 4,
},
statusText: {
fontWeight: '800',
letterSpacing: 0.5,
} as any,
export default memo(AuctionFullDetail); scrollContent: {
padding: spacing.m,
paddingBottom: 50,
gap: spacing.m,
},
/* Card */
card: {
backgroundColor: colors.surface,
borderRadius: 18,
paddingHorizontal: spacing.m,
paddingTop: spacing.m,
paddingBottom: spacing.s,
marginBottom: spacing.m,
...shadows.light,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
marginBottom: spacing.s + 2,
},
cardTitle: {
fontWeight: '800',
letterSpacing: 1.2,
} as any,
cardDivider: {
height: StyleSheet.hairlineWidth,
marginBottom: spacing.s,
},
/* Items */
itemRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.s + 2,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: spacing.s,
},
itemLeft: {
flex: 1,
},
itemName: {
fontWeight: '700',
marginBottom: 2,
} as any,
eyeBtn: {
width: 32,
height: 32,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
/* Docs */
docRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.s + 2,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: spacing.s,
},
docIcon: {
width: 32,
height: 32,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
docName: {
flex: 1,
fontWeight: '600',
} as any,
});
+69 -72
View File
@@ -1,38 +1,43 @@
import React, { memo, useState } from 'react'; import React, { memo, useState, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, Modal } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, Modal } from 'react-native';
import { Package, IndianRupee, FileText, Download, ArrowLeft, Info, } from 'lucide-react-native'; import { Package, IndianRupee, FileText, Download, ArrowLeft, Info, } from 'lucide-react-native';
import colors from '../../../Constant/Color';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useAucitonItemDetail } from '../../../Services/BBPS/Hooks'; import { useAucitonItemDetail } from '../../../Services/ApiCall/Hooks';
import ReactNativeBlobUtil from 'react-native-blob-util'; import ReactNativeBlobUtil from 'react-native-blob-util';
import { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/BBPS/ApiUrls'; import { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
import { Loader } from '../../../Component/Index'; import { Loader } from '../../../Component/Index';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props { visible: boolean; onClose: () => void; data?: any; } interface Props { visible: boolean; onClose: () => void; data?: any; }
const Section = ({ title, icon: Icon, children }: any) => ( const Section = ({ title, icon: Icon, children, styles, colors }: any) => (
<View style={styles.sectionCard}> <View style={styles.sectionCard}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<View style={styles.iconCircle}> <View style={styles.iconCircle}>
<Icon size={18} color={colors.primary} strokeWidth={2.5} /> <Icon size={18} color={colors.primary} strokeWidth={2.5} />
</View> </View>
<Text style={styles.sectionTitle}>{title}</Text> <AppText variant="caption" bold style={styles.sectionTitle}>{title}</AppText>
</View> </View>
{children} {children}
</View> </View>
); );
const PriceTile = ({ label, fig, words, full = false }: any) => ( const PriceTile = ({ label, fig, words, full = false, styles }: any) => (
<View style={[styles.priceTile, full ? { width: '100%' } : { width: '48%' }]}> <View style={[styles.priceTile, full ? { width: '100%' } : { width: '48%' }]}>
<Text style={styles.label}>{label}</Text> <AppText variant="caption" color={styles.label.color} bold style={styles.label}>{label}</AppText>
<Text style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</Text> <AppText variant="h3" bold style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</AppText>
<Text style={styles.priceWords}>{words}</Text> <AppText variant="caption" style={styles.priceWords}>{words}</AppText>
</View> </View>
); );
const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => { const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets() const inset = useSafeAreaInsets()
const { colors, typography } = useTheme();
const styles = useMemo(() => createStyles(colors, typography), [colors, typography]);
const [allBoolean, setAllBoolean] = useState({ fileLoading: false }); const [allBoolean, setAllBoolean] = useState({ fileLoading: false });
const { data: itemData } = useAucitonItemDetail({ auctionItemId: data.id }); const { data: itemData } = useAucitonItemDetail({ auctionItemId: data?.id });
const handleDownload = async (id: number, fileName: string) => { const handleDownload = async (id: number, fileName: string) => {
try { try {
@@ -41,10 +46,7 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const path = `${dirs.DownloadDir}/${fileName}.pdf`; const path = `${dirs.DownloadDir}/${fileName}.pdf`;
// console.log('Saving to:', path);
const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`; const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`;
// console.log('Download URL:', downloadUrl);
const response = await ReactNativeBlobUtil.config({ const response = await ReactNativeBlobUtil.config({
fileCache: false, fileCache: false,
@@ -62,7 +64,6 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
}); });
const stat = await ReactNativeBlobUtil.fs.stat(path); const stat = await ReactNativeBlobUtil.fs.stat(path);
console.log(' File size:', stat.size);
} catch (error: any) { } catch (error: any) {
console.log('Error:', error?.message || error); console.log('Error:', error?.message || error);
@@ -84,11 +85,11 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
{/* Navbar */} {/* Navbar */}
<View style={[styles.header, { paddingTop: inset.top }]}> <View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}> <TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" /> <ArrowLeft size={24} color={colors.text} />
</TouchableOpacity> </TouchableOpacity>
<View> <View>
<Text style={styles.headerTitle}>Item Information</Text> <AppText variant="body" bold style={styles.headerTitle}>Item Information</AppText>
<Text style={styles.headerSub}>Detail View</Text> <AppText variant="caption" style={styles.headerSub}>Detail View</AppText>
</View> </View>
</View> </View>
@@ -97,69 +98,66 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
<Section title="Product Specification" icon={Package}> <Section title="Product Specification" icon={Package} styles={styles} colors={colors}>
<View style={styles.identityHeader}> <View style={styles.identityHeader}>
<Text style={styles.productNameText}>{data.productName.toUpperCase()}</Text> <AppText variant="h4" bold style={styles.productNameText}>{data?.productName?.toUpperCase()}</AppText>
<View style={styles.categoryBadge}> <View style={styles.categoryBadge}>
<Text style={styles.categoryText}>{data.categoryName}</Text> <AppText variant="caption" bold style={styles.categoryText}>{data?.categoryName}</AppText>
</View> </View>
</View> </View>
<View style={styles.specGrid}> <View style={styles.specGrid}>
<View style={styles.specItem}> <View style={styles.specItem}>
<Text style={styles.label}>Sub Category</Text> <AppText variant="caption" bold style={styles.label}>Sub Category</AppText>
<Text style={styles.specValue}>{data.subCategoryName}</Text> <AppText variant="bodySmall" bold style={styles.specValue}>{data?.subCategoryName}</AppText>
</View> </View>
<View style={styles.specItem}> <View style={styles.specItem}>
<Text style={styles.label}>Duration</Text> <AppText variant="caption" bold style={styles.label}>Duration</AppText>
<Text style={styles.specValue}>{data.timeDurationType}</Text> <AppText variant="bodySmall" bold style={styles.specValue}>{data?.timeDurationType}</AppText>
</View> </View>
</View> </View>
</Section> </Section>
<Section title="Reserve & Bid Values" icon={IndianRupee}> <Section title="Reserve & Bid Values" icon={IndianRupee} styles={styles} colors={colors}>
<PriceTile <PriceTile
label="Bidding Price" label="Bidding Price"
fig={data.reservePrice || data.emdAmount} fig={data?.reservePrice || data?.emdAmount}
words={data.reservePriceInWords || data.emdAmountInWords} words={data?.reservePriceInWords || data?.emdAmountInWords}
full full
styles={styles}
/> />
<View style={styles.hDivider} /> <View style={styles.hDivider} />
<PriceTile <PriceTile
label="Auction Start Value" label="Auction Start Value"
fig={data.reservePrice || data.auctionStartValueFigure} fig={data?.reservePrice || data?.auctionStartValueFigure}
words={data.reservePriceInWords || data.auctionStartValueWords} words={data?.reservePriceInWords || data?.auctionStartValueWords}
full full
styles={styles}
/> />
<View style={styles.hDivider} /> <View style={styles.hDivider} />
{/* <View style={styles.row}>
<PriceTile label="Bid Variation" fig={data.bidVariationFig} words={data.bidVariationWords} />
<PriceTile label="Auction Start Value" fig={data.auctionStartValue} words={data.auctionStartValueInWords
} />
</View> */}
</Section> </Section>
<Section title="Brief Description" icon={Info}> <Section title="Brief Description" icon={Info} styles={styles} colors={colors}>
<Text style={styles.descContent}>{data.briefDescription}</Text> <AppText variant="bodySmall" style={styles.descContent}>{data?.briefDescription}</AppText>
</Section> </Section>
{itemData?.length && <Section title="View Documents" icon={FileText}> {itemData?.length && <Section title="View Documents" icon={FileText} styles={styles} colors={colors}>
<View style={styles.docTableHead}> <View style={styles.docTableHead}>
<Text style={styles.tableLabel}>Sr No.</Text> <AppText variant="caption" bold style={styles.tableLabel}>Sr No.</AppText>
<Text style={[styles.tableLabel, { flex: 2 }]}>Document Name</Text> <AppText variant="caption" bold style={[styles.tableLabel, { flex: 2 }]}>Document Name</AppText>
<Text style={styles.tableLabel}>Action</Text> <AppText variant="caption" bold style={styles.tableLabel}>Action</AppText>
</View> </View>
{itemData?.map((doc: any, index: any) => ( {itemData?.map((doc: any, index: any) => (
<TouchableOpacity style={styles.docRow} activeOpacity={0.7} onPress={() => { handleDownload(doc.assetId, doc.fileName) }} key={doc.assetId}> <TouchableOpacity style={styles.docRow} activeOpacity={0.7} onPress={() => { handleDownload(doc.assetId, doc.fileName) }} key={doc.assetId}>
<Text style={styles.docIndex}>{index + 1}</Text> <AppText variant="bodySmall" bold style={styles.docIndex}>{index + 1}</AppText>
<View style={{ flex: 2 }}> <View style={{ flex: 2 }}>
<Text style={styles.docTitle} numberOfLines={1}>{doc.fileName}</Text> <AppText variant="bodySmall" bold style={styles.docTitle} numberOfLines={1}>{doc.fileName}</AppText>
</View> </View>
<View style={styles.downloadIconBox}> <View style={styles.downloadIconBox}>
{allBoolean.fileLoading ? ( {allBoolean.fileLoading ? (
<View > <View >
<Loader size='small' color='#94a3b8' /> <Loader size='small' color={colors.textSecondary} />
</View> </View>
) : ( ) : (
<Download size={20} color={colors.primary} /> <Download size={20} color={colors.primary} />
@@ -174,26 +172,26 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
); );
}; };
const styles = StyleSheet.create({ const createStyles = (colors: any, typography: any) => StyleSheet.create({
fullModal: { margin: 0, backgroundColor: '#f8fafc' }, fullModal: { margin: 0, backgroundColor: colors.background },
safeArea: { flex: 1 }, safeArea: { flex: 1, backgroundColor: colors.background },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: 16, padding: 16,
backgroundColor: '#fff', backgroundColor: colors.surface,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f1f5f9', borderBottomColor: colors.border,
}, },
backBtn: { padding: 8, marginRight: 8 }, backBtn: { padding: 8, marginRight: 8 },
headerTitle: { fontSize: 18, fontWeight: '800', color: '#0f172a' }, headerTitle: { fontSize: 18, color: colors.text },
headerSub: { fontSize: 12, color: '#94a3b8', fontWeight: '600', textTransform: 'uppercase' }, headerSub: { fontSize: 12, color: colors.textSecondary, textTransform: 'uppercase' },
scrollView: { flex: 1 }, scrollView: { flex: 1 },
scrollContent: { padding: 16, paddingBottom: 50 }, scrollContent: { padding: 16, paddingBottom: 50 },
sectionCard: { sectionCard: {
backgroundColor: '#fff', backgroundColor: colors.surface,
borderRadius: 24, borderRadius: 24,
padding: 20, padding: 20,
marginBottom: 16, marginBottom: 16,
@@ -218,55 +216,54 @@ const styles = StyleSheet.create({
}, },
sectionTitle: { sectionTitle: {
fontSize: 14, fontSize: 14,
fontWeight: '800', color: colors.text,
color: '#334155',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: 0.5,
}, },
// Product Identity // Product Identity
identityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }, identityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
productNameText: { fontSize: 20, fontWeight: '900', color: '#1e293b', flex: 1 }, productNameText: { fontSize: 20, color: colors.text, flex: 1 },
categoryBadge: { backgroundColor: '#f1f5f9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 }, categoryBadge: { backgroundColor: colors.border, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
categoryText: { fontSize: 11, fontWeight: '800', color: '#64748b', textTransform: 'uppercase' }, categoryText: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase' },
specGrid: { flexDirection: 'row', gap: 24, marginTop: 5 }, specGrid: { flexDirection: 'row', gap: 24, marginTop: 5 },
specItem: { flex: 1 }, specItem: { flex: 1 },
specValue: { fontSize: 15, fontWeight: '700', color: '#334155', marginTop: 2 }, specValue: { fontSize: 15, color: colors.text, marginTop: 2 },
// Pricing // Pricing
priceTile: { marginBottom: 16 }, priceTile: { marginBottom: 16 },
label: { fontSize: 11, color: '#94a3b8', fontWeight: '700', textTransform: 'uppercase', marginBottom: 4 }, label: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase', marginBottom: 4 },
priceFig: { fontSize: 22, fontWeight: '900', color: colors.black }, priceFig: { fontSize: 22, color: colors.text },
priceWords: { fontSize: 13, color: '#64748b', fontStyle: 'italic', marginTop: 2 }, priceWords: { fontSize: 13, color: colors.textSecondary, fontStyle: 'italic', marginTop: 2 },
hDivider: { height: 1, backgroundColor: '#f1f5f9', marginBottom: 16 }, hDivider: { height: 1, backgroundColor: colors.border, marginBottom: 16 },
row: { flexDirection: 'row', justifyContent: 'space-between' }, row: { flexDirection: 'row', justifyContent: 'space-between' },
// Description // Description
descContent: { fontSize: 15, color: '#475569', lineHeight: 22, fontWeight: '500' }, descContent: { fontSize: 15, color: colors.textSecondary, lineHeight: 22, fontFamily: typography.fonts.medium },
// Table // Table
docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', marginBottom: 10 }, docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: colors.border, marginBottom: 10 },
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 }, tableLabel: { fontSize: 11, color: colors.textSecondary, flex: 1 },
docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 }, docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background, padding: 12, borderRadius: 12 },
docIndex: { flex: 1, fontSize: 14, fontWeight: '700', color: '#64748b' }, docIndex: { flex: 1, fontSize: 14, color: colors.textSecondary },
docTitle: { fontSize: 14, fontWeight: '700', color: '#1e293b' }, docTitle: { fontSize: 14, color: colors.text },
downloadIconBox: { backgroundColor: '#fff', padding: 8, borderRadius: 10, borderWidth: 1, borderColor: '#e2e8f0' }, downloadIconBox: { backgroundColor: colors.surface, padding: 8, borderRadius: 10, borderWidth: 1, borderColor: colors.border },
// Footer // Footer
footer: { footer: {
position: 'absolute', position: 'absolute',
bottom: 0, width: '100%', bottom: 0, width: '100%',
backgroundColor: '#fff', backgroundColor: colors.surface,
padding: 20, padding: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20, paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1, borderTopColor: '#f1f5f9' borderTopWidth: 1, borderTopColor: colors.border
}, },
primaryBtn: { primaryBtn: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
height: 56, borderRadius: 18, height: 56, borderRadius: 18,
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10 flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10
}, },
primaryBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' } primaryBtnText: { color: colors.buttonText, fontSize: 16, fontFamily: typography.fonts.bold }
}); });
export default memo(AuctionItemDetail); export default memo(AuctionItemDetail);
@@ -25,10 +25,8 @@ const FilterModal: React.FC<Props> = ({ visible, onClose, setFilter, filter }) =
if (pickerType === 'FROM') { if (pickerType === 'FROM') {
setFromDate(date); setFromDate(date);
console.log('FROM date set');
} else { } else {
setToDate(date); setToDate(date);
console.log('TO date set');
} }
setOpenPicker(false); setOpenPicker(false);
+3 -3
View File
@@ -3,7 +3,7 @@ import { View, Text, StyleSheet, FlatList, RefreshControl, ActivityIndicator } f
import LiveAuctionTile from './LiveAuctionTile'; import LiveAuctionTile from './LiveAuctionTile';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import moment from 'moment'; import moment from 'moment';
import { useFetchLiveAuctionPage, useFetchPurchaseAuction } from '../../Services/BBPS/Hooks'; import { useFetchLiveAuctionPage, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
import PurchaseAuctionTile from './PurchaseAuctionTile'; import PurchaseAuctionTile from './PurchaseAuctionTile';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { setAuction } from '../../Redux/Slices/SelectedAuction'; import { setAuction } from '../../Redux/Slices/SelectedAuction';
@@ -36,7 +36,7 @@ const PurchaseAuction = () => {
function isAuctionEnd(item: any): boolean { function isAuctionEnd(item: any): boolean {
const now = moment(); const now = moment();
const end = moment(item?.keyDates?.auctionBidding?.endDateTime, format); const end = moment(item.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.endDateTime : item?.keyDates?.auctionBidding?.endDateTime, format);
if (!end.isValid()) return false; if (!end.isValid()) return false;
@@ -64,7 +64,7 @@ const PurchaseAuction = () => {
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} /> <SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} filter={false} />
{isPending ? <Skelton /> {isPending ? <Skelton />
: :
<FlatList <FlatList
+375 -175
View File
@@ -1,51 +1,58 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native'; import {
import { InsideHeader, Loader } from '../../Component/Index'; View,
import colors from '../../Constant/Color'; FlatList,
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native'; StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import Animated, { FadeInDown, FadeIn } from 'react-native-reanimated';
import {
Calendar,
Hash,
Wifi,
WifiOff,
Receipt,
CheckCircle2,
Gavel,
Building2,
BadgeIndianRupee,
} from 'lucide-react-native';
import { InsideHeader } from '../../Component/Index';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/BBPS/Hooks'; import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/ApiCall/Hooks';
import { showSuccessAlert } from '../../Constant/ShowDailog'; import { showSuccessAlert } from '../../Constant/ShowDailog';
import { clearPurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction'; import { clearPurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native'; import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import Skeleton from '../../Component/Skelton/Skelton'; import Skeleton from '../../Component/Skelton/Skelton';
import { useTheme } from '../../Hooks/useTheme';
const dummyAuctionList = [ import AppText from '../../components/AppText';
{
auctionId: 1,
auctionNumber: 'AUC-2025-001',
auctionPattern: 'Single Stage',
auctionDispatchDate: '25 Feb 2025',
auctionDispatcherNumber: 'DISP-001',
auctionFees: 2500,
gst: 18,
gstAmount: 450,
portalCharge: 100,
payableAmount: 3050,
},
];
const PurchaseAuctionList = () => { const PurchaseAuctionList = () => {
const Navigation = useNavigation() const Navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [list, setList] = useState([]); const [list, setList] = useState<any[]>([]);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const LoginUser = useSelector((state: any) => state.token.token); const LoginUser = useSelector((state: any) => state.token.token);
const { mutate, isPending } = useOfflineAuctionPurchase()
const { mutate, isPending } = useOfflineAuctionPurchase();
const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList(); const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList();
const calculateTotalAmount = (item: any) => { const calculateTotalAmount = (item: any) => {
const gstAmount = const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) * (Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100); (Number(item.gst) / 100);
return Number(item.auctionFees) + Number(item.portalCharge) + gstAmount;
};
const payableAmount = const grandTotal = list.reduce(
Number(item.auctionFees) + Number(item.portalCharge) + gstAmount; (sum: number, item: any) => sum + calculateTotalAmount(item),
return payableAmount 0,
} );
const handleOfflinePurchase = async () => { const handleOfflinePurchase = async () => {
const body = { const body = {
@@ -59,233 +66,426 @@ const PurchaseAuctionList = () => {
gst: b?.gst, gst: b?.gst,
gstAmount: b?.gstAmount, gstAmount: b?.gstAmount,
payableAmount: calculateTotalAmount(b), payableAmount: calculateTotalAmount(b),
portalCharge: b?.portalCharge portalCharge: b?.portalCharge,
})), })),
orderNo: '' orderNo: '',
}; };
try { try {
mutate(body, { mutate(body, {
onSuccess: (res) => { onSuccess: res => {
if (res.statusCode == 200) { if (res.statusCode === 200) {
showSuccessAlert(res.message) showSuccessAlert(res.message);
Dispatch(clearPurchaseAuction()) Dispatch(clearPurchaseAuction());
Navigation.dispatch( Navigation.dispatch(
CommonActions.reset({ CommonActions.reset({ index: 0, routes: [{ name: NavigationString.Home }] }),
index: 0,
routes: [{ name: NavigationString.Home }],
})
); );
} }
}, },
onError: (error) => { onError: error => console.log(error, 'purchase Error'),
console.log(error, "================purchse Error"); });
}
})
} catch (error) { } catch (error) {
console.log("purchase offline error", error); console.log('purchase offline error', error);
}
} }
};
const handlePurchase = (type: 'online' | 'offline') => { const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true); setLoading(true);
setTimeout(() => { setTimeout(() => {
setLoading(false); setLoading(false);
type === 'online' ? Alert.alert('Online Purchase Successful!') : handleOfflinePurchase(); type === 'online' ? undefined : handleOfflinePurchase();
}, 1200); }, 1200);
}; };
const renderItem = ({ item }: { item: typeof dummyAuctionList[0] }) => (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.title}>{item.auctionNumber}</Text>
<Text style={styles.pattern}>{item.auctionPattern}</Text>
</View>
<View style={styles.cardRow}>
<Calendar size={18} color={colors.primary} />
<Text style={styles.text}>Dispatch Date: {item.auctionDispatchDate}</Text>
</View>
<View style={styles.cardRow}>
<User size={18} color={colors.primary} />
<Text style={styles.text}>Dispatcher No: {item.auctionDispatcherNumber}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Auction Fees: ₹{item.auctionFees}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>GST ({item.gst}%): ₹{item.gstAmount}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Portal Charge: ₹{item.portalCharge}</Text>
</View>
<View style={styles.totalRow}>
<CreditCard size={20} color={colors.white} />
<Text style={styles.total}>Total: ₹{item.payableAmount}</Text>
</View>
</View>
);
const getPurchaseList = () => { const getPurchaseList = () => {
const auctionNumbers: string[] = SelectedPurchaseList.map( const auctionNumbers: string[] = SelectedPurchaseList.map(
(item: { auctionNumber: string }) => item.auctionNumber (item: { auctionNumber: string }) => item.auctionNumber,
); );
purchaseListMutation({ auctionNumbers, contractorId: LoginUser.contractorId }, { purchaseListMutation(
onSuccess: (res) => { { auctionNumbers, contractorId: LoginUser.contractorId },
if (res.statusCode == 200) { {
setList(res.data) onSuccess: res => {
} if (res.statusCode === 200) setList(res.data);
}, },
onError: (error) => { onError: error => console.log(error, 'purchase list error'),
console.log(error, "=================purchase list error"); },
} );
}) };
}
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
getPurchaseList() getPurchaseList();
}, [SelectedPurchaseList]) }, [SelectedPurchaseList]),
); );
const styles = makeStyles(colors, spacing, shadows);
/* ── Render Item ── */
const renderItem = ({ item, index }: { item: any; index: number }) => {
const total = calculateTotalAmount(item);
const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return (
<Animated.View
entering={FadeInDown.delay(index * 90).springify()}
style={styles.card}
>
{/* Top accent stripe */}
<View style={styles.cardAccent} />
<View style={styles.cardBody}>
{/* Header */}
<View style={styles.cardHeader}>
<View style={[styles.indexBadge, { backgroundColor: colors.primary }]}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '800' } as any}>
{String(index + 1).padStart(2, '0')}
</AppText>
</View>
<View style={{ flex: 1, marginLeft: spacing.s }}>
<AppText variant="h4" color={colors.text} numberOfLines={2}>
{item.auctionNumber}
</AppText>
{item.auctionPattern ? (
<View style={[styles.patternBadge, { backgroundColor: `${colors.primary}15` }]}>
<Gavel size={11} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} style={{ fontWeight: '600' } as any}>
{item.auctionPattern}
</AppText>
</View>
) : null}
</View>
</View>
<View style={styles.divider} />
{/* Info Rows */}
<InfoRow
icon={<Hash size={15} color={colors.primary} />}
label="Dispatcher No"
value={item.auctionDispatcherNumber ?? '—'}
colors={colors}
/>
<InfoRow
icon={<Calendar size={15} color={colors.primary} />}
label="Dispatch Date"
value={item.auctionDispatchDate ?? '—'}
colors={colors}
/>
<View style={[styles.divider, { marginVertical: spacing.s }]} />
{/* Fee Breakdown */}
<View style={[styles.feeSection, { backgroundColor: colors.overlay }]}>
<AppText variant="caption" color={colors.textSecondary} style={styles.feeHeader as any}>
FEE BREAKDOWN
</AppText>
<FeeRow label="Auction Fees" value={`₹ ${Number(item.auctionFees).toFixed(2)}`} colors={colors} />
<FeeRow label={`GST (${item.gst}%)`} value={`₹ ${gstAmount.toFixed(2)}`} colors={colors} />
<FeeRow label="Portal Charge" value={`₹ ${Number(item.portalCharge).toFixed(2)}`} colors={colors} />
</View>
{/* Total Banner */}
<View style={[styles.totalBanner, { backgroundColor: colors.primary }]}>
<View style={styles.totalLeft}>
<Receipt size={18} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '600' } as any}>
Total Payable
</AppText>
</View>
<AppText variant="h4" color={colors.buttonText}>
₹ {total.toFixed(2)}
</AppText>
</View>
</View>
</Animated.View>
);
};
return ( return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<InsideHeader title="Purchase List" showArrow />
<View style={styles.screen}> <View style={styles.screen}>
{purchaseListPending ? <Skeleton /> : <FlatList <InsideHeader title="Auction Purchase" showArrow />
{/* Grand Total Strip */}
<Animated.View entering={FadeInDown.springify()} style={styles.grandTotalStrip}>
<View>
<AppText variant="caption" color={colors.textSecondary}>
{list.length} auction{list.length !== 1 ? 's' : ''}
</AppText>
<AppText variant="h3" color={colors.text}>
₹ {grandTotal.toFixed(2)}
</AppText>
</View>
<View style={[styles.grandTotalRight, { backgroundColor: `${colors.success}15` }]}>
<CheckCircle2 size={18} color={colors.success} />
<AppText variant="caption" color={colors.success} style={{ fontWeight: '600' } as any}>
Ready to pay
</AppText>
</View>
</Animated.View>
{/* List */}
{purchaseListPending ? (
<Skeleton />
) : (
<FlatList
data={list} data={list}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item) => item.auctionId.toString()} keyExtractor={item => item.auctionId?.toString()}
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
/>} showsVerticalScrollIndicator={false}
</View> />
)}
<View style={styles.bottomButtonContainer}> {/* Bottom Buttons */}
<View style={styles.footer}>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { marginRight: 8 }]} style={[styles.button, styles.onlineButton, { borderColor: colors.primary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('online')} onPress={() => handlePurchase('online')}
> >
<Text style={styles.buttonText}>Purchase Online</Text> <Wifi size={18} color={colors.primary} />
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} /> <AppText variant="button" color={colors.primary} style={styles.btnText as any}>
Online
</AppText>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { marginLeft: 8 }]} style={[styles.button, styles.offlineButton, { backgroundColor: colors.buttonPrimary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('offline')} onPress={() => handlePurchase('offline')}
> >
{ {isPending ? (
isPending ? <Loader size='small' color={colors.white} /> : <> <ActivityIndicator size="small" color="#fff" />
<Text style={styles.buttonText}>Purchase Offline</Text> ) : (
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} /> <>
<WifiOff size={18} color={colors.buttonText} />
<AppText variant="button" color={colors.buttonText} style={styles.btnText as any}>
Offline
</AppText>
</> </>
} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Loading Overlay */}
{(loading || isPending) && (
<Animated.View entering={FadeIn.duration(200)} style={styles.loadingOverlay}>
<View style={[styles.loadingCard, { backgroundColor: colors.surface }]}>
<ActivityIndicator size="large" color={colors.primary} />
<AppText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: spacing.s } as any}>
Processing purchase…
</AppText>
</View>
</Animated.View>
)}
</View> </View>
); );
}; };
export default PurchaseAuctionList; export default PurchaseAuctionList;
const styles = StyleSheet.create({ /* ─── Sub-components ─── */
const InfoRow = ({
icon,
label,
value,
colors,
}: {
icon: React.ReactNode;
label: string;
value: string;
colors: any;
}) => (
<View style={infoRowStyles.row}>
<View style={infoRowStyles.iconWrap}>{icon}</View>
<AppText variant="caption" color={colors.textSecondary} style={infoRowStyles.label as any}>
{label}
</AppText>
<AppText variant="bodySmall" color={colors.text} style={infoRowStyles.value as any}>
{value}
</AppText>
</View>
);
const infoRowStyles = StyleSheet.create({
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 5, gap: 6 },
iconWrap: { width: 22, alignItems: 'center' },
label: { flex: 1, fontWeight: '500' } as any,
value: { fontWeight: '600' } as any,
});
const FeeRow = ({
label,
value,
colors,
}: {
label: string;
value: string;
colors: any;
}) => (
<View style={feeRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary}>{label}</AppText>
<AppText variant="caption" color={colors.text} style={{ fontWeight: '600' } as any}>{value}</AppText>
</View>
);
const feeRowStyles = StyleSheet.create({
row: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 3 },
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: { screen: {
flex: 1, flex: 1,
backgroundColor: colors.background,
}, },
/* Grand total strip */
grandTotalStrip: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderRadius: 18,
...shadows.medium,
},
grandTotalRight: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
paddingHorizontal: spacing.s + 2,
paddingVertical: spacing.xs + 2,
borderRadius: 20,
},
container: { container: {
padding: 16, paddingHorizontal: spacing.m,
paddingTop: spacing.s,
paddingBottom: 120, paddingBottom: 120,
}, },
/* Card */
card: { card: {
backgroundColor: '#fff', backgroundColor: colors.surface,
borderRadius: 16, borderRadius: 20,
padding: 16, marginBottom: spacing.m,
marginBottom: 16, overflow: 'hidden',
elevation: 5, ...shadows.medium,
shadowColor: '#000', },
shadowOpacity: 0.12, cardAccent: {
shadowOffset: { width: 0, height: 3 }, height: 4,
shadowRadius: 8, backgroundColor: colors.primary,
},
cardBody: {
padding: spacing.m,
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'flex-start',
marginBottom: 12, marginBottom: spacing.s,
}, },
title: { indexBadge: {
fontSize: 16, width: 32,
fontWeight: '700', height: 32,
color: '#0F172A', borderRadius: 10,
},
pattern: {
fontSize: 14,
color: colors.primary,
fontWeight: '600',
},
cardRow: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 6,
marginBottom: 4,
},
text: {
fontSize: 14,
color: '#555',
},
totalRow: {
marginTop: 10,
backgroundColor: colors.primary,
borderRadius: 12,
paddingVertical: 10,
flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
marginTop: 2,
},
patternBadge: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 4,
alignSelf: 'flex-start',
paddingHorizontal: spacing.s,
paddingVertical: 2,
borderRadius: 8,
marginTop: 4,
}, },
total: { divider: {
fontSize: 15, height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginVertical: spacing.s,
},
feeSection: {
borderRadius: 12,
padding: spacing.s + 2,
marginBottom: spacing.s,
},
feeHeader: {
letterSpacing: 1.2,
marginBottom: spacing.xs + 2,
fontWeight: '700', fontWeight: '700',
color: '#fff', } as any,
totalBanner: {
borderRadius: 14,
paddingVertical: 12,
paddingHorizontal: spacing.m,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}, },
bottomButtonContainer: { totalLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
/* Footer */
footer: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0,
flexDirection: 'row', flexDirection: 'row',
backgroundColor: colors.white, paddingHorizontal: spacing.m,
width: '100%', paddingTop: spacing.s + 4,
paddingVertical: 20, paddingBottom: spacing.m + 4,
paddingHorizontal: 10, backgroundColor: colors.surface,
elevation: 5 gap: spacing.s,
...shadows.strong,
}, },
button: { button: {
flex: 1, flex: 1,
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 14,
borderRadius: 16,
gap: spacing.s,
}, },
buttonText: { onlineButton: {
color: '#fff', backgroundColor: colors.surface,
fontWeight: '700', borderWidth: 1.5,
fontSize: 14,
}, },
offlineButton: {
...shadows.medium,
},
btnText: {
letterSpacing: 1.2,
} as any,
/* Loading overlay */
loadingOverlay: { loadingOverlay: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.2)', backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
zIndex: 10, zIndex: 10,
}, },
loadingCard: {
borderRadius: 20,
padding: spacing.l,
alignItems: 'center',
...shadows.strong,
},
}); });
+220 -185
View File
@@ -1,263 +1,298 @@
import React from 'react'; import React, { useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native'; import { View, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import moment from 'moment'; import moment from 'moment';
import CountdownTimer from '../../Component/Counter/CountdownTimer'; import CountdownTimer from '../../Component/Counter/CountdownTimer';
import colors from '../../Constant/Color';
import AuctionDetail from './Modal/AuctionDetail'; import AuctionDetail from './Modal/AuctionDetail';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import {
Tag,
Calendar,
IndianRupee,
ArrowRight,
CircleDot,
Clock,
TimerOff,
} from 'lucide-react-native';
const PurchaseAuctionTile = ({ item, onPressDetails, onStartBidding, buttonEnable, isAuctionEnd }: any) => { const PurchaseAuctionTile = ({
item,
onPressDetails,
onStartBidding,
buttonEnable,
isAuctionEnd,
}: any) => {
const { colors, spacing, shadows } = useTheme();
const [showDetail, setShowDetail] = React.useState(false); const [showDetail, setShowDetail] = React.useState(false);
const scale = useSharedValue(1); const scale = useSharedValue(1);
const elevation = useSharedValue(8);
const animatedStyle = useAnimatedStyle(() => { const animatedStyle = useAnimatedStyle(() => ({
return {
transform: [{ scale: scale.value }], transform: [{ scale: scale.value }],
elevation: elevation.value, // Android-safe }));
};
}, []);
const handlePressIn = () => { const handlePressIn = () => scale.value = withTiming(0.98, { duration: 140 });
scale.value = withTiming(0.97, { duration: 120 }); const handlePressOut = () => scale.value = withTiming(1, { duration: 160 });
elevation.value = withTiming(3, { duration: 120 });
};
const handlePressOut = () => { const isAmended = item?.auctionStatus === 'Amended';
scale.value = withTiming(1, { duration: 160 }); const biddingEnd = isAmended
elevation.value = withTiming(8, { duration: 160 }); ? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '')
}; : item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', '');
const biddingStart = isAmended
? item?.amendedkeyDates?.auctionBidding?.startDateTime?.replace(' kk', '')
: item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', '');
const statusColor = isAuctionEnd ? '#EF4444' : buttonEnable ? colors.primary : '#94A3B8'; const statusConfig = useMemo(() => {
if (isAuctionEnd) return { label: 'ENDED', color: colors.error, bg: `${colors.error}15`, Icon: TimerOff };
if (buttonEnable) return { label: 'LIVE', color: colors.success, bg: `${colors.success}15`, Icon: CircleDot };
return { label: 'PENDING', color: colors.textSecondary, bg: `${colors.textSecondary}12`, Icon: Clock };
}, [isAuctionEnd, buttonEnable, colors]);
const lightStatusBg = isAuctionEnd ? '#FEF2F2' : buttonEnable ? '#EFF6FF' : '#F8FAFC'; const styles = makeStyles(colors, spacing);
if (showDetail) {
return <AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />;
}
return ( return (
<View style={{ flex: 1 }}> <Animated.View
{!showDetail ? <Animated.View entering={FadeInUp.duration(450).springify()}
entering={FadeInUp.duration(400)} style={[styles.container, shadows.medium, animatedStyle]}
style={[styles.shadowContainer, animatedStyle]}
> >
<TouchableOpacity <TouchableOpacity
activeOpacity={1} activeOpacity={1}
onPress={() => setShowDetail(true)} onPress={() => setShowDetail(true)}
onPressIn={handlePressIn} onPressIn={handlePressIn}
onPressOut={handlePressOut} onPressOut={handlePressOut}
style={styles.cardInternal} style={styles.card}
> >
<View style={[styles.statusAccent, { backgroundColor: statusColor }]} /> {/* ── Header ── */}
<View style={styles.header}>
<View style={styles.headerTop}>
{/* Status pill */}
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bg }]}>
<statusConfig.Icon size={11} color={statusConfig.color} strokeWidth={3} />
<AppText variant="caption" color={statusConfig.color} style={styles.statusText as any}>
{statusConfig.label}
</AppText>
</View>
{/* Auction number */}
<View style={styles.numberRow}>
<Tag size={12} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary} style={styles.numberText as any}>
{item?.auctionNumber}
</AppText>
</View>
</View>
<View style={styles.mainInfo}> {/* Work title */}
{/* Header */} <AppText variant="bodySmall" color={colors.text} numberOfLines={2} style={styles.workTitle as any}>
<View style={styles.headerRow}> {item?.auctionOfWork ?? item?.auctionNumber ?? 'Auction Details'}
</AppText>
</View>
<View style={styles.divider} />
{/* ── Info Grid ── */}
<View style={styles.infoGrid}>
{/* Fee */}
<View style={styles.infoRow}>
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<IndianRupee size={17} color={colors.primary} strokeWidth={2} />
</View>
<View> <View>
<View style={[styles.statusBadge, { backgroundColor: lightStatusBg }]}> <AppText variant="caption" color={colors.textSecondary}>Auction Fee</AppText>
<View style={[styles.statusDot, { backgroundColor: statusColor }]} /> <AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
<Text style={[styles.statusText, { color: statusColor }]}> ₹{Number(item?.auctionFees ?? 0).toLocaleString('en-IN')}
{isAuctionEnd ? 'ENDED' : buttonEnable ? 'LIVE' : 'PENDING'} </AppText>
</Text>
</View>
<Text style={styles.title}>{item.auctionNumber}</Text>
</View>
<View style={styles.amountContainer}>
<Text style={styles.amount}>
₹{item.auctionFees?.toLocaleString()}
</Text>
<Text style={styles.feeLabel}>Auction Fee</Text>
</View> </View>
</View> </View>
{/* Date */} {/* Bidding End */}
<View style={styles.dateRow}> <View style={styles.infoRow}>
<Text style={styles.dateText}> <View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<Text style={styles.dateLabel}>Bidding Start: </Text> <Calendar size={17} color={colors.primary} strokeWidth={2} />
{moment( </View>
item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', ''), <View>
'DD-MM-YYYY HH:mm:ss' <AppText variant="caption" color={colors.textSecondary}>Bidding Ends</AppText>
).format('DD MMM YYYY, hh:mm A')} <AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
</Text> {biddingEnd
<Text style={styles.dateText}> ? moment(biddingEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')
<Text style={styles.dateLabel}>Bidding Ends: </Text> : '—'}
{moment( </AppText>
item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', ''), </View>
'DD-MM-YYYY HH:mm:ss' </View>
).format('DD MMM YYYY, hh:mm A')}
</Text>
</View> </View>
{/* Footer */} {/* ── Footer ── */}
<View style={styles.footer}> <View style={styles.footer}>
<View style={styles.footerTop}> <AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' } as any}>
<Text style={styles.footerLabel}>Remaining Time</Text> Time Remaining
</AppText>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
{/* Timer */}
<View style={styles.timerSection}>
{buttonEnable && !isAuctionEnd ? ( {buttonEnable && !isAuctionEnd ? (
<CountdownTimer <CountdownTimer endDate={biddingEnd} />
endDate={item.keyDates.auctionBidding.endDateTime}
/>
) : ( ) : (
<Text style={styles.endedText}>--:--:--</Text> <AppText variant="bodySmall" color={colors.textSecondary} style={{ fontWeight: '700' } as any}>
-- : -- : --
</AppText>
)} )}
</View> </View>
{/* CTA Button */}
<TouchableOpacity <TouchableOpacity
disabled={!buttonEnable} disabled={!buttonEnable || isAuctionEnd}
onPress={onStartBidding} onPress={onStartBidding}
activeOpacity={0.85} activeOpacity={0.85}
style={[ style={[
styles.actionButton, styles.ctaBtn,
{ backgroundColor: statusColor }, {
!buttonEnable && styles.disabledButton, backgroundColor:
isAuctionEnd
? colors.error
: buttonEnable
? colors.primary
: colors.border,
opacity: (!buttonEnable || isAuctionEnd) ? 0.8 : 1,
},
]} ]}
> >
<Text style={styles.addText}> <AppText
{isAuctionEnd variant="button"
? 'Bidding Closed' color={colors.buttonText}
: !buttonEnable style={styles.ctaText as any}
? 'Pending Bid' >
: 'Proceed to Start Bidding'} {isAuctionEnd ? 'Ended' : !buttonEnable ? 'Pending' : 'Bid Now'}
</Text> </AppText>
{buttonEnable && !isAuctionEnd && (
<ArrowRight size={15} color={colors.buttonText} strokeWidth={2.5} />
)}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</Animated.View> : </Animated.View>
<AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
}
</View>
); );
}; };
export default PurchaseAuctionTile; export default PurchaseAuctionTile;
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any) =>
shadowContainer: { StyleSheet.create({
marginHorizontal: 5, container: {
marginTop: 14, marginHorizontal: spacing.s,
marginBottom: 6, marginBottom: spacing.m,
borderRadius: 16, borderRadius: 16,
backgroundColor: '#fff', borderWidth: 1,
elevation: 8, borderColor: colors.border,
backgroundColor: colors.surface,
}, },
cardInternal: { card: {
borderRadius: 16, borderRadius: 16,
overflow: 'hidden', backgroundColor: colors.surface,
flexDirection: 'row', padding: spacing.m,
backgroundColor: '#fff', borderWidth: 1,
borderColor: 'rgba(0,0,0,0.03)',
}, },
statusAccent: {
width: 5, /* Header */
height: '100%', header: {
marginBottom: spacing.m,
}, },
mainInfo: { headerTop: {
flex: 1,
padding: 16,
},
headerRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'center',
marginBottom: 12, marginBottom: spacing.s,
}, },
statusBadge: { statusBadge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 8, paddingHorizontal: 10,
paddingVertical: 3, paddingVertical: 5,
borderRadius: 6, borderRadius: 20,
marginBottom: 6, gap: 5,
alignSelf: 'flex-start',
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 5,
}, },
statusText: { statusText: {
fontSize: 10,
fontWeight: '800', fontWeight: '800',
fontSize: 10,
textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: 0.5,
} as any,
numberRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
}, },
title: { numberText: {
fontWeight: '800', letterSpacing: 0.4,
fontSize: 16,
color: '#1E293B',
},
amountContainer: {
alignItems: 'flex-end',
},
amount: {
fontWeight: '900',
fontSize: 18,
color: '#0F172A',
},
feeLabel: {
fontSize: 10,
color: '#64748B',
fontWeight: '500',
},
dateRow: {
backgroundColor: '#F8FAFC',
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 10,
marginBottom: 16,
borderWidth: 1,
borderColor: '#F1F5F9',
},
dateText: {
fontSize: 12,
color: '#334155',
fontWeight: '600', fontWeight: '600',
} as any,
workTitle: {
fontWeight: '600',
lineHeight: 20,
} as any,
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginBottom: spacing.m,
opacity: 0.6,
}, },
dateLabel: {
fontWeight: '400', /* Info grid */
color: '#64748B', infoGrid: {
},
footer: {
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
paddingTop: 12,
},
footerTop: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: spacing.l,
gap: spacing.s,
},
infoRow: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, gap: 10,
flex: 1,
}, },
footerLabel: { iconBox: {
fontSize: 11, width: 36,
fontWeight: '600', height: 36,
color: '#94A3B8', borderRadius: 10,
textTransform: 'uppercase', alignItems: 'center',
},
endedText: {
fontSize: 13,
fontWeight: '700',
color: '#CBD5E1',
},
actionButton: {
width: '100%',
height: 48,
borderRadius: 12,
justifyContent: 'center', justifyContent: 'center',
},
infoValue: {
fontWeight: '700',
} as any,
/* Footer */
footer: {
justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
...Platform.select({ backgroundColor: colors.background,
ios: { marginHorizontal: -spacing.m,
shadowColor: '#000', marginBottom: -spacing.m,
shadowOffset: { width: 0, height: 2 }, padding: spacing.m,
shadowOpacity: 0.1, borderBottomLeftRadius: 16,
shadowRadius: 4, borderBottomRightRadius: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
}, },
}), timerSection: {
flex: 1,
}, },
disabledButton: { ctaBtn: {
backgroundColor: '#E2E8F0', flexDirection: 'row',
}, alignItems: 'center',
addText: { justifyContent: 'center',
color: '#fff', paddingVertical: 10,
fontSize: 14, paddingHorizontal: 18,
fontWeight: '800', borderRadius: 12,
letterSpacing: 0.2, gap: 5,
}, },
ctaText: {
letterSpacing: 0.8,
} as any,
}); });
+325 -158
View File
@@ -1,220 +1,387 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; import {
import Animated, { FadeIn, FadeOut, Layout, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; View,
import { ChevronDown, Trash2, Info } from 'lucide-react-native'; StyleSheet,
ScrollView,
TouchableOpacity,
} from 'react-native';
import Animated, {
FadeIn,
FadeOut,
FadeInDown,
Layout,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, Gavel, Tag } from 'lucide-react-native';
import { InsideHeader } from '../../Component/Index'; import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction'; import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const PurchaseList = () => { const PurchaseList = () => {
const Navigation: any = useNavigation() const Navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [expandedIndex, setExpandedIndex] = useState<number | null>(null); const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const toggleExpand = (index: number) => { const toggleExpand = (index: number) => {
setExpandedIndex(prev => (prev === index ? null : index)); setExpandedIndex(prev => (prev === index ? null : index));
}; };
useEffect(() => {
if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) {
Navigation.goBack();
}
}, [SelectedPurchaseList]);
const styles = makeStyles(colors, spacing, shadows);
return ( return (
<View style={styles.screen}> <View style={styles.screen}>
<InsideHeader title="Purchase" showArrow /> <InsideHeader title="Purchase Auctions" showArrow />
<ScrollView contentContainerStyle={styles.container}> {/* Summary Strip */}
{/* Header */} <View style={styles.summaryStrip}>
<View style={styles.header}> <View style={styles.summaryIcon}>
<Text style={styles.heading}>Purchase List</Text> <ShoppingBag size={16} color={colors.primary} />
{/* <Info size={20} color="#0F172A" /> */} </View>
<AppText variant="caption" color={colors.textSecondary}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' } as any}>
{SelectedPurchaseList.length}
</AppText>
{' '}auction{SelectedPurchaseList.length !== 1 ? 's' : ''} selected for purchase
</AppText>
</View> </View>
{/* Cards */} <ScrollView
{SelectedPurchaseList.map((item: any, index: any) => { contentContainerStyle={styles.container}
const expanded = expandedIndex === index; showsVerticalScrollIndicator={false}
return ( >
<Animated.View {SelectedPurchaseList.map((item: any, index: number) => (
<PurchaseListItem
key={item.auctionId} key={item.auctionId}
layout={Layout.springify()} item={item}
style={styles.card} index={index}
> expandedIndex={expandedIndex}
<TouchableOpacity toggleExpand={toggleExpand}
activeOpacity={0.9} colors={colors}
onPress={() => toggleExpand(index)} styles={styles}
> Dispatch={Dispatch}
<CardHeader
title={item.auctionOfWork}
expanded={expanded}
/> />
))}
<Text style={styles.subTitle}> <View style={{ height: 90 }} />
Auction No: {item.auctionNumber}
</Text>
<Text style={styles.subTitle}>
Status:{' '}
<Text
style={{
color: item.status ? '#16A34A' : '#DC2626',
fontWeight: '700',
}}
>
{item.status ? 'Open' : 'Closed'}
</Text>
</Text>
</TouchableOpacity>
{/* Expanded Content */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(180)}
layout={Layout.springify()}
style={styles.details}
>
{renderRow('Auction Mode', item.auctionMode)}
{renderRow('Pattern', item.auctionPattern)}
{renderRow('Fees', `₹ ${item.auctionFees}`)}
{renderRow('Department', item.deptName)}
{renderRow('Purchase End', item.purchaseEnd)}
<TouchableOpacity style={styles.removeButton} onPress={() => { Dispatch(togglePurchaseAuction(item)) }}>
<Trash2 size={18} color="#fff" />
<Text style={styles.removeText}>Remove</Text>
</TouchableOpacity>
</Animated.View>
)}
</Animated.View>
);
})}
</ScrollView> </ScrollView>
{/* Footer */} {/* Floating Proceed Button */}
<TouchableOpacity disabled={Boolean(!SelectedPurchaseList.length)} style={[styles.proceedButton, { backgroundColor: Boolean(!SelectedPurchaseList.length) ? colors.lightText : colors.primary }]} onPress={() => { Navigation.navigate(NavigationString.PurchaseAuctionList) }}> <View style={styles.footerContainer}>
<Text style={styles.proceedText}>Proceed to Purchase</Text> <TouchableOpacity
activeOpacity={0.9}
disabled={!SelectedPurchaseList.length}
style={[
styles.proceedButton,
{
backgroundColor: SelectedPurchaseList.length
? colors.buttonPrimary
: colors.secondary,
},
]}
onPress={() => Navigation.navigate(NavigationString.PurchaseAuctionList)}
>
<AppText variant="button" color={colors.buttonText} style={styles.proceedText as any}>
Proceed to Purchase
</AppText>
<View style={styles.countBadge}>
<AppText variant="caption" color={colors.buttonPrimary} style={{ fontWeight: '800' } as any}>
{SelectedPurchaseList.length}
</AppText>
</View>
<ChevronRight size={20} color={colors.buttonText} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View>
); );
}; };
export default PurchaseList; export default PurchaseList;
const CardHeader = ({ title, expanded }: { /* ─── Sub-components ─── */
title: string;
expanded: boolean; const PurchaseListItem = ({ item, index, expandedIndex, toggleExpand, colors, styles, Dispatch }: any) => {
}) => { const expanded = expandedIndex === index;
const rotation = useSharedValue(expanded ? 1 : 0); const isActive = item.status;
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 }); const chevronStyle = useChevronStyle(expanded);
const arrowStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
return ( return (
<View style={styles.cardHeader}> <Animated.View
<Text style={styles.title}>{title}</Text> entering={FadeInDown.delay(index * 80).springify()}
<Animated.View style={arrowStyle}> layout={Layout.springify()}
<ChevronDown size={22} color={colors.black} /> style={[styles.card, expanded && styles.cardExpanded]}
>
{/* Left Accent Bar */}
<View style={[styles.accentBar, { backgroundColor: isActive ? colors.success : colors.primary }]} />
<View style={styles.cardInner}>
<TouchableOpacity
activeOpacity={0.85}
onPress={() => toggleExpand(index)}
>
{/* Badge Row */}
<View style={styles.badgeRow}>
<View style={styles.auctionBadge}>
<Gavel size={11} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} style={styles.badgeText as any}>
{item.auctionNumber}
</AppText>
</View>
</View>
{/* Title + Chevron */}
<View style={styles.titleRow}>
<AppText variant="h4" style={styles.title as any} numberOfLines={2}>
{item.auctionOfWork ?? item.auctionNumber}
</AppText>
<Animated.View style={chevronStyle}>
<ChevronDown size={20} color={colors.textSecondary} />
</Animated.View> </Animated.View>
</View> </View>
{/* Dept Tag */}
<View style={styles.deptRow}>
<Tag size={12} color={colors.textSecondary} />
<AppText variant="caption" color={colors.textSecondary} style={styles.deptText as any}>
{item.deptName || 'Department'}
</AppText>
</View>
</TouchableOpacity>
{/* Expanded Section */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(160)}
layout={Layout.springify()}
style={styles.detailsContainer}
>
<View style={styles.divider} />
<DetailRow label="Auction Mode" value={item.auctionMode} colors={colors} />
<DetailRow label="Pattern" value={item.auctionPattern} colors={colors} />
<DetailRow label="Auction Fees" value={`₹ ${item.auctionFees}`} colors={colors} valueColor={colors.primary} />
<DetailRow label="Department" value={item.deptName} colors={colors} />
<DetailRow label="Purchase End" value={item.purchaseEnd} colors={colors} />
{/* Remove Button */}
<TouchableOpacity
style={styles.removeButton}
activeOpacity={0.85}
onPress={() => Dispatch(togglePurchaseAuction(item))}
>
<Trash2 size={16} color="#fff" />
<AppText variant="button" color="#fff" style={styles.removeText as any}>
Remove
</AppText>
</TouchableOpacity>
</Animated.View>
)}
</View>
</Animated.View>
); );
}; };
const renderRow = (label: string, value: string) => ( const DetailRow = ({
<View style={styles.row}> label,
<Text style={styles.label}>{label}</Text> value,
<Text style={styles.value}>{value}</Text> colors,
valueColor,
}: {
label: string;
value: string;
colors: any;
valueColor?: string;
}) => (
<View style={detailRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary} style={detailRowStyles.label as any}>
{label}
</AppText>
<AppText variant="caption" color={valueColor || colors.text} style={detailRowStyles.value as any}>
{value ?? '—'}
</AppText>
</View> </View>
); );
const detailRowStyles = StyleSheet.create({
const styles = StyleSheet.create({ row: {
screen: {
flex: 1,
backgroundColor: '#F8FAFC',
},
container: {
padding: 16,
paddingBottom: 110,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 14,
},
heading: {
fontSize: 22,
fontWeight: '800',
color: colors.black,
},
card: {
backgroundColor: '#fff',
borderRadius: 18,
padding: 16,
marginBottom: 14,
borderWidth: 1,
borderColor: '#E5E7EB',
},
cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
}, paddingVertical: 8,
title: { borderBottomWidth: StyleSheet.hairlineWidth,
fontSize: 16, borderBottomColor: 'rgba(0,0,0,0.06)',
fontWeight: '700',
color: '#0F172A',
flex: 1,
marginRight: 12,
},
subTitle: {
fontSize: 14,
color: '#64748B',
marginTop: 6,
},
details: {
marginTop: 16,
paddingTop: 14,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
row: {
marginVertical: 6,
}, },
label: { label: {
fontSize: 13, flex: 1,
color: '#64748B', fontWeight: '500',
}, } as any,
value: { value: {
fontSize: 15, flex: 1,
textAlign: 'right',
fontWeight: '600', fontWeight: '600',
color: '#0F172A', } as any,
});
/* ─── Chevron animated style hook ─── */
function useChevronStyle(expanded: boolean) {
const rotation = useSharedValue(expanded ? 1 : 0);
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
return useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
}
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: {
flex: 1,
backgroundColor: colors.background,
},
summaryStrip: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
borderRadius: 14,
gap: spacing.s,
...shadows.light,
},
summaryIcon: {
width: 28,
height: 28,
borderRadius: 8,
backgroundColor: `${colors.primary}15`,
alignItems: 'center',
justifyContent: 'center',
},
container: {
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
},
card: {
backgroundColor: colors.surface,
borderRadius: 20,
marginBottom: spacing.m,
flexDirection: 'row',
overflow: 'hidden',
...shadows.medium,
},
cardExpanded: {
...shadows.strong,
},
accentBar: {
width: 5,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
},
cardInner: {
flex: 1,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
},
badgeRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: spacing.s,
},
auctionBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: `${colors.primary}15`,
paddingHorizontal: spacing.s,
paddingVertical: 3,
borderRadius: 8,
gap: 4,
},
badgeText: {
fontWeight: '600',
letterSpacing: 0.2,
} as any,
titleRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: spacing.s,
},
title: {
flex: 1,
fontWeight: '700',
} as any,
deptRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
marginTop: spacing.xs + 2,
},
deptText: {
letterSpacing: 0.3,
} as any,
detailsContainer: {
marginTop: spacing.s,
},
divider: {
height: 1,
backgroundColor: colors.border,
marginBottom: spacing.s,
}, },
removeButton: { removeButton: {
marginTop: 18, marginTop: spacing.m,
backgroundColor: colors.primary, backgroundColor: colors.error,
borderRadius: 14, borderRadius: 14,
paddingVertical: 12, paddingVertical: 12,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
gap: 8, alignItems: 'center',
gap: spacing.s,
}, },
removeText: { removeText: {
color: '#fff', letterSpacing: 1,
fontWeight: '700', } as any,
footerContainer: {
position: 'absolute',
bottom: 20,
left: spacing.m,
right: spacing.m,
}, },
proceedButton: { proceedButton: {
position: 'absolute', flexDirection: 'row',
bottom: 16,
left: 16,
right: 16,
backgroundColor: colors.primary,
paddingVertical: 16,
borderRadius: 18,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
paddingVertical: 16,
borderRadius: 20,
gap: spacing.s,
...shadows.strong,
}, },
proceedText: { proceedText: {
color: '#fff', letterSpacing: 1.5,
fontSize: 16, } as any,
fontWeight: '800', countBadge: {
backgroundColor: '#fff',
borderRadius: 12,
minWidth: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
}, },
}); });
+1
View File
@@ -10,6 +10,7 @@ export interface LiveAuctionTileProps {
isPurchased?: boolean; isPurchased?: boolean;
auctionId?: any; auctionId?: any;
auctionNumber?: any; auctionNumber?: any;
isPurchasedByContractor?: boolean
} }
export interface allBooleanProps { export interface allBooleanProps {
+50 -66
View File
@@ -1,9 +1,11 @@
import React, { memo, useCallback, useRef, useState } from 'react'; import React, { memo, useCallback, useRef, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native'; import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native';
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import colors from '../../../Constant/Color'; import colors_const from '../../../Constant/Color';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail'; import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle'; import MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import { Loader } from '../../../Component/Index'; import { Loader } from '../../../Component/Index';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import axios from 'axios'; import axios from 'axios';
@@ -11,6 +13,8 @@ import Variables from '../../../Constant/Variable';
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog'; import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => { const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const Token = useSelector((state: any) => state.token.token) const Token = useSelector((state: any) => state.token.token)
const [showDetail, setShowDetail] = useState(false); const [showDetail, setShowDetail] = useState(false);
const [value, setValue] = useState(`${item.bidAmount}`); const [value, setValue] = useState(`${item.bidAmount}`);
@@ -93,7 +97,6 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
setAllState((prev: any) => ({ ...prev, isLoading: false })) setAllState((prev: any) => ({ ...prev, isLoading: false }))
} }
} }
// console.log(item, 'jkjjj');
return ( return (
<View> <View>
@@ -101,7 +104,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
!showDetail ? <Animated.View !showDetail ? <Animated.View
layout={Layout.springify()} layout={Layout.springify()}
entering={FadeInUp.delay(100)} entering={FadeInUp.delay(100)}
style={[styles.card, styles.leadingCard, { borderColor: item.me ? '#22C55E' : 'red', backgroundColor: item.me ? "#F0FDF4" : '#fdf0f0ff' }]} style={[styles.card, item.me ? styles.leadingCard : undefined, { borderColor: item.me ? colors.success : colors.error, backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10` }]}
> >
{/* HEADER */} {/* HEADER */}
<TouchableOpacity <TouchableOpacity
@@ -110,28 +113,28 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
style={styles.cardHeader} style={styles.cardHeader}
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text> <AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
{/* auctionStartValueFigure */} {/* auctionStartValueFigure */}
<Text style={styles.itemIdText}> <AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
Initial Amt : {item?.auctionStartValueFigure} Initial Amt : {item?.auctionStartValueFigure}
</Text> </AppText>
</View> </View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}> <TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
<View style={[MainStyle.flexBetween, { gap: 10 }]}> <View style={[MainStyle.flexBetween, { gap: 10 }]}>
<View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}> <View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}>
<Text style={[styles.badgeText, item.me ? styles.meText : styles.liveText]}> <AppText variant="caption" bold style={[styles.badgeText as any, item.me ? styles.meText : styles.liveText]}>
{item.me ? 'LEADING' : 'LIVE'} {item.me ? 'LEADING' : 'LIVE'}
</Text> </AppText>
</View> </View>
<Animated.Text style={[styles.arrow, arrowStyle]}> <Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
▼ ▼
</Animated.Text> </Animated.Text>
</View> </View>
<Text style={styles.itemIdText}> <AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
{SelectedAuction?.auctionNumber} {SelectedAuction?.auctionNumber}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
</TouchableOpacity> </TouchableOpacity>
@@ -139,15 +142,15 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
{/* PRICE INFO */} {/* PRICE INFO */}
<View style={styles.priceRow}> <View style={styles.priceRow}>
<View> <View>
<Text style={styles.label}>Current Bid</Text> <AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
<Text style={styles.mainPrice}>₹{item?.bidAmount || item.auctionStartValueFigure}</Text> <AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item?.bidAmount || item.auctionStartValueFigure}</AppText>
</View> </View>
<View style={{ alignItems: 'flex-end' }}> <View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Increment</Text> <AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Increment</AppText>
<Text style={styles.incrementText}> <AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
+₹{item.bidVariationValue} +₹{item.bidVariationValue}
</Text> </AppText>
</View> </View>
</View> </View>
@@ -155,7 +158,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
<Animated.View style={[styles.collapsibleContainer, (!allBoolean.rebid && item.contractorId == undefined) ? bodyStyle : bodyStyle2]}> <Animated.View style={[styles.collapsibleContainer, (!allBoolean.rebid && item.contractorId == undefined) ? bodyStyle : bodyStyle2]}>
<View style={styles.divider} /> <View style={styles.divider} />
<Text style={styles.bidLabel}>Set Your Bid</Text> <AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
{!allBoolean.oneTimeBid && <View style={styles.stepperContainer}> {!allBoolean.oneTimeBid && <View style={styles.stepperContainer}>
<TouchableOpacity <TouchableOpacity
@@ -163,29 +166,29 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
disabled={isMin} disabled={isMin}
style={[styles.stepBtn, isMin && styles.disabledBtn]} style={[styles.stepBtn, isMin && styles.disabledBtn]}
> >
<Text style={styles.stepText}>−</Text> <AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.inputBox}> <View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text> <AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
<Text style={styles.bidValueText}>{bidValue}</Text> <AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
</View> </View>
<TouchableOpacity onPress={onIncrease} style={styles.stepBtn}> <TouchableOpacity onPress={onIncrease} style={styles.stepBtn}>
<Text style={styles.stepText}>+</Text> <AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
</TouchableOpacity> </TouchableOpacity>
</View>} </View>}
{ {
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}> (allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
<Text style={styles.currency2}>₹</Text> <AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
<TextInput <TextInput
value={value} value={value}
onChangeText={handleChange} onChangeText={handleChange}
keyboardType="decimal-pad" keyboardType="decimal-pad"
placeholder="0.00" placeholder="0.00"
style={styles.input} style={[styles.input, { color: colors.text }]}
placeholderTextColor="#94A3B8" placeholderTextColor={colors.textSecondary}
/> />
</View> </View>
} }
@@ -195,7 +198,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid} onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
> >
{ {
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text> allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
} }
</TouchableOpacity> : null} </TouchableOpacity> : null}
</Animated.View> </Animated.View>
@@ -210,11 +213,11 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
export default memo(BiddingCard); export default memo(BiddingCard);
const styles = StyleSheet.create({ const makeStyles = (colors: any) => StyleSheet.create({
inputBox2: { inputBox2: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#F1F5F9', backgroundColor: colors.background,
borderRadius: 12, borderRadius: 12,
height: 45, height: 45,
paddingHorizontal: 12, paddingHorizontal: 12,
@@ -222,39 +225,23 @@ const styles = StyleSheet.create({
marginBottom: 20 marginBottom: 20
}, },
currency2: { currency2: {
fontSize: 16,
fontWeight: 'bold',
color: '#64748B',
marginRight: 4, marginRight: 4,
}, } as any,
input: { input: {
flex: 1, flex: 1,
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: '#0F172A',
width: '100%' width: '100%'
}, },
card: { card: {
backgroundColor: '#FFF',
borderRadius: 24, borderRadius: 24,
padding: 20, padding: 20,
marginBottom: 16, marginBottom: 16,
borderWidth: 1, borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden', overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
}, },
leadingCard: { leadingCard: {
borderColor: '#22C55E',
borderWidth: 2, borderWidth: 2,
backgroundColor: '#F0FDF4',
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
@@ -262,27 +249,24 @@ const styles = StyleSheet.create({
alignItems: 'flex-start', alignItems: 'flex-start',
}, },
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 }, headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' }, productName: {} as any,
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 }, itemIdText: { marginTop: 2 } as any,
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' }, liveBadge: { backgroundColor: `${colors.error}20` },
meBadge: { backgroundColor: '#BBF7D0' }, meBadge: { backgroundColor: `${colors.success}20` },
badgeText: { fontSize: 10, fontWeight: '900' }, badgeText: { fontSize: 10 } as any,
liveText: { color: '#EF4444' }, liveText: { color: colors.error },
meText: { color: '#15803D' }, meText: { color: colors.success },
arrow: { fontSize: 10, color: '#94A3B8' }, arrow: { fontSize: 10 },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 }, priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { label: {
fontSize: 11,
color: '#94A3B8',
textTransform: 'uppercase', textTransform: 'uppercase',
fontWeight: '600', } as any,
}, mainPrice: {} as any,
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' }, incrementText: {} as any,
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' }, collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 }, divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 }, bidLabel: { marginBottom: 10 } as any,
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 }, stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { stepBtn: {
width: 45, width: 45,
@@ -292,19 +276,19 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
disabledBtn: { backgroundColor: '#CBD5E1' }, disabledBtn: { backgroundColor: colors.border },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' }, stepText: {} as any,
inputBox: { inputBox: {
flex: 1, flex: 1,
height: 45, height: 45,
backgroundColor: '#F1F5F9', backgroundColor: colors.background,
borderRadius: 12, borderRadius: 12,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 }, currency: { marginRight: 4 } as any,
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' }, bidValueText: {} as any,
submitBtn: { submitBtn: {
height: 50, height: 50,
backgroundColor: colors.primary, backgroundColor: colors.primary,
@@ -312,5 +296,5 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' }, submitBtnText: {} as any,
}); });
@@ -0,0 +1,133 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { ShieldCheck, X } from 'lucide-react-native';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
visible: boolean;
onClose: () => void;
onConfirm: () => void;
bidAmount: number;
isLoading?: boolean;
}
const ConfirmBidModal = ({ visible, onClose, onConfirm, bidAmount, isLoading }: Props) => {
const { colors, shadows } = useTheme();
if (!visible) return null;
return (
<View style={styles.overlay}>
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
</TouchableOpacity>
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<ShieldCheck size={32} color={colors.primary} strokeWidth={2} />
</View>
<AppText variant="h3" color={colors.text} bold centered style={styles.title}>
Confirm Your Bid
</AppText>
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle}>
Are you sure you want to place this bid? This action cannot be undone.
</AppText>
<View style={[styles.amountBox, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}30` }]}>
<AppText variant="caption" color={colors.primary} bold>
BID AMOUNT
</AppText>
<AppText variant="h1" color={colors.primary} bold>
₹{bidAmount?.toLocaleString('en-IN') || 0}
</AppText>
</View>
<View style={styles.btnRow}>
<TouchableOpacity style={[styles.btn, styles.cancelBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
<AppText variant="button" color={colors.text} bold>Cancel</AppText>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, styles.confirmBtn, { backgroundColor: colors.primary }]} onPress={onConfirm} disabled={isLoading}>
{isLoading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<AppText variant="button" color="#fff" bold>Submit Bid</AppText>
)}
</TouchableOpacity>
</View>
</View>
</View>
);
};
export default ConfirmBidModal;
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
zIndex: 9999,
elevation: 9999,
},
modalCard: {
width: '100%',
borderRadius: 24,
padding: 24,
alignItems: 'center',
},
closeBtn: {
position: 'absolute',
top: 16,
right: 16,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
iconBox: {
width: 64,
height: 64,
borderRadius: 32,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
title: {
marginBottom: 8,
},
subtitle: {
marginBottom: 24,
paddingHorizontal: 10,
},
amountBox: {
width: '100%',
paddingVertical: 16,
borderRadius: 16,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
gap: 4,
},
btnRow: {
flexDirection: 'row',
gap: 12,
width: '100%',
},
btn: {
flex: 1,
height: 50,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
cancelBtn: {},
confirmBtn: {},
});
+178 -74
View File
@@ -1,105 +1,209 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, StyleSheet, TouchableOpacity } from 'react-native';
import LottieView from 'lottie-react-native'; import LottieView from 'lottie-react-native';
import Animated, { FadeInUp } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeInUp, ZoomIn } from 'react-native-reanimated';
import colors from '../../../Constant/Color'; import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import {
ShieldCheck,
CheckCircle2,
FileText,
Gavel,
ArrowRight,
} from 'lucide-react-native';
const STEPS = [
{ icon: FileText, label: 'Documents Submitted', done: true },
{ icon: ShieldCheck, label: 'EMD Verified', done: true },
{ icon: Gavel, label: 'Ready to Bid', done: true },
];
const EmdComplete = ({ onNext }: { onNext?: () => void }) => { const EmdComplete = ({ onNext }: { onNext?: () => void }) => {
return ( const { colors, spacing, shadows } = useTheme();
<View style={styles.container}>
<LottieView
source={require('../../../lottie/confetti.json')}
autoPlay
loop={true}
style={styles.confetti}
/>
return (
<View style={[styles.root, { backgroundColor: colors.background }]}>
{/* ── Center content ── */}
<View style={styles.centerContent}>
{/* Success animation bubble */}
<Animated.View entering={ZoomIn.delay(100).springify()} style={[styles.lottieBubble, { backgroundColor: `${colors.primary}12` }]}>
<LottieView <LottieView
source={require('../../../lottie/Success.json')} source={require('../../../lottie/Success.json')}
autoPlay autoPlay
loop={true} loop={false}
style={styles.success} style={styles.lottie}
/> />
</Animated.View>
<Animated.Text entering={FadeInUp.delay(400).duration(700)} style={styles.title}> {/* Title */}
EMD Complete <Animated.View entering={FadeInUp.delay(400).springify()} style={styles.titleBlock}>
</Animated.Text> <AppText variant="h2" color={colors.text} bold centered>
EMD Submitted!
</AppText>
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle as any}>
Your Earnest Money Deposit has been{'\n'}successfully recorded.
</AppText>
</Animated.View>
<Animated.Text entering={FadeInUp.delay(700).duration(700)} style={styles.subText} > {/* ── Step Completion Chips ── */}
Your submission was successful <Animated.View entering={FadeInDown.delay(600).springify()} style={[styles.stepsCard,{backgroundColor: colors.surface, shadowColor: colors.shadow}]}>
</Animated.Text> {STEPS.map((step, index) => (
<React.Fragment key={index}>
<View style={styles.stepRow}>
<View style={[styles.stepIconBox, { backgroundColor: `${colors.success}18` }]}>
<step.icon size={16} color={colors.success} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} semiBold style={styles.stepLabel as any}>
{step.label}
</AppText>
<CheckCircle2 size={18} color={colors.success} strokeWidth={2.5} />
</View>
{index < STEPS.length - 1 && (
<View style={[styles.stepConnector, { backgroundColor: `${colors.success}30` }]} />
)}
</React.Fragment>
))}
</Animated.View>
<Animated.View entering={FadeInUp.delay(900).duration(700)} style={styles.nextWrapper} > {/* ── Info badge ── */}
<TouchableOpacity activeOpacity={0.85} style={styles.nextBtn} onPress={onNext} > <Animated.View
<Text style={styles.nextText}>Next</Text> entering={FadeInUp.delay(900).springify()}
style={[styles.infoBadge, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}30` }]}
>
<AppText variant="caption" color={colors.primary} semiBold centered>
🎉 You are now eligible to participate in the bidding process
</AppText>
</Animated.View>
</View>
{/* ── Bottom CTA ── */}
<Animated.View entering={FadeInUp.delay(1100).springify()} style={styles.bottomArea}>
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.primary, ...shadows.medium }]}
onPress={onNext}
activeOpacity={0.85}
>
<AppText variant="button" color="#fff" bold style={styles.ctaText as any}>
Continue to Bidding
</AppText>
<ArrowRight size={18} color="#fff" strokeWidth={2.5} />
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
</View> </View>
); );
}; };
export default EmdComplete; export default EmdComplete;
/* ---------------- Styles ---------------- */ /* ─── Styles ─── */
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { root: {
flex: 1, flex: 1,
backgroundColor: '#F8FAFC', alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 40,
},
centerContent: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 24,
gap: 24,
width: '100%',
},
/* Lottie bubble */
lottieBubble: {
width: 160,
height: 160,
borderRadius: 80,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
lottie: {
confetti: { width: 130,
position: 'absolute', height: 130,
top: 0,
width: '100%',
height: '100%',
}, },
success: { /* Title */
width: 220, titleBlock: {
height: 220, alignItems: 'center',
marginTop: -100 gap: 8,
}, },
title: { title: {
marginTop: 16, letterSpacing: 0.3,
fontSize: 24, } as any,
fontWeight: '900', subtitle: {
color: '#0F172A', lineHeight: 22,
opacity: 0.75,
} as any,
/* Steps */
stepsCard: {
width: '100%',
borderRadius: 20,
paddingVertical: 16,
paddingHorizontal: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.06,
shadowRadius: 12,
elevation: 4,
},
stepRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
stepIconBox: {
width: 34,
height: 34,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
stepLabel: {
flex: 1,
} as any,
stepConnector: {
width: 2,
height: 14,
marginLeft: 16,
borderRadius: 1,
marginVertical: 2,
},
/* Info badge */
infoBadge: {
width: '100%',
borderRadius: 16,
borderWidth: 1.2,
paddingVertical: 12,
paddingHorizontal: 16,
},
infoText: {
lineHeight: 20,
} as any,
/* CTA */
bottomArea: {
width: '100%',
paddingHorizontal: 24,
},
ctaBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 16,
borderRadius: 18,
},
ctaText: {
letterSpacing: 0.5, letterSpacing: 0.5,
}, } as any,
subText: {
marginTop: 8,
fontSize: 14,
color: '#475569',
},
/* ---------- Floating Next Button ---------- */
nextWrapper: {
position: 'absolute',
right: 20,
bottom: 30,
},
nextBtn: {
backgroundColor: colors.primary,
paddingHorizontal: 26,
paddingVertical: 14,
borderRadius: 28,
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 14,
elevation: 6,
},
nextText: {
color: '#FFFFFF',
fontSize: 15,
fontWeight: '800',
letterSpacing: 0.4,
},
}); });
@@ -0,0 +1,108 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { LogOut, X } from 'lucide-react-native';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
visible: boolean;
onClose: () => void;
onConfirm: () => void;
}
const GoBackModal = ({ visible, onClose, onConfirm }: Props) => {
const { colors, shadows } = useTheme();
if (!visible) return null;
return (
<View style={styles.overlay}>
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose}>
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
</TouchableOpacity>
<View style={[styles.iconBox, { backgroundColor: `${colors.error}15` }]}>
<LogOut size={32} color={colors.error} strokeWidth={2} />
</View>
<AppText variant="h3" color={colors.text} bold centered style={styles.title as any}>
Leave Bidding?
</AppText>
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle as any}>
Are you sure you want to go back to the Dashboard? You can always return to this panel later.
</AppText>
<View style={styles.btnRow}>
<TouchableOpacity style={[styles.btn, styles.cancelBtn, { backgroundColor: colors.background }]} onPress={onClose}>
<AppText variant="button" color={colors.text} bold>Stay</AppText>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, styles.confirmBtn, { backgroundColor: colors.error }]} onPress={onConfirm}>
<AppText variant="button" color="#fff" bold>Leave</AppText>
</TouchableOpacity>
</View>
</View>
</View>
);
};
export default GoBackModal;
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
zIndex: 9999,
elevation: 9999,
},
modalCard: {
width: '100%',
borderRadius: 24,
padding: 24,
alignItems: 'center',
},
closeBtn: {
position: 'absolute',
top: 16,
right: 16,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
iconBox: {
width: 64,
height: 64,
borderRadius: 32,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
title: {
marginBottom: 8,
},
subtitle: {
marginBottom: 24,
paddingHorizontal: 10,
},
btnRow: {
flexDirection: 'row',
gap: 12,
width: '100%',
},
btn: {
flex: 1,
height: 50,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
cancelBtn: {},
confirmBtn: {},
});
@@ -1,9 +1,11 @@
import React, { memo, useCallback, useRef, useState } from 'react'; import React, { memo, useCallback, useRef, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput } from 'react-native'; import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput } from 'react-native';
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import colors from '../../../Constant/Color'; import colors_const from '../../../Constant/Color';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail'; import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle'; import MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import axios, { all } from 'axios'; import axios, { all } from 'axios';
import Variables from '../../../Constant/Variable'; import Variables from '../../../Constant/Variable';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@@ -11,6 +13,8 @@ import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
import { Loader } from '../../../Component/Index'; import { Loader } from '../../../Component/Index';
const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => { const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const Token = useSelector((state: any) => state.token.token) const Token = useSelector((state: any) => state.token.token)
const [allState, setAllState] = useState({ isLoading: false }) const [allState, setAllState] = useState({ isLoading: false })
const [showDetail, setShowDetail] = useState(false); const [showDetail, setShowDetail] = useState(false);
@@ -108,9 +112,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
entering={FadeInUp.delay(100)} entering={FadeInUp.delay(100)}
style={[ style={[
styles.card, styles.card,
item.me ? styles.leadingCard : undefined,
{ {
borderColor: item.me ? '#22C55E' : '#EF4444', borderColor: item.me ? colors.success : colors.error,
backgroundColor: item.me ? '#F0FDF4' : '#FDF0F0', backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10`,
}, },
]} ]}
> >
@@ -121,10 +126,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
style={styles.cardHeader} style={styles.cardHeader}
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text> <AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
<Text style={styles.itemIdText}> <AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
Initial Amt : ₹{item.auctionStartValue} Initial Amt : ₹{item.auctionStartValue}
</Text> </AppText>
</View> </View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}> <TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
@@ -135,38 +140,40 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
item.me ? styles.meBadge : styles.liveBadge, item.me ? styles.meBadge : styles.liveBadge,
]} ]}
> >
<Text <AppText
variant="caption"
bold
style={[ style={[
styles.badgeText, styles.badgeText as any,
item.me ? styles.meText : styles.liveText, item.me ? styles.meText : styles.liveText,
]} ]}
> >
{item.me ? 'LEADING' : 'LIVE'} {item.me ? 'LEADING' : 'LIVE'}
</Text> </AppText>
</View> </View>
<Animated.Text style={[styles.arrow, arrowStyle]}> <Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
▼ ▼
</Animated.Text> </Animated.Text>
</View> </View>
<Text style={styles.itemIdText}> <AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
{SelectedAuction?.auctionNumber} {SelectedAuction?.auctionNumber}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
</TouchableOpacity> </TouchableOpacity>
{/* PRICE INFO */} {/* PRICE INFO */}
<View style={styles.priceRow}> <View style={styles.priceRow}>
<View> <View>
<Text style={styles.label}>Current Bid</Text> <AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
<Text style={styles.mainPrice}>₹{item.bidAmount || item.auctionStartValue}</Text> <AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item.bidAmount || item.auctionStartValue}</AppText>
</View> </View>
<View style={{ alignItems: 'flex-end' }}> <View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Decrement</Text> <AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Decrement</AppText>
<Text style={styles.incrementText}> <AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
-₹{item.emdAmount} -₹{item.emdAmount}
</Text> </AppText>
</View> </View>
</View> </View>
@@ -175,7 +182,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
<Animated.View style={[styles.collapsibleContainer, bodyStyle]}> <Animated.View style={[styles.collapsibleContainer, bodyStyle]}>
<View style={styles.divider} /> <View style={styles.divider} />
<Text style={styles.bidLabel}>Set Your Bid</Text> <AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
<View style={styles.stepperContainer}> <View style={styles.stepperContainer}>
{/* DECREASE */} {/* DECREASE */}
@@ -187,25 +194,25 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
!canDecrease && styles.disabledBtn, !canDecrease && styles.disabledBtn,
]} ]}
> >
<Text style={styles.stepText}>−</Text> <AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
</TouchableOpacity>} </TouchableOpacity>}
{/* VALUE */} {/* VALUE */}
{!allBoolean.oneTimeBid && <View style={styles.inputBox}> {!allBoolean.oneTimeBid && <View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text> <AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
<Text style={styles.bidValueText}>{bidValue}</Text> <AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
</View>} </View>}
{ {
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}> (allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
<Text style={styles.currency2}>₹</Text> <AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
<TextInput <TextInput
value={value} value={value}
onChangeText={handleChange} onChangeText={handleChange}
keyboardType="decimal-pad" keyboardType="decimal-pad"
placeholder="0.00" placeholder="0.00"
style={styles.input} style={[styles.input, { color: colors.text }]}
placeholderTextColor="#94A3B8" placeholderTextColor={colors.textSecondary}
/> />
</View> </View>
} }
@@ -219,7 +226,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
!canIncrease && styles.disabledBtn, !canIncrease && styles.disabledBtn,
]} ]}
> >
<Text style={styles.stepText}>+</Text> <AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
</TouchableOpacity>} </TouchableOpacity>}
</View> </View>
@@ -228,7 +235,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid} onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
> >
{ {
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text> allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
} }
</TouchableOpacity> </TouchableOpacity>
@@ -265,50 +272,34 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
export default ReverseBiddingCard; export default ReverseBiddingCard;
const styles = StyleSheet.create({ const makeStyles = (colors: any) => StyleSheet.create({
inputBox2: { inputBox2: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#F1F5F9', backgroundColor: colors.background,
borderRadius: 12, borderRadius: 12,
height: 45, height: 45,
paddingHorizontal: 12, paddingHorizontal: 12,
width: '100%' width: '100%'
}, },
currency2: { currency2: {
fontSize: 16,
fontWeight: 'bold',
color: '#64748B',
marginRight: 4, marginRight: 4,
}, } as any,
input: { input: {
flex: 1, flex: 1,
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: '#0F172A',
width: '100%' width: '100%'
}, },
card: { card: {
backgroundColor: '#FFF',
borderRadius: 24, borderRadius: 24,
padding: 20, padding: 20,
marginBottom: 16, marginBottom: 16,
borderWidth: 1, borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden', overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
}, },
leadingCard: { leadingCard: {
borderColor: '#22C55E',
borderWidth: 2, borderWidth: 2,
backgroundColor: '#F0FDF4',
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
@@ -316,27 +307,24 @@ const styles = StyleSheet.create({
alignItems: 'flex-start', alignItems: 'flex-start',
}, },
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 }, headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' }, productName: {} as any,
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 }, itemIdText: { marginTop: 2 } as any,
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' }, liveBadge: { backgroundColor: `${colors.error}20` },
meBadge: { backgroundColor: '#BBF7D0' }, meBadge: { backgroundColor: `${colors.success}20` },
badgeText: { fontSize: 10, fontWeight: '900' }, badgeText: { fontSize: 10 } as any,
liveText: { color: '#EF4444' }, liveText: { color: colors.error },
meText: { color: '#15803D' }, meText: { color: colors.success },
arrow: { fontSize: 10, color: '#94A3B8' }, arrow: { fontSize: 10 },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 }, priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { label: {
fontSize: 11,
color: '#94A3B8',
textTransform: 'uppercase', textTransform: 'uppercase',
fontWeight: '600', } as any,
}, mainPrice: {} as any,
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' }, incrementText: {} as any,
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' }, collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 }, divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 }, bidLabel: { marginBottom: 10 } as any,
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 }, stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { stepBtn: {
width: 45, width: 45,
@@ -346,19 +334,19 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
disabledBtn: { backgroundColor: '#CBD5E1' }, disabledBtn: { backgroundColor: colors.border },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' }, stepText: {} as any,
inputBox: { inputBox: {
flex: 1, flex: 1,
height: 45, height: 45,
backgroundColor: '#F1F5F9', backgroundColor: colors.background,
borderRadius: 12, borderRadius: 12,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 }, currency: { marginRight: 4 } as any,
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' }, bidValueText: {} as any,
submitBtn: { submitBtn: {
height: 50, height: 50,
backgroundColor: colors.primary, backgroundColor: colors.primary,
@@ -366,5 +354,5 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' }, submitBtnText: {} as any,
}); });
@@ -1,22 +1,26 @@
import React, { useState } from 'react' import React, { useState, useMemo } from 'react'
import { Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView } from 'react-native' import { Modal, View, StyleSheet, TextInput, TouchableOpacity, ScrollView, Platform } from 'react-native'
import { Formik } from 'formik' import { Formik } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { pick } from '@react-native-documents/picker' import { pick } from '@react-native-documents/picker'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { apiCall } from '../../../Axios/Axios' import { apiCall } from '../../../Axios/Axios'
import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/BBPS/ApiUrls' import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/ApiCall/ApiUrls'
import { Props } from '../Type/BidType' import { Props } from '../Type/BidType'
import colors from '../../../Constant/Color'
import { Loader } from '../../../Component/Index' import { Loader } from '../../../Component/Index'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import NavigationString from '../../../Constant/NavigationString' import NavigationString from '../../../Constant/NavigationString'
import { useTheme } from '../../../Hooks/useTheme'
import AppText from '../../../components/AppText'
import { Upload, FileText, X } from 'lucide-react-native'
const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }) => { const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }) => {
if (!template) return null if (!template) return null
const SelectedAuction = useSelector((state: any) => state.auction.auction) const SelectedAuction = useSelector((state: any) => state.auction.auction)
const Navigation: any = useNavigation() const Navigation: any = useNavigation()
const { colors, spacing, typography } = useTheme()
const styles = useMemo(() => makeStyles(colors, spacing, typography), [colors, spacing, typography])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const initialValues = template?.labels?.reduce((acc: any, label: any) => { const initialValues = template?.labels?.reduce((acc: any, label: any) => {
acc[label.labelName] = '' acc[label.labelName] = ''
@@ -65,10 +69,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
if (label.labelType === '3') { if (label.labelType === '3') {
return ( return (
<View key={label.labelName} style={styles.field}> <View key={label.labelName} style={styles.field}>
<Text style={styles.label}> <AppText variant="caption" color={colors.textSecondary} style={styles.label as any}>
{label.labelName} {label.labelName}
{label.mandatory && <Text style={styles.required}> *</Text>} {label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
</Text> </AppText>
<TouchableOpacity <TouchableOpacity
style={[ style={[
@@ -79,18 +83,25 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
pickFile(label.labelName, setFieldValue) pickFile(label.labelName, setFieldValue)
} }
> >
<Text {value?.name ? (
style={[ <View style={styles.fileSelectedRow}>
styles.fileText, <FileText size={14} color={colors.success} strokeWidth={2} />
value?.name && styles.fileSelected, <AppText variant="caption" color={colors.text} bold style={{ flex: 1 } as any} numberOfLines={1}>
]} {value.name}
> </AppText>
{value?.name || 'Upload PDF'} </View>
</Text> ) : (
<View style={styles.fileSelectedRow}>
<Upload size={14} color={colors.textSecondary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary}>
Upload PDF
</AppText>
</View>
)}
</TouchableOpacity> </TouchableOpacity>
{showError && ( {showError && (
<Text style={styles.errorText}>{error}</Text> <AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
)} )}
</View> </View>
) )
@@ -98,10 +109,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
return ( return (
<View key={label.labelName} style={styles.field}> <View key={label.labelName} style={styles.field}>
<Text style={styles.label}> <AppText variant="caption" color={colors.textSecondary} style={styles.label as any}>
{label.labelName} {label.labelName}
{label.mandatory && <Text style={styles.required}> *</Text>} {label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
</Text> </AppText>
<TextInput <TextInput
style={[ style={[
@@ -109,7 +120,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
showError && styles.errorBorder, showError && styles.errorBorder,
]} ]}
placeholder={`Enter ${label.labelName}`} placeholder={`Enter ${label.labelName}`}
placeholderTextColor="#9CA3AF" placeholderTextColor={colors.textSecondary}
value={value} value={value}
onChangeText={(text) => onChangeText={(text) =>
setFieldValue(label.labelName, text) setFieldValue(label.labelName, text)
@@ -122,7 +133,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
/> />
{showError && ( {showError && (
<Text style={styles.errorText}>{error}</Text> <AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
)} )}
</View> </View>
) )
@@ -210,26 +221,26 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
<View style={styles.header}> <View style={styles.header}>
<View style={styles.headerLeft}> <View style={styles.headerLeft}>
<Text style={styles.headerTitle}>Technical Documents</Text> <AppText variant="h4" color={colors.text}>Technical Documents</AppText>
<Text style={styles.headerSubtitle}> <AppText variant="caption" color={colors.textSecondary}>
Fill the form, Click save when you're done. Fill the form, Click save when you're done.
</Text> </AppText>
</View> </View>
<TouchableOpacity onPress={onClose} style={styles.closeBtn}> <TouchableOpacity onPress={onClose} style={styles.closeBtn}>
<Text style={styles.closeText}>✕</Text> <X size={20} color={colors.textSecondary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<ScrollView contentContainerStyle={styles.content}> <ScrollView contentContainerStyle={styles.content}>
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.templateHeading}>Template</Text> <AppText variant="h3" color={colors.text} centered style={{ marginBottom: 16 } as any}>Template</AppText>
<View style={styles.templateBox}> <View style={styles.templateBox}>
<Text style={styles.templateName}> <AppText variant="bodySmall" color={colors.text} bold style={{ marginBottom: 12 } as any}>
{template.templateName} {template.templateName}
</Text> </AppText>
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
@@ -264,12 +275,12 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
{loading ? ( {loading ? (
<Loader <Loader
size="small" size="small"
color={colors.white} color="#fff"
/> />
) : ( ) : (
<Text style={styles.submitText}> <AppText variant="button" color="#fff" bold style={styles.submitText as any}>
Submit Submit
</Text> </AppText>
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -287,10 +298,11 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
export default TemplateFormModal export default TemplateFormModal
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, typography: any) =>
StyleSheet.create({
screen: { screen: {
flex: 1, flex: 1,
backgroundColor: '#F9FAFB', backgroundColor: colors.background,
}, },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
@@ -298,118 +310,86 @@ const styles = StyleSheet.create({
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 14, paddingVertical: 14,
backgroundColor: '#FFFFFF', backgroundColor: colors.surface,
borderBottomWidth: 1, borderBottomWidth: 1,
borderColor: '#E5E7EB', borderColor: colors.border,
}, },
headerLeft: { headerLeft: {
flex: 1, flex: 1,
}, },
headerTitle: {
fontSize: 16,
fontWeight: '700',
color: '#111827',
},
headerSubtitle: {
fontSize: 12,
color: '#6B7280',
marginTop: 2,
},
closeBtn: { closeBtn: {
padding: 6, padding: 8,
}, backgroundColor: colors.overlay,
closeText: { borderRadius: 10,
fontSize: 18,
color: '#6B7280',
}, },
content: { content: {
paddingHorizontal: 16, paddingHorizontal: 16,
}, },
card: { card: {
backgroundColor: '#FFFFFF', backgroundColor: colors.surface,
borderColor: '#E5E7EB', borderRadius: 18,
paddingVertical: 20 marginTop: spacing.m,
padding: spacing.m,
...Platform.select({
ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 12 },
android: { elevation: 3 },
}),
}, },
templateHeading: {
fontSize: 18,
fontWeight: '700',
textAlign: 'center',
marginBottom: 16,
color: '#111827',
},
templateBox: { templateBox: {
borderWidth: 1, borderWidth: 1,
borderColor: '#E5E7EB', borderColor: colors.border,
borderRadius: 10, borderRadius: 14,
padding: 16, padding: 16,
}, },
templateName: {
fontSize: 14,
fontWeight: '700',
color: '#111827',
marginBottom: 12,
},
field: { field: {
marginBottom: 14, marginBottom: 14,
}, },
label: { label: {
fontSize: 13, letterSpacing: 0.5,
fontWeight: '600', textTransform: 'uppercase',
color: '#374151',
marginBottom: 6, marginBottom: 6,
}, } as any,
required: {
color: '#EF4444',
},
input: { input: {
backgroundColor: '#FFFFFF', backgroundColor: colors.background,
borderWidth: 1, borderWidth: 1.2,
borderColor: '#E5E7EB', borderColor: colors.border,
borderRadius: 8, borderRadius: 12,
padding: 12, padding: 12,
fontSize: 14, fontSize: 14,
color: '#111827', color: colors.text,
}, },
filePicker: { filePicker: {
borderWidth: 1, borderWidth: 1.2,
borderColor: '#E5E7EB', borderColor: colors.border,
borderRadius: 8, backgroundColor: colors.background,
borderRadius: 12,
padding: 12, padding: 12,
}, },
fileText: { fileSelectedRow: {
color: '#6B7280', flexDirection: 'row',
fontSize: 14, alignItems: 'center',
}, gap: 8,
fileSelected: {
color: '#111827',
fontWeight: '600',
}, },
errorText: { errorText: {
color: '#EF4444',
fontSize: 12,
marginTop: 4, marginTop: 4,
}, } as any,
errorBorder: { errorBorder: {
borderColor: '#EF4444', borderColor: colors.error,
}, },
submitRow: { submitRow: {
marginTop: 20, marginTop: 20,
alignItems: 'flex-end', alignItems: 'flex-end',
height: 50 height: 50,
}, },
submitBtn: { submitBtn: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
paddingVertical: 10, paddingVertical: 12,
paddingHorizontal: 22, paddingHorizontal: 24,
borderRadius: 8, borderRadius: 14,
minWidth: 120, minWidth: 120,
alignItems: 'center', alignItems: 'center',
}, },
submitText: { submitText: {
color: '#FFFFFF', letterSpacing: 0.5,
fontWeight: '700', } as any,
fontSize: 14,
},
}) })
File diff suppressed because it is too large Load Diff
+233 -243
View File
@@ -1,52 +1,74 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { View, FlatList, StyleSheet, Platform, Alert, BackHandler, Text } from 'react-native'; import {
View,
FlatList,
StyleSheet,
Alert,
BackHandler,
} from 'react-native';
import LottieView from 'lottie-react-native'; import LottieView from 'lottie-react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { InsideHeader } from '../../Component/Index'; import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useWebSocketService } from '../../socket/Socket'; import { useWebSocketService } from '../../socket/Socket';
import Variables from '../../Constant/Variable'; import Variables from '../../Constant/Variable';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { apiCall } from '../../Axios/Axios'; import { apiCall } from '../../Axios/Axios';
import { AUCTION_ONE_TIME_BID, FORWARD_AUCTION_ITEM, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECTED_AUCTION_ITEM } from '../../Services/BBPS/ApiUrls'; import {
AUCTION_ONE_TIME_BID,
FORWARD_AUCTION_ITEM,
GET_AUCTION_DETAILS,
REVERSE_AUCTION_ITEM,
SELECTED_AUCTION_ITEM,
} from '../../Services/ApiCall/ApiUrls';
import BiddingCard from './Component/BiddingCard'; import BiddingCard from './Component/BiddingCard';
import ConfirmBidModal from './Component/ConfirmBidModal';
import GoBackModal from './Component/GoBackModal';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native'; import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import CountdownTimer from '../../Component/Counter/CountdownTimer'; import CountdownTimer from '../../Component/Counter/CountdownTimer';
import FontsFamily from '../../Constant/FontsFamily'; import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Gavel, Building2, Clock4 } from 'lucide-react-native';
const StartBidding = () => { const StartBidding = () => {
const { colors, spacing, shadows } = useTheme();
const SelectedAuction = useSelector((state: any) => state.auction.auction); const SelectedAuction = useSelector((state: any) => state.auction.auction);
const loginUser = useSelector((state: any) => state.token.token) const loginUser = useSelector((state: any) => state.token.token);
const Navigation: any = useNavigation() const Navigation: any = useNavigation();
const [items, setItems] = useState<any>([]); const [items, setItems] = useState<any>([]);
const [bidValues, setBidValues] = useState<Record<number, number>>({}); const [bidValues, setBidValues] = useState<Record<number, number>>({});
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, bidPayload: {}, oneTimeBid: false, rebid: false, reload: false }) const [allBoolean, setAllBoolean] = useState({
showSuccess: false,
showDialog: false,
showBackDialog: false,
bidPayload: {} as any,
oneTimeBid: false,
rebid: false,
reload: false,
});
const { subscribe, createRoom, placeBid } = useWebSocketService( const { subscribe, createRoom, placeBid } = useWebSocketService(
Variables.webSocketUrl, Variables.webSocketUrl,
() => console.log('Connected!'), () => console.log('Connected!'),
(error) => console.log('WebSocket Error:', error), (error: any) => console.log('WebSocket Error:', error),
loginUser.token, loginUser.token,
); );
const getAuction = async () => { const getAuction = async () => {
const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId }); const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId });
if (selectedRes.statusCode == 200 && selectedRes.data.contractorId.length) { if (selectedRes.statusCode === 200 && selectedRes.data.contractorId.length) {
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true })) setAllBoolean(prev => ({ ...prev, oneTimeBid: true }));
}
} }
};
const getOneTimeBidValue = async () => { const getOneTimeBidValue = async () => {
try { try {
const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId }); const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId });
if (selectedRes.statusCode == 200) { return selectedRes.statusCode === 200 ? selectedRes.data : [];
return selectedRes.data } catch { return []; }
} else { };
return []
}
} catch (error) {
}
}
const getAllAcutionItems = async () => { const getAllAcutionItems = async () => {
try { try {
const payload = { auctionNumber: SelectedAuction.auctionNumber }; const payload = { auctionNumber: SelectedAuction.auctionNumber };
@@ -58,87 +80,40 @@ const StartBidding = () => {
} }
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload); const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
const uniqueArray: number[] = Array.from(new Set(selectedRes?.data || []));
const filteredItems = auctionItem.filter(item => uniqueArray.includes(item.id));
const uniqueArray: number[] = Array.from( if (SelectedAuction.auctionCategories === 'Live') {
new Set(selectedRes?.data || [])
);
const filteredItems = auctionItem.filter(item =>
uniqueArray.includes(item.id)
);
if (SelectedAuction.auctionCategories == 'Live') {
const result = await createRoom(SelectedAuction.auctionNumber); const result = await createRoom(SelectedAuction.auctionNumber);
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data: any) => {
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, data => {
console.log('🔥 BID UPDATE:', data); console.log('🔥 BID UPDATE:', data);
}); });
const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>(); const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>();
(result || []).forEach((r: any) => { (result || []).forEach((r: any) => {
bidAmountMap2.set(r.auctionItemId, { bidAmountMap2.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId });
bidAmount: r.bidAmount,
contractorId: r.contractorId,
}); });
});
// Check One Time bid enable butoon
if (SelectedAuction.auctionCategories != 'Live') {
const myBid = result.find(
(bid: any) => bid.contractorId === loginUser.contractorId
);
if (myBid) {
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
}
}
const bidAmountMap = new Map<number, number>();
(result || []).forEach((r: any) => {
bidAmountMap.set(r.auctionItemId, r.bidAmount);
});
const updatedFilteredItems = filteredItems.map(item => { const updatedFilteredItems = filteredItems.map(item => {
const bidData = bidAmountMap2.get(item.id); const bidData = bidAmountMap2.get(item.id);
return { return {
...item, ...item,
bidAmount: bidData?.bidAmount ?? item.auctionStartValueFigure, bidAmount: bidData?.bidAmount ?? item.auctionStartValueFigure,
bidAvilable: bidData?.bidAmount ?? 0, bidAvilable: bidData?.bidAmount ?? 0,
me: bidData?.contractorId == loginUser.contractorId ? true : false me: bidData?.contractorId === loginUser.contractorId,
}; };
}); });
const initialBids: Record<number, number> = {}; const initialBids: Record<number, number> = {};
updatedFilteredItems.forEach(item => { updatedFilteredItems.forEach(item => { initialBids[item.id] = item.bidAmount; });
initialBids[item.id] = item.bidAmount;
});
setBidValues(initialBids); setBidValues(initialBids);
setItems(updatedFilteredItems); setItems(updatedFilteredItems);
} else { } else {
let result = [] if (SelectedAuction.rebidAllowed) setAllBoolean(prev => ({ ...prev, rebid: true }));
if (SelectedAuction.rebidAllowed) { let result: any[] = [];
setAllBoolean((prev: any) => ({ ...prev, rebid: true })) if (SelectedAuction.auctionCategories !== 'Live') result = await getOneTimeBidValue();
}
if (SelectedAuction.auctionCategories != 'Live') {
result = await getOneTimeBidValue()
}
const bidMap = new Map<number, any>(); const bidMap = new Map<number, any>();
result.forEach((bid: any) => bidMap.set(bid.auctionItemId, bid));
result.forEach((bid: any) => {
bidMap.set(bid.auctionItemId, bid);
});
const mergedData = auctionItem.map((item: any) => { const mergedData = auctionItem.map((item: any) => {
const bid = bidMap.get(item.id); const bid = bidMap.get(item.id);
if (!bid) return { ...item, bidAmount: item.auctionStartValueFigure, contractorId: item.contractorId };
if (!bid) {
return {
...item,
bidAmount: item.auctionStartValueFigure,
contractorId: item.contractorId,
};
}
return { return {
...item, ...item,
bidAmount: bid.bidAmount, bidAmount: bid.bidAmount,
@@ -150,56 +125,36 @@ const StartBidding = () => {
}; };
}); });
const initialBids: Record<number, number> = {}; const initialBids: Record<number, number> = {};
mergedData.forEach(item => { mergedData.forEach(item => { initialBids[item.id] = item.bidAmount; });
initialBids[item.id] = item.bidAmount;
});
setBidValues(initialBids);
setItems(mergedData)
setBidValues(initialBids); setBidValues(initialBids);
setItems(mergedData);
} }
} catch (error) { } catch (error) {
console.error('Error fetching auction items:', error); console.error('Error fetching auction items:', error);
} }
}; };
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message) => { subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message: any) => {
// console.log('RAW MESSAGE:', message[message.length - 1]); const result = message[message.length - 1];
const result = message[message.length - 1] setItems((prev: any[]) =>
const updatedBidItem = (prev: any[]) =>
prev.map(item => prev.map(item =>
item.id === result.auctionItemId item.id === result.auctionItemId
? { ...item, bidAmount: result.bidAmount, me: result.contractorId == loginUser.contractorId ? true : false } ? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
: item : item,
),
); );
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
setItems(updatedBidItem);
setBidValues(prev => ({
...prev,
[result.auctionItemId]: result.bidAmount,
}));
try {
} catch (err) {
console.log('JSON parse error:', err);
}
}); });
useEffect(() => { useEffect(() => {
if (SelectedAuction.auctionCategories == 'One-time') { if (SelectedAuction.auctionCategories === 'One-time') getAuction();
getAuction()
}
getAllAcutionItems(); getAllAcutionItems();
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data) => { }); subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, () => {});
}, [allBoolean.reload]); }, [allBoolean.reload]);
useEffect(() => { useEffect(() => {
const initial: Record<number, number> = {}; const initial: Record<number, number> = {};
items.forEach((item: any) => { items.forEach((item: any) => { initial[item.id] = item.bidAmount + item.bidVariationValue; });
initial[item.id] = item.bidAmount + item.bidVariationValue;
});
setBidValues(initial); setBidValues(initial);
}, []); }, []);
@@ -216,51 +171,14 @@ const StartBidding = () => {
fullName: loginUser.fullName, fullName: loginUser.fullName,
roomId: SelectedAuction.auctionNumber, roomId: SelectedAuction.auctionNumber,
}; };
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, })); setAllBoolean(prev => ({ ...prev, bidPayload: payload, showDialog: true }));
Alert.alert(
'Confirm Bid',
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Submit',
onPress: () => {
placeBidSubmit(payload);
},
},
],
{ cancelable: true }
);
}; };
const placeBidSubmit = async (data: any) => { const placeBidSubmit = async (data: any) => {
setAllBoolean(prev => ({ ...prev, showDialog: false }));
try { try {
const result: any = await placeBid(data); const result: any = await placeBid(data);
if (result?.status && result.data) getAllAcutionItems();
if (result?.status && result.data) {
const updatedBidItem = (prev: any[]) =>
prev.map(item =>
item.id === result.data.auctionItemId
? { ...item, bidAmount: result.data.bidAmount, me: result.data?.contractorId == loginUser.contractorId ? true : false }
: item
);
// setItems(updatedBidItem);
// setBidValues((prev: any) => ({
// ...prev,
// [result.data.auctionItemId]: {
// ...prev[result.data.auctionItemId],
// bidAmount: result.data.bidAmount,
// },
// }));
getAllAcutionItems()
}
} catch (error) { } catch (error) {
console.error('Error placing bid:', error); console.error('Error placing bid:', error);
} }
@@ -269,56 +187,75 @@ const StartBidding = () => {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
const onBackPress = () => { const onBackPress = () => {
Alert.alert( setAllBoolean(prev => ({ ...prev, showBackDialog: true }));
'Confirm', return true;
'Do you want to go back to Dashboard?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Yes',
onPress: () => {
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
},
},
],
{ cancelable: true }
);
return true; // block default back action
}; };
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
const subscription = BackHandler.addEventListener( return () => sub.remove();
'hardwareBackPress', }, [Navigation]),
onBackPress
); );
return () => subscription.remove(); const styles = makeStyles(colors, spacing, shadows);
}, [Navigation])
); const biddingEndDate =
// console.log(bidValues, 'jjfdlsjfldsjfl'); SelectedAuction?.auctionStatus === 'Amended'
? SelectedAuction?.amendedkeyDates?.auctionBidding?.endDateTime
: SelectedAuction?.keyDates?.auctionBidding?.endDateTime;
return ( return (
<View style={styles.container}> <View style={styles.root}>
<InsideHeader title="Bidding Panel" showArrow /> <InsideHeader title="Bidding Panel" showArrow />
<View style={styles.container2}>
<Text style={styles.title}>
{SelectedAuction.deptName}
</Text>
<View style={styles.timerWrapper}> {/* ── Top Info Band ── */}
<Animated.View entering={FadeInDown.springify()} style={styles.infoBand}>
{/* Department row */}
<View style={styles.deptRow}>
<View style={[styles.deptIcon, { backgroundColor: `${colors.primary}15` }]}>
<Building2 size={16} color={colors.primary} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} bold numberOfLines={1} style={styles.deptName as any}>
{SelectedAuction.deptName ?? 'Auction Department'}
</AppText>
</View>
{/* Timer card */}
<View style={[styles.timerCard, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}25` }]}>
<View style={styles.timerLabelRow}>
<Clock4 size={13} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} bold style={styles.timerLabel as any}>
BIDDING ENDS IN
</AppText>
</View>
<CountdownTimer <CountdownTimer
endDate={SelectedAuction.keyDates.auctionBidding.endDateTime} endDate={biddingEndDate}
myStyle={{ fontSize: 30 }} myStyle={{ fontSize: 28, fontWeight: '900', color: colors.primary, letterSpacing: 1 }}
size={30} size={28}
/> />
</View> </View>
</View>
{/* Auction chip */}
<View style={styles.chipRow}>
<View style={[styles.chip, { backgroundColor: `${colors.primary}12` }]}>
<Gavel size={12} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} bold style={styles.chipText as any}>
{SelectedAuction.auctionNumber}
</AppText>
</View>
<View style={[styles.chip, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? `${colors.success}15` : `${colors.textSecondary}12` }]}>
<View style={[styles.liveDot, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary }]} />
<AppText
variant="caption"
color={SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary}
bold
style={styles.chipText as any}
>
{SelectedAuction.auctionCategories ?? 'Auction'}
</AppText>
</View>
</View>
</Animated.View>
{/* ── Items List ── */}
<FlatList <FlatList
data={items} data={items}
keyExtractor={item => item.id.toString()} keyExtractor={item => item.id.toString()}
@@ -328,15 +265,17 @@ const StartBidding = () => {
bidValue={bidValues[item.id] || 0} bidValue={bidValues[item.id] || 0}
onIncrease={() => handleUpdate(item.id, item.bidVariationValue)} onIncrease={() => handleUpdate(item.id, item.bidVariationValue)}
onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)} onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)}
onPlaceBid={() => { submitBid(item) }} onPlaceBid={() => submitBid(item)}
SelectedAuction={SelectedAuction} SelectedAuction={SelectedAuction}
allBoolean={allBoolean} allBoolean={allBoolean}
setAllBoolean={setAllBoolean} setAllBoolean={setAllBoolean}
/> />
)} )}
contentContainerStyle={{ padding: 16, paddingBottom: 300 }} contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/> />
{/* ── Success overlay ── */}
{allBoolean.showSuccess && ( {allBoolean.showSuccess && (
<View style={styles.overlay}> <View style={styles.overlay}>
<LottieView <LottieView
@@ -347,66 +286,117 @@ const StartBidding = () => {
/> />
</View> </View>
)} )}
<ConfirmBidModal
visible={allBoolean.showDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
bidAmount={allBoolean.bidPayload?.bidAmount}
/>
<GoBackModal
visible={allBoolean.showBackDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
onConfirm={() => {
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
}}
/>
</View> </View>
); );
}; };
export default StartBidding; export default StartBidding;
const styles = StyleSheet.create({ /* ─── Styles factory ─── */
container: { flex: 1, backgroundColor: colors.white }, const makeStyles = (colors: any, spacing: any, shadows: any) =>
container2: { StyleSheet.create({
paddingHorizontal: 16, root: {
paddingTop: 12, flex: 1,
paddingBottom: 8, backgroundColor: colors.background,
}, },
title: {
fontSize: 18, /* Info band */
fontFamily: FontsFamily.poppinsSemiBold, infoBand: {
color: colors.black, backgroundColor: colors.surface,
marginBottom: 6, paddingHorizontal: spacing.m,
// textAlign:'center' paddingVertical: spacing.m,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
...shadows.light,
}, },
timerWrapper: { deptRow: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: spacing.s,
}, },
card: { deptIcon: {
backgroundColor: '#FFF', width: 30,
borderRadius: 24, height: 30,
padding: 20, borderRadius: 9,
marginBottom: 16, alignItems: 'center',
borderWidth: 1, justifyContent: 'center',
borderColor: '#E2E8F0', },
overflow: 'hidden', deptName: {
...Platform.select({ ios: { shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, android: { elevation: 2 } }), flex: 1,
} as any,
/* Timer */
timerCard: {
borderRadius: 16,
borderWidth: 1.2,
paddingVertical: spacing.m,
paddingHorizontal: spacing.m,
alignItems: 'center',
gap: 4,
},
timerLabelRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
timerLabel: {
letterSpacing: 1.2,
} as any,
/* Chips */
chipRow: {
flexDirection: 'row',
gap: spacing.s,
},
chip: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: spacing.s + 4,
paddingVertical: 5,
borderRadius: 20,
},
chipText: {
letterSpacing: 0.3,
} as any,
liveDot: {
width: 6,
height: 6,
borderRadius: 3,
},
listContent: {
padding: spacing.m,
paddingBottom: 120,
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: colors.overlay,
justifyContent: 'center',
alignItems: 'center',
zIndex: 99,
}, },
leadingCard: { borderColor: '#22C55E', borderWidth: 2, backgroundColor: '#F0FDF4' },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { fontSize: 11, color: '#94A3B8', textTransform: 'uppercase', fontWeight: '600' },
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { width: 45, height: 45, backgroundColor: colors.primary, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
inputBox: { flex: 1, height: 45, backgroundColor: '#F1F5F9', borderRadius: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
submitBtn: { height: 50, backgroundColor: colors.primary, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(255,255,255,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 99 },
}); });
+38 -90
View File
@@ -7,20 +7,25 @@ import { useWebSocketService } from '../../socket/Socket';
import Variables from '../../Constant/Variable'; import Variables from '../../Constant/Variable';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { apiCall } from '../../Axios/Axios'; import { apiCall } from '../../Axios/Axios';
import { AUCTION_ONE_TIME_BID, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECTED_AUCTION_ITEM } from '../../Services/BBPS/ApiUrls'; import { AUCTION_ONE_TIME_BID, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECTED_AUCTION_ITEM } from '../../Services/ApiCall/ApiUrls';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native'; import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import CountdownTimer from '../../Component/Counter/CountdownTimer'; import CountdownTimer from '../../Component/Counter/CountdownTimer';
import FontsFamily from '../../Constant/FontsFamily'; import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import ReverseBiddingCard from './Component/ReverseBiddingCard'; import ReverseBiddingCard from './Component/ReverseBiddingCard';
import ConfirmBidModal from './Component/ConfirmBidModal';
import GoBackModal from './Component/GoBackModal';
const StartReverseBidding = () => { const StartReverseBidding = () => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const SelectedAuction = useSelector((state: any) => state.auction.auction); const SelectedAuction = useSelector((state: any) => state.auction.auction);
const loginUser = useSelector((state: any) => state.token.token) const loginUser = useSelector((state: any) => state.token.token)
const Navigation: any = useNavigation() const Navigation: any = useNavigation()
const [items, setItems] = useState<any>([]); const [items, setItems] = useState<any>([]);
const [bidValues, setBidValues] = useState<Record<number, number>>({}); const [bidValues, setBidValues] = useState<Record<number, number>>({});
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, bidPayload: {}, oneTimeBid: false, rebid: false, reload: false }) const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, showBackDialog: false, bidPayload: {} as any, oneTimeBid: false, rebid: false, reload: false })
const { subscribe, createRoom, placeBid } = useWebSocketService( const { subscribe, createRoom, placeBid } = useWebSocketService(
Variables.webSocketUrl, Variables.webSocketUrl,
() => console.log('Connected!'), () => console.log('Connected!'),
@@ -216,30 +221,13 @@ const StartReverseBidding = () => {
fullName: loginUser.fullName, fullName: loginUser.fullName,
roomId: SelectedAuction.auctionNumber, roomId: SelectedAuction.auctionNumber,
}; };
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, })); setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, showDialog: true }));
Alert.alert(
'Confirm Bid',
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Submit',
onPress: () => {
placeBidSubmit(payload);
},
},
],
{ cancelable: true }
);
}; };
const placeBidSubmit = async (data: any) => { const placeBidSubmit = async (data: any) => {
setAllBoolean((prev: any) => ({ ...prev, showDialog: false }));
try { try {
const result: any = await placeBid(data); const result: any = await placeBid(data);
console.log(result, 'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk');
if (result?.status && result.data) { if (result?.status && result.data) {
const updatedBidItem = (prev: any[]) => const updatedBidItem = (prev: any[]) =>
@@ -259,26 +247,7 @@ const StartReverseBidding = () => {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
const onBackPress = () => { const onBackPress = () => {
Alert.alert( setAllBoolean((prev: any) => ({ ...prev, showBackDialog: true }));
'Confirm',
'Do you want to go back to Dashboard?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Yes',
onPress: () => {
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
},
},
],
{ cancelable: true }
);
return true; // block default back action return true; // block default back action
}; };
@@ -295,9 +264,9 @@ const StartReverseBidding = () => {
<View style={styles.container}> <View style={styles.container}>
<InsideHeader title="Bidding Panel" showArrow /> <InsideHeader title="Bidding Panel" showArrow />
<View style={styles.container2}> <View style={styles.container2}>
<Text style={styles.title}> <AppText variant="h3" color={colors.text} bold style={styles.title as any}>
{SelectedAuction.deptName} {SelectedAuction.deptName}
</Text> </AppText>
<View style={styles.timerWrapper}> <View style={styles.timerWrapper}>
<CountdownTimer <CountdownTimer
@@ -336,66 +305,45 @@ const StartReverseBidding = () => {
/> />
</View> </View>
)} )}
<ConfirmBidModal
visible={allBoolean.showDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
bidAmount={allBoolean.bidPayload?.bidAmount}
/>
<GoBackModal
visible={allBoolean.showBackDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
onConfirm={() => {
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
}}
/>
</View> </View>
); );
}; };
export default StartReverseBidding; export default StartReverseBidding;
const styles = StyleSheet.create({ const makeStyles = (colors: any) => StyleSheet.create({
container: { flex: 1, backgroundColor: colors.white }, container: { flex: 1, backgroundColor: colors.background },
container2: { container2: {
paddingHorizontal: 16, paddingHorizontal: 16,
paddingTop: 12, paddingTop: 12,
paddingBottom: 8, paddingBottom: 8,
}, },
title: { title: {
fontSize: 18,
fontFamily: FontsFamily.poppinsSemiBold,
color: colors.black,
marginBottom: 6, marginBottom: 6,
// textAlign:'center' } as any,
},
timerWrapper: { timerWrapper: {
alignItems: 'center', alignItems: 'center',
}, },
card: { overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.overlay, justifyContent: 'center', alignItems: 'center', zIndex: 99 },
backgroundColor: '#FFF',
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden',
...Platform.select({ ios: { shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, android: { elevation: 2 } }),
},
leadingCard: { borderColor: '#22C55E', borderWidth: 2, backgroundColor: '#F0FDF4' },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { fontSize: 11, color: '#94A3B8', textTransform: 'uppercase', fontWeight: '600' },
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { width: 45, height: 45, backgroundColor: colors.primary, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
inputBox: { flex: 1, height: 45, backgroundColor: '#F1F5F9', borderRadius: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
submitBtn: { height: 50, backgroundColor: colors.primary, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(255,255,255,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 99 },
}); });
+217 -225
View File
@@ -1,260 +1,252 @@
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { View, Text, StyleSheet, Dimensions, FlatList, ActivityIndicator } from 'react-native'; import { View, StyleSheet, Dimensions, ActivityIndicator, ScrollView, TouchableOpacity } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withDelay, withTiming } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeInUp } from 'react-native-reanimated';
import { Layers, Clock, ShoppingCart, X, FileText } from 'lucide-react-native'; import { Layers, ShoppingCart, X, FileText, Menu, Radio, Clock, ChevronRight } from 'lucide-react-native';
import { Header } from '../../Component/Index'; import { useTheme } from '../../Hooks/useTheme';
import colors from '../../Constant/Color'; import { useDashboard, useTenderDashboard } from '../../Services/ApiCall/Hooks';
import { useDashboard, useESetuAdharVarification } from '../../Services/BBPS/Hooks';
import { log } from 'console';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog'; import { getToken } from '../../Redux/Slices/Token';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import AppText from '../../components/AppText';
import SidebarModal from '../../components/SidebarModal';
import NavigationString from '../../Constant/NavigationString';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const CARD_WIDTH = (width - 48) / 2;
const StatCard = ({ item, index, isLoading }: any) => {
const translateY = useSharedValue(30);
const opacity = useSharedValue(0);
useEffect(() => {
translateY.value = withDelay(index * 80, withTiming(0, { duration: 400 }));
opacity.value = withDelay(index * 80, withTiming(1, { duration: 400 }));
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
opacity: opacity.value,
}));
const Icon = item.icon;
return (
<Animated.View style={[styles.card, animatedStyle]}>
<View style={styles.header}>
<Text style={styles.title} numberOfLines={2}>
{item.title}
</Text>
<View style={[styles.badge, { borderColor: item.color }]}>
<Icon size={14} color={item.color} />
<Text style={[styles.badgeText, { color: item.color }]}>
{item.value}
</Text>
</View>
</View>
{/* VALUE / LOADER */}
{isLoading ? (
<ActivityIndicator
size="small"
color={item.color}
style={{ marginTop: 12 }}
/>
) : (
<Text style={styles.value}>{item.value}</Text>
)}
<Text style={styles.desc}>{item.desc}</Text>
</Animated.View>
);
};
const Home = () => { const Home = () => {
const { data, isLoading } = useDashboard(); const { colors } = useTheme();
const user = useSelector((state: any) => state.token?.token);
console.log("-=--",user) const [auctionStats, setAuctionStats] = useState<any>({});
const { mutateAsync:eSetuAdharVarification } = useESetuAdharVarification(); const [tenderStats, setTenderStats] = useState<any>({});
const dashboard = data?.data || {}; const [isMenuVisible, setIsMenuVisible] = useState(false);
const [esetu,setEsetu] = useState<boolean>();
const navigation = useNavigation<any>();
const userData = useSelector(getToken);
// Hooks for data
const { data: dashboardData, refetch: refetchDashboard, isPending: isLoadingDashboard } = useDashboard();
const { data: tenderData, refetch: refetchTender, isPending: isLoadingTender } = useTenderDashboard();
useFocusEffect(
useCallback(() => {
refetchDashboard();
refetchTender();
}, [])
);
useEffect(() => { useEffect(() => {
if(user?.esetuVerified === 'Not Verified'){ if (dashboardData) {
const payLoad= { setAuctionStats(dashboardData.data || {});
contId: user,
transactionId: "string",
udf1: "string",
udf2: "string",
udf3: "string",
udf4: "string",
responseUrl: "string"
} }
setEsetu(true) if (tenderData) {
eSetuAdharVarification(payLoad, { setTenderStats(tenderData.data || {});
onSuccess: (res) => {
console.log(res)
}, onError: (err) => {
console.log(err, '-----auction item error');
} }
}); }, [dashboardData, tenderData]);
}else{
setEsetu(false)
}
},[user])
const DASHBOARD_CARDS = [ const renderGlassCard = (title: string, value: string | number, icon: React.ReactNode, index: number) => (
<Animated.View
entering={FadeInUp.duration(600).delay(index * 50)}
style={[
styles.glassCard,
styles.halfWidthCard,
{ {
title: 'Total Auctions', backgroundColor: colors.glass,
value: dashboard.myAuctionCount ?? 0, borderColor: colors.overlay,
desc: 'Auctions purchased by you', }
icon: Layers, ]}
color: '#2563EB', >
}, <View style={styles.cardHeader}>
{ <View style={[styles.iconContainer, { backgroundColor: colors.overlay }]}>
title: 'Live Auctions', {icon}
value: dashboard.liveAuctionCount ?? 0, </View>
desc: 'Currently live', <AppText variant="h3" color={colors.primary}>{value || '0'}</AppText>
icon: Clock, </View>
color: '#0284C7', <View style={styles.cardContent}>
}, <AppText variant="caption" color={colors.textSecondary} numberOfLines={1}>{title}</AppText>
{ </View>
title: 'Participated Auctions', </Animated.View>
value: dashboard.currentBiddingCount ?? 0, );
desc: 'Participated in auction',
icon: ShoppingCart,
color: '#7C3AED',
},
{
title: 'Cancelled Auctions',
value: dashboard.cancelledAuctionCount ?? 0,
desc: 'Cancelled by department',
icon: X,
color: '#DC2626',
},
{
title: 'Total Tenders',
value: 0,
desc: 'Tenders assigned',
icon: FileText,
color: '#16A34A',
},
{
title: 'Live Tenders',
value: 0,
desc: 'Currently live',
icon: Clock,
color: '#059669',
},
{
title: 'Participated Tenders',
value: 0,
desc: 'Participated in tender',
icon: ShoppingCart,
color: '#0EA5E9',
},
{
title: 'Cancelled Tenders',
value: 0,
desc: 'Cancelled by authority',
icon: X,
color: '#EF4444',
},
];
return ( return (
<View style={styles.screen}> <View style={[styles.container, { backgroundColor: colors.background }]}>
<Header title="Dashboard" />
<ConfirmDialog
visible={esetu as boolean}
title="Aadhaar Verification Required"
message="To access features, please complete your Aadhaar verification."
confirmText="Proceed"
cancelText="Cancel"
onConfirm={() => {
setEsetu(false);
}} <ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
onClose={() => setEsetu(false)}
/> {/* Top Bar */}
<FlatList <Animated.View entering={FadeInDown.duration(600)} style={styles.headerTop}>
data={DASHBOARD_CARDS} <View style={styles.userInfo}>
keyExtractor={(_, index) => index.toString()} <View style={[styles.avatarContainer, { backgroundColor: colors.primary }]}>
numColumns={2} <AppText variant="h3" color="#FFF">{userData?.firstName?.charAt(0) || 'U'}</AppText>
columnWrapperStyle={styles.row} </View>
contentContainerStyle={styles.list} <View>
renderItem={({ item, index }) => ( <AppText variant="caption" color={colors.textSecondary}>Welcome,</AppText>
<StatCard <AppText variant="body" color={colors.text} style={{ fontWeight: '600' }}>{userData?.firstName || 'User'}</AppText>
item={item} </View>
index={index} </View>
isLoading={isLoading} <View style={styles.headerActions}>
/> <TouchableOpacity onPress={() => setIsMenuVisible(true)}>
<Menu size={24} color={colors.text} />
</TouchableOpacity>
</View>
</Animated.View>
<SidebarModal isVisible={isMenuVisible} onClose={() => setIsMenuVisible(false)} />
{isLoadingDashboard || isLoadingTender ? (
<ActivityIndicator size="large" color={colors.primary} style={{ marginTop: 40 }} />
) : (
<View style={styles.sectionsWrapper}>
{/* Auction Section */}
<View style={styles.sectionContainer}>
<View style={styles.sectionHeader}>
<AppText variant="body" color={colors.text} style={{ fontWeight: 'bold' }}>Auctions</AppText>
<TouchableOpacity style={styles.viewAllBtn} onPress={() => navigation.navigate(NavigationString.Auction)}>
<AppText variant="caption" color={colors.primary}>Explore</AppText>
<ChevronRight size={14} color={colors.primary} />
</TouchableOpacity>
</View>
<View style={styles.gridContainer}>
{renderGlassCard('Live Auctions', auctionStats?.liveAuctionCount, <Radio size={18} color={colors.primary} />, 0)}
{renderGlassCard('Purchased', auctionStats?.myAuctionCount, <ShoppingCart size={18} color={colors.primary} />, 1)}
{renderGlassCard('Participated', auctionStats?.currentBiddingCount, <FileText size={18} color={colors.primary} />, 2)}
{renderGlassCard('Cancelled', auctionStats?.cancelledAuctionCount, <X size={18} color={colors.primary} />, 3)}
</View>
</View>
{/* Tender Section */}
<View style={styles.sectionContainer}>
<View style={styles.sectionHeader}>
<AppText variant="body" color={colors.text} style={{ fontWeight: 'bold' }}>Tenders</AppText>
<TouchableOpacity style={styles.viewAllBtn} onPress={() => navigation.navigate(NavigationString.Tender)}>
<AppText variant="caption" color={colors.primary}>Explore</AppText>
<ChevronRight size={14} color={colors.primary} />
</TouchableOpacity>
</View>
<View style={styles.gridContainer}>
{renderGlassCard('Total Tenders', tenderStats[0]?.totalTender, <Layers size={18} color={colors.primary} />, 4)}
{renderGlassCard('Purchased', tenderStats[0]?.totalPurchased, <ShoppingCart size={18} color={colors.primary} />, 5)}
{renderGlassCard('Participated', tenderStats[0]?.totalParticipate, <FileText size={18} color={colors.primary} />, 6)}
</View>
</View>
</View>
)} )}
showsVerticalScrollIndicator={false}
/> {/* Recent Activity */}
<View style={styles.sectionContainer}>
<AppText variant="body" color={colors.text} style={{ fontWeight: 'bold', marginBottom: 10 }}>Recent Activity</AppText>
<View style={[styles.activityCard, { backgroundColor: colors.glass, borderColor: colors.overlay }]}>
<View style={[styles.activityIcon, { backgroundColor: colors.overlay }]}>
<Clock size={20} color={colors.primary} />
</View>
<View style={{ flex: 1 }}>
<AppText variant="bodySmall" color={colors.text}>No recent activity</AppText>
<AppText variant="caption" color={colors.textSecondary}>Your latest bids will appear here</AppText>
</View>
</View>
</View>
</ScrollView>
</View > </View >
); );
}; };
export default Home; export default Home;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
screen: { container: {
flex: 1, flex: 1,
backgroundColor: colors.white,
}, },
scrollContent: {
list: { paddingTop: 40,
padding: 16, paddingHorizontal: 16,
paddingBottom: 24, paddingBottom: 90,
}, },
headerTop: {
row: {
justifyContent: 'space-between',
marginBottom: 16,
},
card: {
width: CARD_WIDTH,
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
borderWidth: 1,
borderColor: '#E5E7EB',
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 3,
},
header: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'center',
marginBottom: 20,
}, },
userInfo: {
title: {
fontSize: 13,
fontWeight: '600',
color: '#374151',
flex: 1,
paddingRight: 8,
},
badge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 6, },
avatarContainer: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
sectionsWrapper: {
marginTop: 0,
},
sectionContainer: {
marginBottom: 20,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 10,
},
viewAllBtn: {
flexDirection: 'row',
alignItems: 'center',
},
gridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
glassCard: {
padding: 12,
borderRadius: 16,
borderWidth: 1, borderWidth: 1,
borderRadius: 20, marginBottom: 12,
paddingHorizontal: 8, flexDirection: 'column',
paddingVertical: 4, justifyContent: 'center',
}, },
halfWidthCard: {
badgeText: { width: (width - 42) / 2,
fontSize: 12,
fontWeight: '700',
}, },
cardHeader: {
value: { flexDirection: 'row',
fontSize: 28, justifyContent: 'space-between',
fontWeight: '800', alignItems: 'center',
color: '#111827',
marginTop: 5,
}, },
iconContainer: {
desc: { width: 32,
fontSize: 12, height: 32,
color: '#6B7280', borderRadius: 8,
marginTop: 6, alignItems: 'center',
justifyContent: 'center',
},
cardContent: {
marginTop: 8,
},
activityCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
borderRadius: 16,
borderWidth: 1,
},
activityIcon: {
width: 40,
height: 40,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
}, },
}); });
+271
View File
@@ -0,0 +1,271 @@
import React, { useEffect, useRef, useState } from "react";
import { View, ActivityIndicator, StyleSheet, Text } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { WebView } from "react-native-webview";
import { CommonActions } from "@react-navigation/native";
import colors from "../../Constant/Color";
import { BASE_URL } from "../../Services/ApiCall/ApiUrls";
const SUCCESS_DELAY_MS = 5000; // 5 seconds
export const KycWebViewScreen = ({ route, navigation }: any) => {
const webRef = useRef<WebView>(null);
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const hasCompletedRef = useRef(false);
const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const data = route.params ?? {};
const html = data.res.data;
const [isLoading, setIsLoading] = useState<boolean>(true);
const [loadError, setLoadError] = useState<boolean>(false);
const [showSuccessMessage, setShowSuccessMessage] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(5);
const url = false;
useEffect(() => {
return () => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
}
if (successTimeoutRef.current) {
clearTimeout(successTimeoutRef.current);
}
};
}, []);
const handleNavigationBack = () => {
if (data.returnRoute) {
navigation.replace(data.returnRoute, data.returnParams);
} else {
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'Login' }],
})
);
}
};
const startSuccessCountdown = () => {
setShowSuccessMessage(true);
setCountdown(5);
let remainingSeconds = 5;
const countdownInterval = setInterval(() => {
remainingSeconds -= 1;
setCountdown(remainingSeconds);
if (remainingSeconds <= 0) {
clearInterval(countdownInterval);
}
}, 1000);
successTimeoutRef.current = setTimeout(() => {
clearInterval(countdownInterval);
handleNavigationBack();
}, SUCCESS_DELAY_MS);
};
const handleLoadStart = (event: any) => {
setIsLoading(true);
setLoadError(false);
};
const handleLoadEnd = (event: any) => {
setIsLoading(false);
};
const handleError = (event: any) => {
setIsLoading(false);
setLoadError(true);
};
const handleNavigationStateChange = (navState: any) => {
if (
navState.title &&
navState.title.includes('E-KYC SETU - Verification Success') &&
!hasCompletedRef.current
) {
hasCompletedRef.current = true;
startSuccessCountdown();
}
};
const LoadingIndicator = () => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary || '#2563EB'} />
<Text style={styles.loadingText}>Loading KYC form...</Text>
</View>
);
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.white }}>
{url ? (
<WebView
ref={webRef}
source={{
uri: url,
baseUrl: "http://192.168.1.15:8080/",
}}
javaScriptEnabled
domStorageEnabled
startInLoadingState
originWhitelist={["*"]}
setSupportMultipleWindows={false}
onLoadStart={handleLoadStart}
onLoadEnd={handleLoadEnd}
onError={handleError}
onNavigationStateChange={handleNavigationStateChange}
renderLoading={() => <LoadingIndicator />}
/>
) : (
<WebView
ref={webRef}
source={{
html: html ?? "<h3>No content</h3>",
// baseUrl: `${BASE_URL}ESetuKYC/initiate/`,
}}
javaScriptEnabled
domStorageEnabled
startInLoadingState
onLoadStart={handleLoadStart}
onLoadEnd={handleLoadEnd}
onError={handleError}
onNavigationStateChange={(navState) => {
console.log("Current -- URL:", navState.url);
handleNavigationStateChange(navState);
}}
onShouldStartLoadWithRequest={(request) => {
console.log("Request URL:", request.url);
return true;
}}
renderLoading={() => <LoadingIndicator />}
/>
)}
{isLoading && !showSuccessMessage && <LoadingIndicator />}
{showSuccessMessage && (
<View style={styles.successContainer}>
<View style={styles.successCard}>
<Text style={styles.successIcon}>✅</Text>
<Text style={styles.successTitle}>Verification Successful!</Text>
<Text style={styles.successMessage}>
Your KYC verification has been completed successfully.
</Text>
<Text style={styles.countdownText}>
Redirecting in {countdown} seconds...
</Text>
<ActivityIndicator
size="small"
color={colors.primary || '#2563EB'}
style={{ marginTop: 16 }}
/>
</View>
</View>
)}
{loadError && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Failed to load content</Text>
<Text style={styles.errorSubText}>Please try again</Text>
</View>
)}
</SafeAreaView>
);
};
export default KycWebViewScreen;
const styles = StyleSheet.create({
loadingContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.white,
zIndex: 1,
},
loadingText: {
marginTop: 12,
fontSize: 16,
fontWeight: '600',
color: '#374151',
},
successContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
zIndex: 2,
padding: 24,
},
successCard: {
backgroundColor: '#FFFFFF',
borderRadius: 20,
padding: 32,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 8,
maxWidth: 340,
width: '100%',
},
successIcon: {
fontSize: 64,
marginBottom: 16,
},
successTitle: {
fontSize: 24,
fontWeight: '700',
color: '#16A34A',
marginBottom: 12,
textAlign: 'center',
},
successMessage: {
fontSize: 16,
color: '#374151',
textAlign: 'center',
marginBottom: 20,
lineHeight: 24,
},
countdownText: {
fontSize: 18,
fontWeight: '600',
color: '#2563EB',
textAlign: 'center',
},
errorContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.white,
zIndex: 1,
},
errorText: {
fontSize: 18,
fontWeight: '700',
color: '#DC2626',
marginBottom: 8,
},
errorSubText: {
fontSize: 14,
color: '#6B7280',
},
});
+8 -15
View File
@@ -15,6 +15,9 @@ import {
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import colors from '../../Constant/Color'; import colors from '../../Constant/Color';
import { showToast } from '../../Constant/ShowDailog';
import NavigationString from '../../Constant/NavigationString';
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
interface ForgotModalProps { interface ForgotModalProps {
visible: boolean; visible: boolean;
@@ -28,7 +31,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
visible, visible,
onClose onClose
}) => { }) => {
const navigation = useNavigation() as Navigation; const Navigation = useNavigation<any>();
const [email, setEmail] = useState<string>(''); const [email, setEmail] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
@@ -56,7 +59,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
try { try {
// Make API call to send OTP for forgot password // Make API call to send OTP for forgot password
const response = await fetch( const response = await fetch(
`https://procurelinc.in/EProcurementSB/eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`, `${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
{ {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -67,20 +70,10 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
); );
if (response.ok) { if (response.ok) {
Alert.alert( showToast('OTP has been sent to your email address. Please check your inbox.')
'Success',
'OTP has been sent to your email address. Please check your inbox.',
[
{
text: 'OK',
onPress: () => {
onClose(); onClose();
navigation.navigate('ResetPassword', { email }); Navigation.navigate(NavigationString.ResetPassword, { email });
}
}
]
);
setEmail('');
} else { } else {
const errorData = await response.text(); const errorData = await response.text();
console.error('API Error:', errorData); console.error('API Error:', errorData);
File diff suppressed because it is too large Load Diff
+20 -36
View File
@@ -16,8 +16,10 @@ import {
TextInputKeyPressEventData, TextInputKeyPressEventData,
ToastAndroid, ToastAndroid,
} from 'react-native'; } from 'react-native';
import { BASE_URL } from '../../Services/BBPS/ApiUrls'; import { Eye, EyeOff } from 'lucide-react-native';
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
import colors from '../../Constant/Color'; import colors from '../../Constant/Color';
interface ResetPasswordProps { interface ResetPasswordProps {
route?: any; route?: any;
navigation?: any; navigation?: any;
@@ -45,12 +47,10 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
const confirmPasswordRef = useRef<TextInput>(null); const confirmPasswordRef = useRef<TextInput>(null);
const resendTimerRef = useRef<NodeJS.Timeout | null>(null); const resendTimerRef = useRef<NodeJS.Timeout | null>(null);
// Initialize resend cooldown from storage or set to 0
useEffect(() => { useEffect(() => {
checkResendCooldown(); checkResendCooldown();
}, []); }, []);
// Clean up timer on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
if (resendTimerRef.current) { if (resendTimerRef.current) {
@@ -59,7 +59,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
}; };
}, []); }, []);
// Update timer every second
useEffect(() => { useEffect(() => {
if (resendCooldown > 0) { if (resendCooldown > 0) {
resendTimerRef.current = setInterval(() => { resendTimerRef.current = setInterval(() => {
@@ -98,13 +97,12 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
}; };
const startResendCooldown = async () => { const startResendCooldown = async () => {
const cooldownTime = 300; // 5 minutes in seconds const cooldownTime = 300;
setResendCooldown(cooldownTime); setResendCooldown(cooldownTime);
await AsyncStorage.setItem('otpResendTime', Date.now().toString()); await AsyncStorage.setItem('otpResendTime', Date.now().toString());
}; };
useEffect(() => { useEffect(() => {
// Auto-submit OTP when all fields are filled
const fullOtp = otp.join(''); const fullOtp = otp.join('');
if (fullOtp.length === 6 && !isOtpVerified) { if (fullOtp.length === 6 && !isOtpVerified) {
handleSubmitOtp(); handleSubmitOtp();
@@ -112,14 +110,13 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
}, [otp]); }, [otp]);
const handleOtpChange = (value: string, index: number) => { const handleOtpChange = (value: string, index: number) => {
if (!/^\d*$/.test(value)) return; // Only allow numbers if (!/^\d*$/.test(value)) return;
const newOtp = [...otp]; const newOtp = [...otp];
newOtp[index] = value; newOtp[index] = value;
setOtp(newOtp); setOtp(newOtp);
setOtpError(''); setOtpError('');
// Auto-focus to next input
if (value && index < 5) { if (value && index < 5) {
otpInputs.current[index + 1]?.focus(); otpInputs.current[index + 1]?.focus();
} }
@@ -127,7 +124,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
const handleOtpKeyPress = (e: NativeSyntheticEvent<TextInputKeyPressEventData>, index: number) => { const handleOtpKeyPress = (e: NativeSyntheticEvent<TextInputKeyPressEventData>, index: number) => {
if (e.nativeEvent.key === 'Backspace' && !otp[index] && index > 0) { if (e.nativeEvent.key === 'Backspace' && !otp[index] && index > 0) {
// Move focus to previous input on backspace
otpInputs.current[index - 1]?.focus(); otpInputs.current[index - 1]?.focus();
} }
}; };
@@ -149,7 +145,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
setOtpError(''); setOtpError('');
try { try {
// Make the API request to verify OTP
const res = await axios.post( const res = await axios.post(
`${BASE_URL}eproc/verify-otp?email=${encodeURIComponent(email)}&otp=${fullOtp}`, `${BASE_URL}eproc/verify-otp?email=${encodeURIComponent(email)}&otp=${fullOtp}`,
null, null,
@@ -167,7 +162,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
ToastAndroid.show('OTP verified successfully!', ToastAndroid.SHORT); ToastAndroid.show('OTP verified successfully!', ToastAndroid.SHORT);
setIsOtpVerified(true); setIsOtpVerified(true);
// Focus on password field after successful OTP verification
setTimeout(() => { setTimeout(() => {
newPasswordRef.current?.focus(); newPasswordRef.current?.focus();
}, 100); }, 100);
@@ -193,19 +187,16 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
const validateForm = (): boolean => { const validateForm = (): boolean => {
let isValid = true; let isValid = true;
// Validate OTP
if (!isOtpVerified) { if (!isOtpVerified) {
setOtpError('Please verify OTP first'); setOtpError('Please verify OTP first');
isValid = false; isValid = false;
} }
// Validate password
if (newPassword.length < 8) { if (newPassword.length < 8) {
setPasswordError('Password must be at least 8 characters long'); setPasswordError('Password must be at least 8 characters long');
isValid = false; isValid = false;
} }
// Validate confirm password
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
setConfirmPasswordError('Passwords do not match'); setConfirmPasswordError('Passwords do not match');
isValid = false; isValid = false;
@@ -225,7 +216,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
setLoading(true); setLoading(true);
try { try {
// Make API call to reset password according to curl
const res = await axios.post( const res = await axios.post(
`${BASE_URL}eproc/reset-password?email=${encodeURIComponent(email)}&otp=${otp.join('')}&newPassword=${encodeURIComponent(newPassword)}&confirmPassword=${encodeURIComponent(confirmPassword)}`, `${BASE_URL}eproc/reset-password?email=${encodeURIComponent(email)}&otp=${otp.join('')}&newPassword=${encodeURIComponent(newPassword)}&confirmPassword=${encodeURIComponent(confirmPassword)}`,
null, null,
@@ -245,7 +235,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
'Your password has been reset successfully.', 'Your password has been reset successfully.',
[{ [{
text: 'OK', onPress: () => { text: 'OK', onPress: () => {
// Navigate to login screen
navigation.navigate('Login'); navigation.navigate('Login');
} }
}] }]
@@ -273,7 +262,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
setResendLoading(true); setResendLoading(true);
try { try {
// Make API call to resend OTP
const res = await axios.post( const res = await axios.post(
`${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`, `${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
null, null,
@@ -286,11 +274,9 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
if (res.data?.statusCode === 200) { if (res.data?.statusCode === 200) {
ToastAndroid.show('OTP sent successfully', ToastAndroid.SHORT); ToastAndroid.show('OTP sent successfully', ToastAndroid.SHORT);
// Clear OTP fields
setOtp(['', '', '', '', '', '']); setOtp(['', '', '', '', '', '']);
setIsOtpVerified(false); setIsOtpVerified(false);
setOtpError(''); setOtpError('');
// Start the cooldown timer
startResendCooldown(); startResendCooldown();
} else { } else {
throw new Error(res.data?.message || 'Failed to resend OTP'); throw new Error(res.data?.message || 'Failed to resend OTP');
@@ -325,7 +311,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
Enter the OTP sent to {email} and your new password Enter the OTP sent to {email} and your new password
</Text> </Text>
{/* OTP Input Section */}
<View style={styles.otpSection}> <View style={styles.otpSection}>
<Text style={styles.sectionLabel}>OTP Code</Text> <Text style={styles.sectionLabel}>OTP Code</Text>
<View style={styles.otpContainer}> <View style={styles.otpContainer}>
@@ -363,10 +348,8 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
)} )}
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
{/* Password Input Section - Only enabled after OTP verification */}
<View style={styles.passwordSection}> <View style={styles.passwordSection}>
<Text style={styles.sectionLabel}>New Password</Text> <Text style={styles.sectionLabel}>New Password</Text>
<View style={styles.passwordInputContainer}> <View style={styles.passwordInputContainer}>
@@ -374,7 +357,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
ref={newPasswordRef} ref={newPasswordRef}
style={[styles.input, passwordError ? styles.inputError : null, !isOtpVerified && styles.disabledInput]} style={[styles.input, passwordError ? styles.inputError : null, !isOtpVerified && styles.disabledInput]}
placeholder="Enter new password" placeholder="Enter new password"
placeholderTextColor={!isOtpVerified ? '#4f058bff' : '#000'} placeholderTextColor={!isOtpVerified ? colors.lightText : colors.black}
value={newPassword} value={newPassword}
onChangeText={(text) => { onChangeText={(text) => {
setNewPassword(text); setNewPassword(text);
@@ -390,9 +373,11 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
style={styles.eyeIcon} style={styles.eyeIcon}
onPress={() => setNewPasswordVisible(!newPasswordVisible)} onPress={() => setNewPasswordVisible(!newPasswordVisible)}
> >
<Text style={styles.eyeIconText}> {newPasswordVisible ? (
{newPasswordVisible ? '🙈' : '👁️'} <EyeOff size={20} color={colors.primary} />
</Text> ) : (
<Eye size={20} color={colors.primary} />
)}
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
@@ -404,7 +389,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
ref={confirmPasswordRef} ref={confirmPasswordRef}
style={[styles.input, confirmPasswordError ? styles.inputError : null, !isOtpVerified && styles.disabledInput]} style={[styles.input, confirmPasswordError ? styles.inputError : null, !isOtpVerified && styles.disabledInput]}
placeholder="Confirm new password" placeholder="Confirm new password"
placeholderTextColor={!isOtpVerified ? '#4f058bff' : '#000'} placeholderTextColor={!isOtpVerified ? colors.lightText : colors.black}
value={confirmPassword} value={confirmPassword}
onChangeText={(text) => { onChangeText={(text) => {
setConfirmPassword(text); setConfirmPassword(text);
@@ -420,9 +405,11 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
style={styles.eyeIcon} style={styles.eyeIcon}
onPress={() => setConfirmPasswordVisible(!confirmPasswordVisible)} onPress={() => setConfirmPasswordVisible(!confirmPasswordVisible)}
> >
<Text style={styles.eyeIconText}> {confirmPasswordVisible ? (
{confirmPasswordVisible ? '🙈' : '👁️'} <EyeOff size={20} color={colors.primary} />
</Text> ) : (
<Eye size={20} color={colors.primary} />
)}
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
@@ -445,7 +432,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
)} )}
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity {!isOtpVerified && <TouchableOpacity
style={[styles.resendButton, resendCooldown > 0 && styles.disabledResendButton]} style={[styles.resendButton, resendCooldown > 0 && styles.disabledResendButton]}
onPress={handleResendOtp} onPress={handleResendOtp}
disabled={resendLoading || resendCooldown > 0} disabled={resendLoading || resendCooldown > 0}
@@ -459,7 +446,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
) : ( ) : (
<Text style={styles.resendButtonText}>Resend OTP</Text> <Text style={styles.resendButtonText}>Resend OTP</Text>
)} )}
</TouchableOpacity> </TouchableOpacity>}
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
); );
@@ -550,12 +537,9 @@ const styles = StyleSheet.create({
eyeIcon: { eyeIcon: {
position: 'absolute', position: 'absolute',
right: 15, right: 15,
top: 13, top: 15,
zIndex: 1, zIndex: 1,
}, },
eyeIconText: {
fontSize: 20,
},
errorText: { errorText: {
color: '#ff3b30', color: '#ff3b30',
fontSize: 14, fontSize: 14,
+278 -226
View File
@@ -1,7 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState, useCallback } from 'react';
import { import {
View, View,
Text,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
@@ -15,8 +14,11 @@ import {
TextInput, TextInput,
KeyboardAvoidingView, KeyboardAvoidingView,
Alert, Alert,
StatusBar,
Image,
} from 'react-native'; } from 'react-native';
import { LinearGradient } from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import Animated, { FadeInDown, FadeInUp, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import { import {
AlertCircle, AlertCircle,
UserCircle, UserCircle,
@@ -51,10 +53,18 @@ import {
Edit, Edit,
X, X,
Save, Save,
ChevronRight,
LogOut
} from 'lucide-react-native'; } from 'lucide-react-native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { useContractorProfile, useUpdateUserProfile } from '../../Services/BBPS/Hooks'; import { useContractorProfile, useUpdateUserProfile, useESetuAdharVarification } from '../../Services/ApiCall/Hooks';
import colors from '../../Constant/Color'; import { useSelector } from 'react-redux';
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog';
import { generateTransactionId } from '../../Helper/uniqueId';
import NavigationString from '../../Constant/NavigationString';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import ImagePath from '../../Constant/ImagePath';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@@ -102,10 +112,12 @@ interface ContractorData {
interface ProfileHeaderProps { interface ProfileHeaderProps {
data: ContractorData; data: ContractorData;
displayName: string;
} }
interface QuickStatsProps { interface QuickStatsProps {
data: ContractorData; data: ContractorData;
onAadhaarPress: () => void;
} }
interface InfoSectionProps { interface InfoSectionProps {
@@ -129,6 +141,7 @@ interface StatItem {
label: string; label: string;
value: string; value: string;
color: string; color: string;
onPress?: () => void;
} }
interface UpdateProfilePayload { interface UpdateProfilePayload {
@@ -150,16 +163,68 @@ const isValidValue = (value: any): boolean => {
return true; return true;
}; };
// Helper function to check if section has any valid data
const hasSectionData = (values: any[]): boolean => {
return values.some(value => isValidValue(value));
};
// Main Component // Main Component
const ProfileTab: React.FC = () => { const ProfileTab: React.FC = () => {
const { colors, spacing, shadows, isDarkMode } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const { data, refetch, isLoading, error } = useContractorProfile(); const { data, refetch, isLoading, error } = useContractorProfile();
const user = useSelector((state: any) => state.token?.token);
console.log("User: ", user)
const Navigation: any = useNavigation();
const { mutateAsync: eSetuAdharVarification, isPending } = useESetuAdharVarification();
const [editModalVisible, setEditModalVisible] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false);
const [showDialog, setShowDialog] = useState<boolean>(false);
const [isNavigating, setIsNavigating] = useState<boolean>(false);
const handleAadhaarPress = () => {
if (user?.esetuVerified === 'Not Verified' || user?.esetuVerified === null) {
setShowDialog(true);
}
};
useFocusEffect(() => {
setIsNavigating(false)
})
const handleProceedVerification = async () => {
try {
setIsNavigating(true);
const payload = {
contId: user.id,
transactionId: generateTransactionId(),
udf1: "23.77",
udf2: "77.34",
udf3: "string",
udf4: "string",
// responseUrl: "http://dev.192.168.6.194:8090/"
};
const res = await eSetuAdharVarification(payload);
setShowDialog(false);
Navigation.navigate(NavigationString.KycWebViewScreen, { res });
} catch (err) {
Alert.alert('Error', 'Failed to initiate verification. Please try again.');
setIsNavigating(false);
}
};
if (isLoading) { if (isLoading) {
return ( return (
<View style={styles.centerContainer}> <View style={styles.centerContainer}>
<ActivityIndicator size="large" color={colors.primary} /> <ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>Loading profile...</Text> <AppText style={styles.loadingText}>Loading profile...</AppText>
</View> </View>
); );
} }
@@ -168,9 +233,9 @@ const ProfileTab: React.FC = () => {
return ( return (
<View style={styles.centerContainer}> <View style={styles.centerContainer}>
<AlertCircle size={64} color={colors.error} /> <AlertCircle size={64} color={colors.error} />
<Text style={styles.errorText}>{error}</Text> <AppText style={styles.screenErrorText}>{error instanceof Error ? error.message : String(error)}</AppText>
<TouchableOpacity style={styles.retryButton} onPress={refetch}> <TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
<Text style={styles.retryButtonText}>Retry</Text> <AppText style={styles.retryButtonText}>Retry</AppText>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );
@@ -181,7 +246,7 @@ const ProfileTab: React.FC = () => {
return ( return (
<> <>
<ScrollView <ScrollView
style={styles.container} style={[styles.container, { backgroundColor: colors.background }]}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
@@ -192,19 +257,14 @@ const ProfileTab: React.FC = () => {
/> />
} }
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={8}
windowSize={10}
> >
{/* Header with Gradient */} {/* Header with Gradient */}
<ProfileHeader data={data} /> <ProfileHeader data={data} displayName={data?.fullName} />
{/* Quick Stats */} {/* Quick Stats */}
<QuickStats data={data} /> <QuickStats data={data} onAadhaarPress={handleAadhaarPress} />
{/* Personal Information */} {/* Personal Information - Always show */}
<InfoSection <InfoSection
title="Personal Information" title="Personal Information"
IconComponent={UserCircle} IconComponent={UserCircle}
@@ -212,7 +272,11 @@ const ProfileTab: React.FC = () => {
showEdit={true} showEdit={true}
onEdit={() => setEditModalVisible(true)} onEdit={() => setEditModalVisible(true)}
> >
<InfoRow label="Full Name" value={data.firstName+" "+data.middleName+" "+data.lastName} IconComponent={User} /> <InfoRow
label="Full Name"
value={`${data.firstName} ${data.middleName} ${data.lastName}`.trim()}
IconComponent={User}
/>
<InfoRow label="Email" value={data.emailId} IconComponent={Mail} /> <InfoRow label="Email" value={data.emailId} IconComponent={Mail} />
<InfoRow <InfoRow
label="Mobile" label="Mobile"
@@ -222,7 +286,14 @@ const ProfileTab: React.FC = () => {
<InfoRow label="Designation" value={data.designation} IconComponent={Briefcase} /> <InfoRow label="Designation" value={data.designation} IconComponent={Briefcase} />
</InfoSection> </InfoSection>
{/* Company Details */} {/* Company Details - Only show if has data */}
{hasSectionData([
data.company,
data.contractorType,
data.contOrganization,
data.agentWebsite,
data.agentRegno
]) && (
<InfoSection title="Company Details" IconComponent={Building2} delay={0}> <InfoSection title="Company Details" IconComponent={Building2} delay={0}>
<InfoRow label="Company" value={data.company} IconComponent={Building} /> <InfoRow label="Company" value={data.company} IconComponent={Building} />
<InfoRow label="Type" value={data.contractorType} IconComponent={Tag} /> <InfoRow label="Type" value={data.contractorType} IconComponent={Tag} />
@@ -230,16 +301,31 @@ const ProfileTab: React.FC = () => {
<InfoRow label="Website" value={data.agentWebsite} IconComponent={Globe} /> <InfoRow label="Website" value={data.agentWebsite} IconComponent={Globe} />
<InfoRow label="Registration No" value={data.agentRegno} IconComponent={Award} /> <InfoRow label="Registration No" value={data.agentRegno} IconComponent={Award} />
</InfoSection> </InfoSection>
)}
{/* Address Information */} {/* Address Information - Only show if has data */}
{hasSectionData([
data.agentAddress,
data.agentCity,
data.agentState,
data.agentPincode
]) && (
<InfoSection title="Address Information" IconComponent={MapPin} delay={0}> <InfoSection title="Address Information" IconComponent={MapPin} delay={0}>
<InfoRow label="Address" value={data.agentAddress} IconComponent={Home} /> <InfoRow label="Address" value={data.agentAddress} IconComponent={Home} />
<InfoRow label="City" value={data.agentCity} IconComponent={Building2} /> <InfoRow label="City" value={data.agentCity} IconComponent={Building2} />
<InfoRow label="State" value={data.agentState} IconComponent={Map} /> <InfoRow label="State" value={data.agentState} IconComponent={Map} />
<InfoRow label="Pincode" value={data.agentPincode} IconComponent={Mailbox} /> <InfoRow label="Pincode" value={data.agentPincode} IconComponent={Mailbox} />
</InfoSection> </InfoSection>
)}
{/* Tax & Legal */} {/* Tax & Legal - Only show if has data */}
{hasSectionData([
data.orgPAN,
data.orgPANname,
data.orgPANdob,
data.gistnNo,
data.comAadharNo
]) && (
<InfoSection title="Tax & Legal Information" IconComponent={FileText} delay={0}> <InfoSection title="Tax & Legal Information" IconComponent={FileText} delay={0}>
<InfoRow label="PAN Number" value={data.orgPAN} IconComponent={CreditCard} /> <InfoRow label="PAN Number" value={data.orgPAN} IconComponent={CreditCard} />
<InfoRow label="PAN Name" value={data.orgPANname} IconComponent={Type} /> <InfoRow label="PAN Name" value={data.orgPANname} IconComponent={Type} />
@@ -247,8 +333,15 @@ const ProfileTab: React.FC = () => {
<InfoRow label="GSTIN" value={data.gistnNo} IconComponent={Receipt} /> <InfoRow label="GSTIN" value={data.gistnNo} IconComponent={Receipt} />
<InfoRow label="Aadhar" value={data.comAadharNo} IconComponent={FileText} /> <InfoRow label="Aadhar" value={data.comAadharNo} IconComponent={FileText} />
</InfoSection> </InfoSection>
)}
{/* Registration Status */} {/* Registration Status - Only show if has data */}
{hasSectionData([
data.contractorClass,
data.socialCategory,
data.msmeStatus,
data.expiryDate
]) && (
<InfoSection title="Registration Details" IconComponent={ClipboardCheck} delay={0}> <InfoSection title="Registration Details" IconComponent={ClipboardCheck} delay={0}>
<InfoRow label="Class" value={data.contractorClass} IconComponent={Star} /> <InfoRow label="Class" value={data.contractorClass} IconComponent={Star} />
<InfoRow label="Category" value={data.socialCategory} IconComponent={List} /> <InfoRow label="Category" value={data.socialCategory} IconComponent={List} />
@@ -257,9 +350,10 @@ const ProfileTab: React.FC = () => {
label="Expiry Date" label="Expiry Date"
value={data.expiryDate} value={data.expiryDate}
IconComponent={CalendarClock} IconComponent={CalendarClock}
valueColor={colors.warning} valueColor={colors.text}
/> />
</InfoSection> </InfoSection>
)}
<View style={{ height: 40 }} /> <View style={{ height: 40 }} />
</ScrollView> </ScrollView>
@@ -271,6 +365,20 @@ const ProfileTab: React.FC = () => {
data={data} data={data}
onSuccess={refetch} onSuccess={refetch}
/> />
{/* Aadhaar Verification Dialog */}
<ConfirmDialog
visible={showDialog}
title="Aadhaar Verification Required"
message="To access all features, please complete your Aadhaar verification."
confirmText={isPending || isNavigating ? "Processing..." : "Proceed"}
cancelText="Cancel"
onConfirm={handleProceedVerification}
onClose={() => !isPending && !isNavigating && setShowDialog(false)}
loading={isPending || isNavigating}
/>
</> </>
); );
}; };
@@ -289,6 +397,8 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
data, data,
onSuccess, onSuccess,
}) => { }) => {
const { colors } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const [formData, setFormData] = useState<UpdateProfilePayload>({ const [formData, setFormData] = useState<UpdateProfilePayload>({
firstName: data.firstName || '', firstName: data.firstName || '',
middleName: data.middleName || '', middleName: data.middleName || '',
@@ -302,7 +412,6 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
const updateProfileMutation = useUpdateUserProfile(); const updateProfileMutation = useUpdateUserProfile();
// Reset form when modal opens
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
setFormData({ setFormData({
@@ -340,7 +449,6 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
newErrors.mobileNo = 'Invalid mobile number'; newErrors.mobileNo = 'Invalid mobile number';
} }
// Auto-generate fullname
const generatedFullName = `${formData.firstName} ${formData.middleName} ${formData.lastName}`.trim(); const generatedFullName = `${formData.firstName} ${formData.middleName} ${formData.lastName}`.trim();
setFormData(prev => ({ ...prev, fullname: generatedFullName })); setFormData(prev => ({ ...prev, fullname: generatedFullName }));
@@ -354,7 +462,7 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
} }
try { try {
await updateProfileMutation.mutateAsync(formData); const result = await updateProfileMutation.mutateAsync(formData);
Alert.alert('Success', 'Profile updated successfully'); Alert.alert('Success', 'Profile updated successfully');
onSuccess(); onSuccess();
onClose(); onClose();
@@ -374,102 +482,94 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
style={styles.modalOverlay} style={styles.modalOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
> >
<View style={styles.modalContent}> <View style={[styles.modalContent, { backgroundColor: colors.background }]}>
{/* Header */}
<View style={styles.modalHeader}> <View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Edit Personal Information</Text> <AppText variant="h3" style={styles.modalTitle}>Edit Personal Information</AppText>
<TouchableOpacity onPress={onClose} style={styles.closeButton}> <TouchableOpacity onPress={onClose} style={styles.closeButton}>
<X size={24} color={colors.darkText} /> <X size={24} color={colors.text} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Form */}
<ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}> <ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}>
{/* First Name */}
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>First Name *</Text> <AppText variant="bodySmall" style={styles.inputLabel}>First Name *</AppText>
<TextInput <TextInput
style={[styles.input, errors.firstName && styles.inputError]} style={[styles.input, errors.firstName && styles.inputError]}
value={formData.firstName} value={formData.firstName}
onChangeText={(text) => setFormData({ ...formData, firstName: text })} onChangeText={(text) => setFormData({ ...formData, firstName: text })}
placeholder="Enter first name" placeholder="Enter first name"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
/> />
{errors.firstName && ( {errors.firstName && (
<Text style={styles.errorText}>{errors.firstName}</Text> <AppText variant="caption" style={styles.errorText}>{errors.firstName}</AppText>
)} )}
</View> </View>
{/* Middle Name */}
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Middle Name</Text> <AppText variant="bodySmall" style={styles.inputLabel}>Middle Name</AppText>
<TextInput <TextInput
style={styles.input} style={styles.input}
value={formData.middleName} value={formData.middleName}
onChangeText={(text) => setFormData({ ...formData, middleName: text })} onChangeText={(text) => setFormData({ ...formData, middleName: text })}
placeholder="Enter middle name" placeholder="Enter middle name"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
/> />
</View> </View>
{/* Last Name */}
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Last Name *</Text> <AppText variant="bodySmall" style={styles.inputLabel}>Last Name *</AppText>
<TextInput <TextInput
style={[styles.input, errors.lastName && styles.inputError]} style={[styles.input, errors.lastName && styles.inputError]}
value={formData.lastName} value={formData.lastName}
onChangeText={(text) => setFormData({ ...formData, lastName: text })} onChangeText={(text) => setFormData({ ...formData, lastName: text })}
placeholder="Enter last name" placeholder="Enter last name"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
/> />
{errors.lastName && ( {errors.lastName && (
<Text style={styles.errorText}>{errors.lastName}</Text> <AppText variant="caption" style={styles.errorText}>{errors.lastName}</AppText>
)} )}
</View> </View>
{/* Email */}
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Email *</Text> <AppText variant="bodySmall" style={styles.inputLabel}>Email *</AppText>
<TextInput <TextInput
style={[styles.input, errors.emailId && styles.inputError]} style={[styles.input, errors.emailId && styles.inputError]}
value={formData.emailId} value={formData.emailId}
onChangeText={(text) => setFormData({ ...formData, emailId: text })} onChangeText={(text) => setFormData({ ...formData, emailId: text })}
placeholder="Enter email" placeholder="Enter email"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
keyboardType="email-address" keyboardType="email-address"
autoCapitalize="none" autoCapitalize="none"
/> />
{errors.emailId && ( {errors.emailId && (
<Text style={styles.errorText}>{errors.emailId}</Text> <AppText variant="caption" style={styles.errorText}>{errors.emailId}</AppText>
)} )}
</View> </View>
{/* Mobile Number */}
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Mobile Number *</Text> <AppText variant="bodySmall" style={styles.inputLabel}>Mobile Number *</AppText>
<TextInput <TextInput
style={[styles.input, errors.mobileNo && styles.inputError]} style={[styles.input, errors.mobileNo && styles.inputError]}
value={formData.mobileNo} value={formData.mobileNo}
onChangeText={(text) => setFormData({ ...formData, mobileNo: text })} onChangeText={(text) => setFormData({ ...formData, mobileNo: text })}
placeholder="Enter mobile number" placeholder="Enter mobile number"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
keyboardType="phone-pad" keyboardType="phone-pad"
maxLength={10} maxLength={10}
/> />
{errors.mobileNo && ( {errors.mobileNo && (
<Text style={styles.errorText}>{errors.mobileNo}</Text> <AppText variant="caption" style={styles.errorText}>{errors.mobileNo}</AppText>
)} )}
</View> </View>
</ScrollView> </ScrollView>
{/* Footer Buttons */}
<View style={styles.modalFooter}> <View style={styles.modalFooter}>
<TouchableOpacity <TouchableOpacity
style={[styles.button, styles.cancelButton]} style={[styles.button, styles.cancelButton]}
onPress={onClose} onPress={onClose}
disabled={updateProfileMutation.isPending} disabled={updateProfileMutation.isPending}
> >
<Text style={styles.cancelButtonText}>Cancel</Text> <AppText variant="button" style={styles.cancelButtonText}>Cancel</AppText>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
@@ -478,11 +578,11 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
disabled={updateProfileMutation.isPending} disabled={updateProfileMutation.isPending}
> >
{updateProfileMutation.isPending ? ( {updateProfileMutation.isPending ? (
<ActivityIndicator size="small" color={colors.white} /> <ActivityIndicator size="small" color={colors.primary} />
) : ( ) : (
<> <>
<Save size={18} color={colors.white} /> <Save size={18} color={colors.buttonText} />
<Text style={styles.saveButtonText}>Save</Text> <AppText variant="button" style={styles.saveButtonText}>Save</AppText>
</> </>
)} )}
</TouchableOpacity> </TouchableOpacity>
@@ -494,7 +594,10 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
}; };
// Profile Header Component - OPTIMIZED // Profile Header Component - OPTIMIZED
const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data }) => { const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayName }) => {
const { colors } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const getInitials = (name: string): string => { const getInitials = (name: string): string => {
if (!name || typeof name !== 'string' || name.trim().length === 0) { if (!name || typeof name !== 'string' || name.trim().length === 0) {
const fallbackName = `${data.firstName || ''} ${data.lastName || ''}`.trim(); const fallbackName = `${data.firstName || ''} ${data.lastName || ''}`.trim();
@@ -528,198 +631,148 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data }) => {
return ( return (
<View> <View>
<LinearGradient <View
colors={[colors.primary, colors.primaryLight]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.headerGradient} style={styles.headerGradient}
> >
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
<View style={styles.avatar}> <View style={styles.avatar}>
<Text style={styles.avatarText}>{getInitials(data.fullName)}</Text> <AppText variant="h1" color={colors.text} style={styles.avatarText}>{getInitials(displayName)}</AppText>
</View> </View>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}> <View style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}>
<View style={styles.statusDot} /> <View style={styles.statusDot} />
</View> </View>
</View> </View>
<Text style={styles.headerName}> <AppText variant="h2" style={styles.headerName}>
{data.fullName || {displayName || 'Unknown User'}
`${data.firstName} ${data.lastName}`.trim() || </AppText>
'Unknown User'}
</Text>
<View style={styles.roleBadge}> <View style={styles.roleBadge}>
<ShieldCheck size={16} color={colors.white} /> <ShieldCheck size={16} color={colors.text} />
<Text style={styles.roleText}>{data.role}</Text> <AppText variant="caption" style={styles.roleText}>{data.role}</AppText>
</View>
</View> </View>
</LinearGradient>
</View> </View>
); );
}); });
ProfileHeader.displayName = 'ProfileHeader'; ProfileHeader.displayName = 'ProfileHeader';
// Quick Stats Component - OPTIMIZED // Quick Stats Component - Glass Cards
const QuickStats: React.FC<QuickStatsProps> = React.memo(({ data }) => { const QuickStats: React.FC<QuickStatsProps> = React.memo(({ data, onAadhaarPress }) => {
const user = useSelector((state: any) => state.token?.token);
const { colors } = useTheme();
const stats: StatItem[] = useMemo( const stats: StatItem[] = useMemo(
() => [ () => [
{ {
Icon: CheckCircle, Icon: CheckCircle,
label: 'Status', label: 'Aadhaar',
value: data.isActive, value: user?.esetuVerified || 'Pending',
color: colors.success, color: user?.esetuVerified === 'Not Verified' ? colors.error : colors.success,
onPress: onAadhaarPress,
}, },
{ {
Icon: ShieldCheck, Icon: ShieldCheck,
label: '2FA', label: '2FA',
value: data.twoFactorAuthEnabled ? 'Enabled' : 'Disabled', value: data.twoFactorAuthEnabled ? 'Enabled' : 'Disabled',
color: data.twoFactorAuthEnabled ? colors.success : colors.grayText, color: data.twoFactorAuthEnabled ? colors.success : colors.textSecondary,
},
{
Icon: FileText,
label: 'Registration',
value: data.registrationStatus,
color: colors.info,
}, },
// ...
], ],
[data.isActive, data.twoFactorAuthEnabled, data.registrationStatus] [data.twoFactorAuthEnabled, user?.esetuVerified, onAadhaarPress, colors]
); );
return ( return (
<View style={styles.statsContainer}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 24 }}>
{stats.map((stat) => ( {stats.map((stat, index) => {
<View key={stat.label} style={styles.statCard}> const content = (
<stat.Icon size={28} color={stat.color} /> <Animated.View
<Text style={styles.statValue} numberOfLines={2}> entering={FadeInUp.duration(600).delay(index * 100)}
{stat.value} style={{
</Text> flex: 1, backgroundColor: colors.glass, borderColor: colors.overlay, borderWidth: 1,
<Text style={styles.statLabel}>{stat.label}</Text> borderRadius: 20, padding: 12, alignItems: 'center', marginHorizontal: 4,
minHeight: 100, justifyContent: 'center'
}}
>
<View style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: stat.color + '10', alignItems: 'center', justifyContent: 'center', marginBottom: 8 }}>
<stat.Icon size={20} color={stat.color} />
</View> </View>
))} <AppText variant="bodySmall" color={colors.text} numberOfLines={1} style={{ textAlign: 'center' }}>{stat.value}</AppText>
<AppText variant="caption" color={colors.textSecondary}>{stat.label}</AppText>
</Animated.View>
);
return stat.onPress ? (
<TouchableOpacity key={stat.label} style={{ flex: 1 }} onPress={stat.onPress}>{content}</TouchableOpacity>
) : (
<View key={stat.label} style={{ flex: 1 }}>{content}</View>
);
})}
</View> </View>
); );
}); });
QuickStats.displayName = 'QuickStats'; // Info Section Component - Glass Card Container
// Info Section Component - OPTIMIZED
const InfoSection: React.FC<InfoSectionProps> = React.memo( const InfoSection: React.FC<InfoSectionProps> = React.memo(
({ title, IconComponent, children, onEdit, showEdit = false }) => { ({ title, IconComponent, children, onEdit, showEdit = false, delay = 0 }) => {
const { colors } = useTheme();
return ( return (
<View style={styles.section}> <Animated.View
<View style={styles.sectionHeader}> entering={FadeInUp.duration(600).delay(delay)}
<View style={styles.sectionHeaderLeft}> style={{
<View style={styles.sectionIconContainer}> backgroundColor: colors.glass,
<IconComponent size={24} color={colors.primary} /> borderColor: colors.overlay,
borderWidth: 1,
borderRadius: 24,
padding: 20,
marginBottom: 16
}}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ padding: 8, backgroundColor: colors.primary + '10', borderRadius: 10, marginRight: 12 }}>
<IconComponent size={20} color={colors.primary} />
</View> </View>
<Text style={styles.sectionTitle}>{title}</Text> <AppText variant="h4" color={colors.text}>{title}</AppText>
</View> </View>
{showEdit && onEdit && ( {showEdit && onEdit && (
<TouchableOpacity style={styles.editButton} onPress={onEdit}> <TouchableOpacity onPress={onEdit} style={{ padding: 4 }}>
<Edit size={20} color={colors.primary} /> <Edit size={18} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
<View style={styles.sectionContent}>{children}</View> <View>{children}</View>
</View> </Animated.View>
); );
} }
); );
InfoSection.displayName = 'InfoSection'; // Info Row Component
// Info Row Component - OPTIMIZED
const InfoRow: React.FC<InfoRowProps> = React.memo( const InfoRow: React.FC<InfoRowProps> = React.memo(
({ label, value, IconComponent, valueColor }) => { ({ label, value, IconComponent, valueColor }) => {
// Return null if value is not valid const { colors } = useTheme();
if (!isValidValue(value)) {
return null; if (!isValidValue(value)) return null;
}
return ( return (
<View style={styles.infoRow}> <View style={{ flexDirection: 'row', marginBottom: 16, alignItems: 'flex-start' }}>
<View style={styles.infoRowTouchable}> <IconComponent size={16} color={colors.textSecondary} style={{ marginTop: 4, marginRight: 12 }} />
<View style={styles.infoRowContent}> <View style={{ flex: 1 }}>
<View style={styles.infoIconContainer}> <AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 2 }}>{label}</AppText>
<IconComponent size={18} color={colors.primary} /> <AppText variant='bodySmall' color={valueColor || colors.text}>{value}</AppText>
</View>
<View style={styles.infoTextContainer}>
<Text style={styles.infoLabel}>{label}</Text>
<Text
style={[styles.infoValue, valueColor && { color: valueColor }]}
>
{value}
</Text>
</View>
</View>
</View> </View>
</View> </View>
); );
} }
); );
InfoRow.displayName = 'InfoRow';
// Styles // Styles
interface Styles { const createStyles = (colors: any) => StyleSheet.create({
container: ViewStyle;
contentContainer: ViewStyle;
centerContainer: ViewStyle;
loadingText: TextStyle;
errorText: TextStyle;
retryButton: ViewStyle;
retryButtonText: TextStyle;
headerGradient: ViewStyle;
avatarContainer: ViewStyle;
avatar: ViewStyle;
avatarText: TextStyle;
statusBadge: ViewStyle;
statusDot: ViewStyle;
headerName: TextStyle;
roleBadge: ViewStyle;
roleText: TextStyle;
statsContainer: ViewStyle;
statCard: ViewStyle;
statValue: TextStyle;
statLabel: TextStyle;
section: ViewStyle;
sectionHeader: ViewStyle;
sectionHeaderLeft: ViewStyle;
sectionIconContainer: ViewStyle;
sectionTitle: TextStyle;
sectionContent: ViewStyle;
infoRow: ViewStyle;
infoRowTouchable: ViewStyle;
infoRowContent: ViewStyle;
infoIconContainer: ViewStyle;
infoTextContainer: ViewStyle;
infoLabel: TextStyle;
infoValue: TextStyle;
editButton: ViewStyle;
modalOverlay: ViewStyle;
modalContent: ViewStyle;
modalHeader: ViewStyle;
modalTitle: TextStyle;
closeButton: ViewStyle;
formContainer: ViewStyle;
inputGroup: ViewStyle;
inputLabel: TextStyle;
input: TextStyle;
inputError: TextStyle;
errorText: TextStyle;
modalFooter: ViewStyle;
button: ViewStyle;
cancelButton: ViewStyle;
saveButton: ViewStyle;
cancelButtonText: TextStyle;
saveButtonText: TextStyle;
}
const styles = StyleSheet.create<Styles>({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.lightGray, paddingHorizontal: 20
}, },
contentContainer: { contentContainer: {
paddingBottom: 20, paddingBottom: 20,
@@ -728,16 +781,16 @@ const styles = StyleSheet.create<Styles>({
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.white, backgroundColor: colors.background,
padding: 20, padding: 20,
}, },
loadingText: { loadingText: {
marginTop: 16, marginTop: 16,
fontSize: 16, fontSize: 16,
color: colors.grayText, color: colors.textSecondary,
fontWeight: '500', fontWeight: '500',
}, },
errorText: { screenErrorText: {
marginTop: 16, marginTop: 16,
fontSize: 16, fontSize: 16,
color: colors.error, color: colors.error,
@@ -762,18 +815,10 @@ const styles = StyleSheet.create<Styles>({
fontWeight: '600', fontWeight: '600',
}, },
// Header Styles
headerGradient: { headerGradient: {
paddingTop: Platform.OS === 'ios' ? 60 : 40, paddingTop: Platform.OS === 'ios' ? 60 : 40,
paddingBottom: 30, paddingBottom: 30,
alignItems: 'center', alignItems: 'center',
borderBottomLeftRadius: 32,
borderBottomRightRadius: 32,
elevation: 8,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
}, },
avatarContainer: { avatarContainer: {
position: 'relative', position: 'relative',
@@ -783,11 +828,11 @@ const styles = StyleSheet.create<Styles>({
width: 100, width: 100,
height: 100, height: 100,
borderRadius: 50, borderRadius: 50,
backgroundColor: colors.white, backgroundColor: colors.card,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderWidth: 4, borderWidth: 4,
borderColor: colors.white, borderColor: colors.card,
elevation: 4, elevation: 4,
shadowColor: colors.shadow, shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
@@ -807,7 +852,7 @@ const styles = StyleSheet.create<Styles>({
height: 24, height: 24,
borderRadius: 12, borderRadius: 12,
borderWidth: 3, borderWidth: 3,
borderColor: colors.white, borderColor: colors.card, // Updated
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
@@ -820,7 +865,7 @@ const styles = StyleSheet.create<Styles>({
headerName: { headerName: {
fontSize: 26, fontSize: 26,
fontWeight: '700', fontWeight: '700',
color: colors.white, color: colors.text, // Updated
marginBottom: 8, marginBottom: 8,
textAlign: 'center', textAlign: 'center',
paddingHorizontal: 20, paddingHorizontal: 20,
@@ -828,19 +873,18 @@ const styles = StyleSheet.create<Styles>({
roleBadge: { roleBadge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.25)', backgroundColor: colors.primary + '20', // Updated opacity approach
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 6, paddingVertical: 6,
borderRadius: 20, borderRadius: 20,
gap: 6, gap: 6,
}, },
roleText: { roleText: {
color: colors.white, color: colors.primary, // Updated
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
}, },
// Stats Styles
statsContainer: { statsContainer: {
flexDirection: 'row', flexDirection: 'row',
marginHorizontal: 16, marginHorizontal: 16,
@@ -850,7 +894,7 @@ const styles = StyleSheet.create<Styles>({
}, },
statCard: { statCard: {
flex: 1, flex: 1,
backgroundColor: colors.white, backgroundColor: colors.card,
borderRadius: 16, borderRadius: 16,
padding: 16, padding: 16,
alignItems: 'center', alignItems: 'center',
@@ -863,21 +907,20 @@ const styles = StyleSheet.create<Styles>({
statValue: { statValue: {
fontSize: 14, fontSize: 14,
fontWeight: '700', fontWeight: '700',
color: colors.darkText, color: colors.text,
marginTop: 8, marginTop: 8,
textAlign: 'center', textAlign: 'center',
}, },
statLabel: { statLabel: {
fontSize: 12, fontSize: 12,
color: colors.grayText, color: colors.textSecondary,
marginTop: 4, marginTop: 4,
}, },
// Section Styles
section: { section: {
marginHorizontal: 16, marginHorizontal: 16,
marginTop: 24, marginTop: 24,
backgroundColor: colors.white, backgroundColor: colors.card,
borderRadius: 20, borderRadius: 20,
padding: 20, padding: 20,
elevation: 2, elevation: 2,
@@ -904,7 +947,7 @@ const styles = StyleSheet.create<Styles>({
width: 40, width: 40,
height: 40, height: 40,
borderRadius: 12, borderRadius: 12,
backgroundColor: colors.primaryHighlight, backgroundColor: colors.primary + '10', // Updated
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: 12, marginRight: 12,
@@ -912,7 +955,7 @@ const styles = StyleSheet.create<Styles>({
sectionTitle: { sectionTitle: {
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: colors.darkText, color: colors.text,
flex: 1, flex: 1,
}, },
sectionContent: { sectionContent: {
@@ -921,17 +964,16 @@ const styles = StyleSheet.create<Styles>({
editButton: { editButton: {
padding: 8, padding: 8,
borderRadius: 8, borderRadius: 8,
backgroundColor: colors.primaryHighlight, backgroundColor: colors.primary + '10', // Updated
}, },
// Info Row Styles
infoRow: { infoRow: {
borderRadius: 12, borderRadius: 12,
overflow: 'hidden', overflow: 'hidden',
}, },
infoRowTouchable: { infoRowTouchable: {
padding: 14, padding: 14,
backgroundColor: colors.lightestGray, backgroundColor: colors.background, // Updated
borderRadius: 12, borderRadius: 12,
}, },
infoRowContent: { infoRowContent: {
@@ -942,7 +984,7 @@ const styles = StyleSheet.create<Styles>({
width: 32, width: 32,
height: 32, height: 32,
borderRadius: 8, borderRadius: 8,
backgroundColor: colors.white, backgroundColor: colors.card, // Updated
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: 12, marginRight: 12,
@@ -954,29 +996,28 @@ const styles = StyleSheet.create<Styles>({
}, },
infoLabel: { infoLabel: {
fontSize: 13, fontSize: 13,
color: colors.grayText, color: colors.textSecondary,
fontWeight: '500', fontWeight: '500',
lineHeight: 18, lineHeight: 18,
}, },
infoValue: { infoValue: {
fontSize: 15, fontSize: 15,
color: colors.darkText, color: colors.text,
fontWeight: '600', fontWeight: '600',
lineHeight: 21, lineHeight: 21,
}, },
// Modal Styles
modalOverlay: { modalOverlay: {
flex: 1, flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)', backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end', justifyContent: 'flex-end',
}, },
modalContent: { modalContent: {
backgroundColor: colors.white,
borderTopLeftRadius: 24, borderTopLeftRadius: 24,
borderTopRightRadius: 24, borderTopRightRadius: 24,
maxHeight: '90%', maxHeight: '90%',
paddingTop: 20, paddingTop: 20,
borderRadius: 30
}, },
modalHeader: { modalHeader: {
flexDirection: 'row', flexDirection: 'row',
@@ -990,7 +1031,7 @@ const styles = StyleSheet.create<Styles>({
modalTitle: { modalTitle: {
fontSize: 20, fontSize: 20,
fontWeight: '700', fontWeight: '700',
color: colors.darkText, color: colors.text,
}, },
closeButton: { closeButton: {
padding: 4, padding: 4,
@@ -1005,7 +1046,7 @@ const styles = StyleSheet.create<Styles>({
inputLabel: { inputLabel: {
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
color: colors.darkText, color: colors.text,
marginBottom: 8, marginBottom: 8,
}, },
input: { input: {
@@ -1014,8 +1055,8 @@ const styles = StyleSheet.create<Styles>({
borderRadius: 12, borderRadius: 12,
padding: 14, padding: 14,
fontSize: 15, fontSize: 15,
color: colors.darkText, color: colors.text,
backgroundColor: colors.white, backgroundColor: colors.card, // Updated
}, },
inputError: { inputError: {
borderColor: colors.error, borderColor: colors.error,
@@ -1042,7 +1083,7 @@ const styles = StyleSheet.create<Styles>({
gap: 8, gap: 8,
}, },
cancelButton: { cancelButton: {
backgroundColor: colors.lightestGray, backgroundColor: colors.background, // Updated
}, },
saveButton: { saveButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
@@ -1050,13 +1091,24 @@ const styles = StyleSheet.create<Styles>({
cancelButtonText: { cancelButtonText: {
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '600',
color: colors.darkText, color: colors.text,
}, },
saveButtonText: { saveButtonText: {
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '600',
color: colors.white, color: colors.white,
}, },
navigationOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 999,
},
}); });
export default ProfileTab; export default ProfileTab;
+108 -85
View File
@@ -3,23 +3,22 @@ import {
SafeAreaView, SafeAreaView,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text,
TouchableOpacity, TouchableOpacity,
View, View,
Platform, Platform,
} from 'react-native'; } from 'react-native';
import React, { use, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useNavigation, useRoute, NavigationProp, RouteProp } from '@react-navigation/native'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton'; import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import MyInput from '../../Component/MyInput'; import MyInput from '../../components/MyInput';
import { showToast } from '../../Constant/ShowDailog'; import { showToast } from '../../Constant/ShowDailog';
import { PARTNER_API_ENDPOINT } from '../../Services/BBPS/ApiUrls'; import { usePartnerShipSubmit } from '../../Services/ApiCall/Hooks';
import { usePartnerShipSubmit } from '../../Services/BBPS/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Navigation } from 'lucide-react-native';
import { NavigationString } from '../../Constant/AllImports'; import { NavigationString } from '../../Constant/AllImports';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Trash2, UserPlus } from 'lucide-react-native';
type Partner = { type Partner = {
partnerName: string; partnerName: string;
@@ -32,11 +31,12 @@ type Navigation = {
replace: (route: string) => void; replace: (route: string) => void;
}; };
type PartnershipParams = { type PartnershipParams = {
otherId: string | number otherId: string | number;
}; };
const Partnership = () => { const Partnership = () => {
const route = useRoute<RouteProp<{ params: PartnershipParams }, 'params'>>(); const route = useRoute<RouteProp<{ params: PartnershipParams }, 'params'>>();
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation() as Navigation; const navigation = useNavigation() as Navigation;
const [partners, setPartners] = useState<Partner[]>([ const [partners, setPartners] = useState<Partner[]>([
{ {
@@ -49,9 +49,11 @@ const Partnership = () => {
]); ]);
const [focusedField, setFocusedField] = useState<string>(''); const [focusedField, setFocusedField] = useState<string>('');
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const { mutateAsync: partnerShipSubmit } = usePartnerShipSubmit(); const { mutateAsync: partnerShipSubmit } = usePartnerShipSubmit();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
const handleChange = (index: number, name: keyof Partner, value: string) => { const handleChange = (index: number, name: keyof Partner, value: string) => {
const updated = [...partners]; const updated = [...partners];
updated[index][name] = value; updated[index][name] = value;
@@ -90,9 +92,8 @@ const Partnership = () => {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
setLoading(true) setLoading(true);
try { try {
// Convert percentage strings to numbers and validate
const payload = partners.map((p, i) => { const payload = partners.map((p, i) => {
const percentage = Number(p.percentage); const percentage = Number(p.percentage);
@@ -105,174 +106,196 @@ const Partnership = () => {
ptnrAadharNo: p.aadharNumber, ptnrAadharNo: p.aadharNumber,
ptnrMobNo: p.mobileNumber, ptnrMobNo: p.mobileNumber,
ptnrName: p.partnerName, ptnrName: p.partnerName,
ptnrPop: percentage, // Ensure this is a number ptnrPop: percentage,
ptnrPANNo: p.partnerPan, ptnrPANNo: p.partnerPan,
ptnrNo: i, ptnrNo: i,
}; };
}); });
// Validate total percentage equals 100%
const totalPercentage = payload.reduce((sum, p) => sum + p.ptnrPop, 0); const totalPercentage = payload.reduce((sum, p) => sum + p.ptnrPop, 0);
if (totalPercentage !== 100) { if (totalPercentage !== 100) {
showToast(`Total percentage must equal 100% (currently ${totalPercentage}%)`); showToast(`Total percentage must equal 100% (currently ${totalPercentage}%)`);
return; return;
} }
console.log("Partner ship payload:", payload);
const response = await partnerShipSubmit(payload); const response = await partnerShipSubmit(payload);
console.log("first ====== ", response)
if (response?.statusCode === 200) { if (response?.statusCode === 200) {
console.log('✅ Partners submitted successfully:', response);
showToast(response.message || 'Submitted successfully!'); showToast(response.message || 'Submitted successfully!');
navigation.replace(NavigationString.Home); navigation.replace(NavigationString.Home);
} }
} catch (error: any) { } catch (error: any) {
console.error('❌ Submission failed:', error.message); console.error('❌ Submission failed:', error.message);
showToast(error.message || 'Submission failed. Please check partner details.'); showToast(error.message || 'Submission failed. Please check partner details.');
} finally { } finally {
setLoading(false) setLoading(false);
} }
}; };
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<Text style={[styles.heading, { paddingTop: insets.top }]}>Partnership Details</Text> <View style={[styles.header, { paddingTop: insets.top + spacing.s }]}>
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
Partnership Details
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Add all partners and their share percentages
</AppText>
</View>
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40} keyboardVerticalOffset={40}
> >
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled"> <ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{partners.map((partner, index) => ( {partners.map((partner, index) => (
<View style={{ marginBottom: 20 }} key={index}> <Animated.View
<Text style={styles.partnerTitle}>Partner {index + 1}</Text> key={index}
entering={FadeInUp.delay(index * 80).springify()}
style={[styles.card, shadows.medium]}
>
<View style={styles.cardHeader}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
Partner {index + 1}
</AppText>
{partners.length > 1 && (
<TouchableOpacity onPress={() => handleRemove(index)} activeOpacity={0.7}>
<Trash2 size={18} color={colors.error} />
</TouchableOpacity>
)}
</View>
<MyInput <MyInput
icon="user" label="Partner Name"
placeholder="Name of Partner *" placeholder="Enter Partner Name"
value={partner.partnerName} value={partner.partnerName}
onChangeText={text => handleChange(index, 'partnerName', text)} onChangeText={text => handleChange(index, 'partnerName', text)}
onFocus={() => setFocusedField(`partnerName-${index}`)} onFocus={() => setFocusedField(`partnerName-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `partnerName-${index}`} focused={focusedField === `partnerName-${index}`}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="percent" label="Share Percentage"
placeholder="Partnership Percentage *" placeholder="Enter Percentage"
value={partner.percentage} value={partner.percentage}
onChangeText={text => handleChange(index, 'percentage', text)} onChangeText={text => handleChange(index, 'percentage', text)}
onFocus={() => setFocusedField(`percentage-${index}`)} onFocus={() => setFocusedField(`percentage-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `percentage-${index}`} focused={focusedField === `percentage-${index}`}
keyboardType="numeric" keyboardType="numeric"
maxlength={3} maxLength={3}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="id-card" label="PAN of Partner"
placeholder="PAN of Partner *" placeholder="Enter PAN Number"
value={partner.partnerPan} value={partner.partnerPan}
onChangeText={text => handleChange(index, 'partnerPan', text)} onChangeText={text => handleChange(index, 'partnerPan', text)}
onFocus={() => setFocusedField(`partnerPan-${index}`)} onFocus={() => setFocusedField(`partnerPan-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `partnerPan-${index}`} focused={focusedField === `partnerPan-${index}`}
autoCapitalize="characters" autoCapitalize="characters"
maxlength={10} maxLength={10}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="credit-card" label="Aadhaar Number"
placeholder="Aadhar Number *" placeholder="Enter 12-digit Aadhaar"
value={partner.aadharNumber} value={partner.aadharNumber}
onChangeText={text => handleChange(index, 'aadharNumber', text)} onChangeText={text => handleChange(index, 'aadharNumber', text)}
onFocus={() => setFocusedField(`aadharNumber-${index}`)} onFocus={() => setFocusedField(`aadharNumber-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `aadharNumber-${index}`} focused={focusedField === `aadharNumber-${index}`}
keyboardType="numeric" keyboardType="numeric"
maxlength={12} maxLength={12}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="phone" label="Mobile Number"
placeholder="Mobile Number *" placeholder="Enter Mobile Number"
value={partner.mobileNumber} value={partner.mobileNumber}
onChangeText={text => handleChange(index, 'mobileNumber', text)} onChangeText={text => handleChange(index, 'mobileNumber', text)}
onFocus={() => setFocusedField(`mobileNumber-${index}`)} onFocus={() => setFocusedField(`mobileNumber-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `mobileNumber-${index}`} focused={focusedField === `mobileNumber-${index}`}
keyboardType="phone-pad" keyboardType="phone-pad"
maxlength={10} maxLength={10}
/> />
</Animated.View>
{partners.length > 1 && (
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
<Text style={styles.deleteText}>Delete Partner</Text>
</TouchableOpacity>
)}
</View>
))} ))}
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}> <TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
<Text style={styles.addText}>+ Add Partner</Text> <UserPlus size={16} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
Add Partner
</AppText>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>
<PrimaryButton title={loading ? "Loading..." : "Submit" }onPress={handleSubmit} /> <View style={styles.footer}>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
</View>
</View> </View>
); );
}; };
export default Partnership; export default Partnership;
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
heading: { header: {
fontSize: 18, paddingHorizontal: spacing.m,
fontWeight: 'bold', paddingBottom: spacing.m,
marginBottom: 10, borderBottomWidth: 1,
marginTop: 10, borderBottomColor: colors.border,
textAlign: 'center', backgroundColor: colors.surface,
},
partnerTitle: {
fontWeight: '600',
fontSize: 16,
marginBottom: 10,
}, },
scrollContainer: { scrollContainer: {
// backgroundColor: color.background,
flexGrow: 1, flexGrow: 1,
padding: spacing.m,
paddingBottom: 30,
},
card: {
backgroundColor: colors.surface,
borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
paddingBottom: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
}, },
addButton: { addButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
padding: 10, flexDirection: 'row',
marginTop: 10,
borderRadius: 10,
alignItems: 'center', alignItems: 'center',
width: 140, justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 12,
alignSelf: 'flex-end', alignSelf: 'flex-end',
}, },
addText: { footer: {
color: 'white', padding: spacing.m,
fontWeight: '600', borderTopWidth: 1,
}, borderTopColor: colors.border,
deleteButton: { backgroundColor: colors.surface,
backgroundColor: '#FF5C5C',
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 140,
alignSelf: 'flex-end',
},
deleteText: {
color: 'white',
fontWeight: '600',
}, },
}); });
+137 -220
View File
@@ -1,31 +1,27 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, SafeAreaView, ScrollView, BackHandler, ToastAndroid, Button, Alert } from 'react-native'; import { View, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, ScrollView, BackHandler, ToastAndroid, Alert } from 'react-native';
// import AntDesign from 'react-native-vector-icons/AntDesign';
import { ArrowLeft } from 'lucide-react-native'; import { ArrowLeft } from 'lucide-react-native';
import { NavigationProp, RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
// import { sendAadhaarOtp, verifyAadhaarOtp, verifyGstNumber, verifyPanHandlerRes } from '../../utils/helper/ragisterationApiCalls';
// import { getAllMsMeOrg, getCityDropdown, getClassDropdown, getMsmeDropdown, getOrgDropdown, getSocialDropdown, getStateDropdown } from '../../utils/helper/dropdownApis';
import { ApiResponse, DocumentsType, FormDataType, OrgType, UserData } from './types/types'; import { ApiResponse, DocumentsType, FormDataType, OrgType, UserData } from './types/types';
import { pick } from '@react-native-documents/picker'; import { pick } from '@react-native-documents/picker';
// import { REJ_STAGE_TWO } from '../../utils/services/apiUrl';
// import LoaderModel from '../../component/LoaderModal';
import axios from 'axios'; import axios from 'axios';
import StageOne from './ragistrationStages/StageOne'; import StageOne from './ragistrationStages/StageOne';
import StageTwo from './ragistrationStages/StageTwo'; import StageTwo from './ragistrationStages/StageTwo';
import StageZero from './ragistrationStages/StageZero'; import StageZero from './ragistrationStages/StageZero';
import StageThree from './ragistrationStages/StageThree'; import StageThree from './ragistrationStages/StageThree';
import StageFour from './ragistrationStages/StageFour'; import StageFour from './ragistrationStages/StageFour';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton'; import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import StepProgressBar from '../../Component/StepProgressBar'; import StepProgressBar from '../../Component/StepProgressBar';
import { formatDateInput } from '../../Helper/formatDateInput'; import { formatDateInput } from '../../Helper/formatDateInput';
import { showToast } from '../../Constant/ShowDailog'; import { showToast } from '../../Constant/ShowDailog';
import { useCityDropdown, useContractorStageTwoSubmit, usefetchMasterData, useSendAadhaarOtp, useVerifyAadhaarOtp, useVerifyGstNumber, useVerifyPan } from '../../Services/BBPS/Hooks'; import { useCityDropdown, useContractorStageTwoSubmit, usefetchMasterData, useSendAadhaarOtp, useVerifyAadhaarOtp, useVerifyGstNumber, useVerifyPan } from '../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import NavigationString from '../../Constant/NavigationString';
type RouteParams = { type RouteParams = {
userData?: { userData?: {
@@ -38,6 +34,7 @@ type RouteParams = {
}; };
const Registration: React.FC = () => { const Registration: React.FC = () => {
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const { params } = useRoute<RouteProp<Record<string, RouteParams>, string>>(); const { params } = useRoute<RouteProp<Record<string, RouteParams>, string>>();
const userData = params as RouteParams; const userData = params as RouteParams;
@@ -102,7 +99,9 @@ const Registration: React.FC = () => {
const { mutateAsync: verifyGstNumber } = useVerifyGstNumber(); const { mutateAsync: verifyGstNumber } = useVerifyGstNumber();
const { mutateAsync: contractorStageTwoSubmit } = useContractorStageTwoSubmit(); const { mutateAsync: contractorStageTwoSubmit } = useContractorStageTwoSubmit();
const { data: masterData } = usefetchMasterData(); const { data: masterData } = usefetchMasterData();
console.log("master",masterData)
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
useEffect(() => { useEffect(() => {
if (!masterData?.data) return; if (!masterData?.data) return;
const contractorClasses = masterData.data.contractorClasses || []; const contractorClasses = masterData.data.contractorClasses || [];
@@ -139,28 +138,15 @@ useEffect(() => {
const { mutate: cityDropdownMutate } = useCityDropdown(); const { mutate: cityDropdownMutate } = useCityDropdown();
const { data: MasterData}=usefetchMasterData() const { data: MasterData } = usefetchMasterData();
const user = useSelector((state: any) => state.token.token); const user = useSelector((state: any) => state.token.token);
const steps = ['', '', '', '', '']; const steps = ['', '', '', '', ''];
const dropdownStyle = {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: '#fff',
marginBottom: 10,
}
useEffect(() => { useEffect(() => {
const fetchUserData = async (user: any) => { const fetchUserData = async (user: any) => {
console.log("hitting it ")
try { try {
console.log("hitting it 1 ")
console.log("object====",user)
if (user) { if (user) {
const parsedUser = user; const parsedUser = user;
console.log(parsedUser);
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
firstName: parsedUser?.firstName || userData?.userData?.firstName, firstName: parsedUser?.firstName || userData?.userData?.firstName,
@@ -169,7 +155,6 @@ useEffect(() => {
middleName: parsedUser?.middleName || userData?.userData?.middleName, middleName: parsedUser?.middleName || userData?.userData?.middleName,
email: parsedUser?.email || userData?.userData?.email, email: parsedUser?.email || userData?.userData?.email,
})); }));
console.log('User Data from Storage:', parsedUser);
} else { } else {
console.log('No user data found'); console.log('No user data found');
} }
@@ -186,25 +171,11 @@ useEffect(() => {
gstin = gstin.toUpperCase(); gstin = gstin.toUpperCase();
const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]$/; const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;
if (!panRegex.test(pan)) { if (!panRegex.test(pan)) {
return { valid: false, message: "Invalid PAN format. It should be like 'ABCDE1234F'." }; return { valid: false, message: "Invalid PAN format. It should be like 'ABCDE1234F'." };
} }
// if (!gstinRegex.test(gstin)) {
// return { valid: false, message: "Invalid GSTIN format. It should be like '22ABCDE1234F1Z5'." };
// }
// const gstinPAN = gstin.slice(2, 12); // Extract PAN part from GSTIN
// if (pan !== gstinPAN) {
// return {
// valid: false,
// message: "PAN does not match the PAN embedded in GSTIN.",
// };
// }
return { valid: true, message: 'PAN and GSTIN are correctly linked.' }; return { valid: true, message: 'PAN and GSTIN are correctly linked.' };
}; };
@@ -213,7 +184,7 @@ useEffect(() => {
}; };
const verifyPanHandler = async () => { const verifyPanHandler = async () => {
setPanVerifying(true); // Start loading setPanVerifying(true);
const payload = { const payload = {
dob: formData?.dateOfIncorp.trim(), dob: formData?.dateOfIncorp.trim(),
name: formData?.orgPanName.trim(), name: formData?.orgPanName.trim(),
@@ -226,11 +197,10 @@ useEffect(() => {
} else { } else {
setPanVerified(false); setPanVerified(false);
} }
setPanVerifying(false); // Stop loading setPanVerifying(false);
}; };
const sendAdharOTPHandler = async (text: string) => { const sendAdharOTPHandler = async (text: string) => {
console.log('Sending Aadhaar OTP...');
setAadhaarVerifying(true); setAadhaarVerifying(true);
setadharreference(''); setadharreference('');
@@ -329,7 +299,6 @@ useEffect(() => {
gistnNo: formData.gstnNo, gistnNo: formData.gstnNo,
stateName: MasterData?.data?.states?.find((state: any) => state.stateId == formData.state)?.stateName || '', stateName: MasterData?.data?.states?.find((state: any) => state.stateId == formData.state)?.stateName || '',
}; };
console.log('Form Data for registration',formData);
try { try {
const fileData = new FormData(); const fileData = new FormData();
@@ -341,38 +310,33 @@ useEffect(() => {
fileData.append('pancardfile', documents.panCard as any); fileData.append('pancardfile', documents.panCard as any);
fileData.append('regfile', documents.registrationCertificate as any); fileData.append('regfile', documents.registrationCertificate as any);
console.log('File Data for registration',fileData);
const res = await contractorStageTwoSubmit(fileData); const res = await contractorStageTwoSubmit(fileData);
console.log('Register contractor Response : ', res); console.log('Stage Two Res : ', res)
if (res.statusCode === 200) {
if (res.status === 200) {
if (res) { if (res) {
showToast('Form submitted successfully!'); showToast('Form submitted successfully!');
console.log('response?.data?.contOtherId : ', res?.data?.contOtherId);
const orgType = selectedOrg; const orgType = selectedOrg;
const params = { otherId: res?.data?.contOtherId, orgType: orgType }; const params = { otherId: res?.data?.contOtherId, orgType: orgType };
console.log("params : ", params)
if ([4, 5, 6].includes(orgType)) { if ([4, 5, 6].includes(orgType)) {
navigation.replace('StageThree' as never, params as never); navigation.replace(NavigationString.StageThreeScreen as never, params as never);
} else if (orgType === 7) { } else if (orgType === 7) {
console.log('org type else if : ', orgType); navigation.replace(NavigationString.Partnership as never, { otherId: res?.data?.contOtherId } as never);
navigation.replace('PartnerShip' as never, { otherId: res?.data?.contOtherId } as never);
} else { } else {
console.log('org type else : ', orgType); navigation.replace(NavigationString.Home as never);
navigation.replace('Dashboard' as never);
} }
} else { } else {
showToast('Form submission failed'); showToast('Form submission failed');
} }
} else { } else {
showToast(res.message || 'Form submission failed. Please try again.'); showToast(res.message || 'Form submission failed. Please try again.');
} }
} catch (error: any) { } catch (error: any) {
console.log("Error in form submission")
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.log('11111', error.response?.status);
console.log('22222', error.response?.data);
console.log('3333333', error.response?.headers);
} else { } else {
console.error('Unexpected error', error); console.error('Unexpected error', error);
} }
@@ -383,27 +347,10 @@ useEffect(() => {
} }
}; };
// const fetchAllMSME = async () => {
// try {
// console.log('Fetching MSME');
// const msmeOption = await getAllMsMeOrg();
// console.log('msme options fetched:', msmeOption);
// setAllMSME(msmeOption);
// } catch (error) {
// setAllMSME([]);
// console.error('Error in city fetch:', error);
// }
// };
const fetchCity = async (stateId: number) => { const fetchCity = async (stateId: number) => {
cityDropdownMutate(stateId, { cityDropdownMutate(stateId, {
onSuccess: (data) => { onSuccess: (data) => {
console.log('City options fetched:', data);
// const cityList = data.data || [];
// const mappedCity = cityList.map((item:any)=> ({
// label: item.name,
// value: item.id,
// }));
setCities(data); setCities(data);
}, },
onError: (error) => { onError: (error) => {
@@ -411,89 +358,10 @@ useEffect(() => {
console.error('Error in city fetch:', error); console.error('Error in city fetch:', error);
} }
}); });
// try {
// console.log('Fetching cities for state ID:', stateId);
// console.log('City options fetched:', cityOptions);
// setCities(cityOptions);
// } catch (error) {
// setCities([]);
// console.error('Error in city fetch:', error);
// }
}; };
// const contactorClassHandler = async () => {
// try {
// setloader(true);
// const mappedItems = await getClassDropdown();
// console.log(mappedItems, 'Contractor class items');
// setContractorClassItems(mappedItems);
// } catch (error) {
// console.log('Contractor class fetch error:', error);
// } finally {
// setloader(false);
// }
// };
// const contactorORG = async () => {
// try {
// setloader(true);
// const mappedItems = await getOrgDropdown();
// setAllOrganizations(mappedItems);
// } catch (error) {
// console.log('Contractor ORG fetch error:', error);
// } finally {
// setloader(false);
// }
// };
// const contactorRegistration = async () => {
// try {
// setloader(true);
// const mappedItems = await getMsmeDropdown();
// setAllMsmeOrgs(mappedItems);
// } catch (error) {
// console.log('Contractor ORG fetch error:', error);
// } finally {
// setloader(false);
// }
// };
console.log('formdata : ', formData); console.log('formdata : ', formData);
// const contactorSocial = async () => {
// try {
// setloader(true);
// const mappedItems = await getSocialDropdown();
// setAllSocialCategories(mappedItems);
// } catch (error) {
// console.log('Contractor ORG fetch error:', error);
// } finally {
// setloader(false);
// }
// };
// const allState = async () => {
// try {
// setloader(true);
// const mappedItems = await getStateDropdown();
// setstates(mappedItems);
// } catch (error) {
// console.log('all states error:', error);
// } finally {
// setloader(false);
// }
// };
// useEffect(() => {
// contactorClassHandler();
// contactorORG();
// contactorRegistration();
// contactorSocial();
// allState();
// fetchAllMSME();
// }, []);
useEffect(() => { useEffect(() => {
if (formData?.state) { if (formData?.state) {
fetchCity(Number(formData.state)); fetchCity(Number(formData.state));
@@ -609,15 +477,31 @@ useEffect(() => {
} }
}; };
// ── Stage Titles ──
const stageTitles = [
'Personal & Contact',
'Registration Details',
'KYC Verification',
'Address Details',
'Upload Documents',
];
return ( return (
<View style={{ flex: 1, backgroundColor: colors.white }}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -30}
>
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
bounces={false}
showsVerticalScrollIndicator={false}
> >
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled" bounces={false}>
<View style={{ paddingBottom: 20, marginTop: insets.top, flex: 1 }}> <View style={{ paddingBottom: 20, marginTop: insets.top, flex: 1 }}>
{/* ═══ Back Button ═══ */}
<View style={styles.headerRow}>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
if (stage > 0) { if (stage > 0) {
@@ -625,12 +509,39 @@ useEffect(() => {
} }
}} }}
style={styles.backBtn} style={styles.backBtn}
activeOpacity={0.7}
> >
{/* Replaced AntDesign with Lucide ArrowLeft */} <ArrowLeft size={22} color={colors.text} />
<ArrowLeft size={25} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
Registration
</AppText>
<View style={{ width: 36 }} />
</View>
{/* ═══ Progress Bar ═══ */}
<StepProgressBar currentStep={stage} steps={steps} /> <StepProgressBar currentStep={stage} steps={steps} />
{/* ═══ Stage Title ═══
<Animated.View
key={`title-${stage}`}
entering={FadeInUp.duration(300)}
style={styles.stageTitleContainer}
>
<AppText variant="h3" color={colors.text} style={styles.stageTitle}>
{stageTitles[stage]}
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Step {stage + 1} of {steps.length}
</AppText>
</Animated.View> */}
{/* ═══ Stage Content ═══ */}
<Animated.View
key={`stage-${stage}`}
entering={FadeInUp.duration(350).delay(100)}
style={[styles.stageCard, shadows.medium]}
>
{stage === 0 && ( {stage === 0 && (
<StageZero <StageZero
formData={formData} formData={formData}
@@ -656,7 +567,7 @@ useEffect(() => {
allMsmeOrgs={allMsmeOrgs} allMsmeOrgs={allMsmeOrgs}
allSocialCategories={allSocialCategories} allSocialCategories={allSocialCategories}
allMSME={allMSME} allMSME={allMSME}
dropdownStyle={dropdownStyle} dropdownStyle={{}}
setSelectedOrg={setSelectedOrg} setSelectedOrg={setSelectedOrg}
selectedOrg={selectedOrg} selectedOrg={selectedOrg}
/> />
@@ -701,8 +612,9 @@ useEffect(() => {
states={states} states={states}
cities={cities} cities={cities}
fetchCity={() => { fetchCity={() => {
fetchCity(Number(formData.state))}} fetchCity(Number(formData.state));
dropdownStyle={dropdownStyle} }}
dropdownStyle={{}}
styles={styles} styles={styles}
/> />
)} )}
@@ -716,10 +628,14 @@ useEffect(() => {
selectedOrg={selectedOrg} selectedOrg={selectedOrg}
/> />
)} )}
</Animated.View>
</View> </View>
{/* ═══ Next / Submit Button ═══ */}
<Animated.View entering={FadeInDown.duration(300).delay(200)}>
<PrimaryButton <PrimaryButton
title={stage === 4 ? (loading ? 'Loading...' : 'Submit') : 'Next'} title={stage === 4 ? (loading ? 'Loading...' : 'Submit') : 'Next'}
style={{ backgroundColor: colors.primary }}
onPress={() => { onPress={() => {
if (validateStage(stage)) { if (validateStage(stage)) {
switch (stage) { switch (stage) {
@@ -764,6 +680,7 @@ useEffect(() => {
} }
}} }}
/> />
</Animated.View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</View> </View>
@@ -772,96 +689,96 @@ useEffect(() => {
export default Registration; export default Registration;
const styles = StyleSheet.create({ // ── Styles ────────────────────────────────────────────────────────────
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
scrollContainer: { scrollContainer: {
flexGrow: 1, flexGrow: 1,
paddingHorizontal:14 paddingHorizontal: spacing.m,
}, },
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: spacing.s,
},
backBtn: {
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: colors.surface,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
},
stageTitleContainer: {
marginBottom: spacing.m,
paddingHorizontal: spacing.xs,
},
stageTitle: {
fontWeight: '800',
marginBottom: 2,
},
stageCard: {
backgroundColor: colors.surface,
borderRadius: 16,
padding: spacing.m,
},
// These styles will be passed to child stages that still use `styles` prop
label: { label: {
fontSize: 15, fontSize: typography.size.s,
fontWeight: '600', fontWeight: '700',
marginVertical: 10, color: colors.primary,
color: colors.black, marginBottom: spacing.s,
letterSpacing: 0.3,
textTransform: 'uppercase',
}, },
row: { row: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: 10, marginBottom: 10,
}, },
backBtn: {
padding: 5,
width: 50,
marginBottom: 5,
},
sectionTitle: { sectionTitle: {
fontSize: 24, fontSize: typography.size.l,
fontWeight: '700', fontWeight: '700',
color: colors.black, color: colors.text,
marginBottom: 20, marginBottom: spacing.m,
marginTop: 10,
textAlign: 'center', textAlign: 'center',
}, },
uploadGrid: { uploadGrid: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'space-between', justifyContent: 'space-between',
gap: spacing.s,
}, },
uploadCard: { uploadCard: {
width: '48%', width: '48%',
backgroundColor: '#fdfdfd', backgroundColor: colors.surface,
borderRadius: 20, borderRadius: 16,
padding: 20, padding: spacing.m,
alignItems: 'center', alignItems: 'center',
marginBottom: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 6,
elevation: 4,
borderWidth: 1, borderWidth: 1,
borderColor: '#ddd', borderColor: colors.border,
}, },
iconCircle: { iconCircle: {
width: 50, width: 46,
height: 50, height: 46,
borderRadius: 25, borderRadius: 23,
backgroundColor: colors.primary, backgroundColor: colors.primary,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginBottom: 10, marginBottom: spacing.s,
}, },
cardTitle: { cardTitle: {
fontSize: 15, fontSize: typography.size.s,
fontWeight: '600', fontWeight: '600',
color: colors.black, color: colors.text,
marginBottom: 4, marginBottom: 4,
textAlign: 'center', textAlign: 'center',
}, },
cardHint: { cardHint: {
fontSize: 12, fontSize: typography.size.xs,
color: '#888', color: colors.textSecondary,
textAlign: 'center', textAlign: 'center',
}, },
dropdown: {
height: 55,
borderColor: '#e0e0e0',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 14,
marginTop: 8,
marginBottom: 14,
backgroundColor: '#fff',
},
selectedTextStyle: {
fontSize: 14,
color: colors.black,
},
placeholderStyle: {
fontSize: 14,
color: colors.black,
},
itemTextStyle: {
fontSize: 14,
color: colors.black,
},
}); });
+112 -73
View File
@@ -3,23 +3,21 @@ import {
SafeAreaView, SafeAreaView,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text,
TouchableOpacity, TouchableOpacity,
View, View,
Platform, Platform,
} from 'react-native'; } from 'react-native';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton'; import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import MyInput from '../../Component/MyInput'; import MyInput from '../../components/MyInput';
import { showToast } from '../../Constant/ShowDailog'; import { showToast } from '../../Constant/ShowDailog';
import { STAGETHREE } from '../../Services/BBPS/ApiUrls'; import { useContractorStageThreeSubmit } from '../../Services/ApiCall/Hooks';
import { useContractorStageThreeSubmit } from '../../Services/BBPS/Hooks'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Trash2, Building2 } from 'lucide-react-native';
type Company = { type Company = {
director: string; director: string;
@@ -31,12 +29,15 @@ type Company = {
type StageThreeRouteParams = { type StageThreeRouteParams = {
orgType: string; orgType: string;
otherId: string | number otherId: string | number;
}; };
const StageThreeScreen = () => { const StageThreeScreen = () => {
const route = useRoute<RouteProp<{ params: StageThreeRouteParams }, 'params'>>(); const route = useRoute<RouteProp<{ params: StageThreeRouteParams }, 'params'>>();
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const { colors, spacing, typography, shadows } = useTheme();
const insets = useSafeAreaInsets();
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
const [company, setCompany] = useState<Company[]>([ const [company, setCompany] = useState<Company[]>([
{ {
@@ -71,7 +72,6 @@ const StageThreeScreen = () => {
]); ]);
}; };
console.log("first company route : ", route);
const handleSubmit = async () => { const handleSubmit = async () => {
setLoading(true); setLoading(true);
try { try {
@@ -118,140 +118,179 @@ const StageThreeScreen = () => {
); );
}; };
const orgLabel =
route.params?.orgType == '4'
? 'Public'
: route.params?.orgType == '5'
? 'Private'
: route.params?.orgType == '6'
? 'LLP'
: route.params?.orgType;
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<Text style={styles.heading}>Stage Three For {route.params?.orgType == '4' ? 'Public' : route.params?.orgType == '5' ? 'Private' : route.params?.orgType == '6' ? 'Limited Liability Partnership (LLP)' : route.params?.orgType} Organization</Text> <View style={[styles.header, { paddingTop: insets.top + spacing.s }]}>
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
{orgLabel} Organization
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Add directors / shareholders for your organization
</AppText>
</View>
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40} keyboardVerticalOffset={40}
> >
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled"> <ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{company.map((partner, index) => ( {company.map((partner, index) => (
<View style={{ marginBottom: 20 }} key={index}> <Animated.View
<Text style={styles.partnerTitle}>Company {index + 1}</Text> key={index}
entering={FadeInUp.delay(index * 80).springify()}
style={[styles.card, shadows.medium]}
>
<View style={styles.cardHeader}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
Director {index + 1}
</AppText>
{company.length > 1 && (
<TouchableOpacity onPress={() => handleRemove(index)} activeOpacity={0.7}>
<Trash2 size={18} color={colors.error} />
</TouchableOpacity>
)}
</View>
<MyInput <MyInput
icon="user" label="Director Name"
placeholder="Name of Director *" placeholder="Enter Name of Director"
value={partner.director} value={partner.director}
onChangeText={(text: any) => handleChange(index, 'director', text)} onChangeText={(text: any) => handleChange(index, 'director', text)}
onFocus={() => setFocusedField(`director-${index}`)} onFocus={() => setFocusedField(`director-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `director-${index}`} focused={focusedField === `director-${index}`}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="id-card" label="DIN Number"
placeholder="DIN Number in Firm *" placeholder="Enter DIN Number"
value={partner.DINNumber} value={partner.DINNumber}
onChangeText={(text: any) => handleChange(index, 'DINNumber', text)} onChangeText={(text: any) => handleChange(index, 'DINNumber', text)}
onFocus={() => setFocusedField(`DINNumber-${index}`)} onFocus={() => setFocusedField(`DINNumber-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `DINNumber-${index}`} focused={focusedField === `DINNumber-${index}`}
keyboardType="numeric" keyboardType="numeric"
maxlength={8} maxLength={8}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="id-card" label="PAN of Director"
placeholder="PAN of Director *" placeholder="Enter PAN Number"
value={partner.panNumber} value={partner.panNumber}
onChangeText={(text: any) => handleChange(index, 'panNumber', text)} onChangeText={(text: any) => handleChange(index, 'panNumber', text)}
onFocus={() => setFocusedField(`panNumber-${index}`)} onFocus={() => setFocusedField(`panNumber-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `panNumber-${index}`} focused={focusedField === `panNumber-${index}`}
maxlength={12} maxLength={12}
autoCapitalize="characters" autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="credit-card" label="Aadhaar Number"
placeholder="Aadhar Number *" placeholder="Enter 12-digit Aadhaar"
value={partner.aadharNumber} value={partner.aadharNumber}
onChangeText={(text: any) => handleChange(index, 'aadharNumber', text)} onChangeText={(text: any) => handleChange(index, 'aadharNumber', text)}
onFocus={() => setFocusedField(`aadharNumber-${index}`)} onFocus={() => setFocusedField(`aadharNumber-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `aadharNumber-${index}`} focused={focusedField === `aadharNumber-${index}`}
keyboardType="numeric" keyboardType="numeric"
maxlength={12} maxLength={12}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="phone" label="Mobile Number"
placeholder="Mobile Number *" placeholder="Enter Mobile Number"
value={partner.mobileNumber} value={partner.mobileNumber}
onChangeText={(text: any) => handleChange(index, 'mobileNumber', text)} onChangeText={(text: any) => handleChange(index, 'mobileNumber', text)}
onFocus={() => setFocusedField(`mobileNumber-${index}`)} onFocus={() => setFocusedField(`mobileNumber-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `mobileNumber-${index}`} focused={focusedField === `mobileNumber-${index}`}
keyboardType="phone-pad" keyboardType="phone-pad"
maxlength={10} maxLength={10}
/> />
</Animated.View>
{company.length > 1 && (
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
<Text style={styles.deleteText}>Delete Company</Text>
</TouchableOpacity>
)}
</View>
))} ))}
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}> <TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
<Text style={styles.addText}>+ Add Company</Text> <Building2 size={16} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
Add Director
</AppText>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>
<View style={styles.footer}>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} /> <PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
</View> </View>
</View>
); );
}; };
export default StageThreeScreen; export default StageThreeScreen;
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
heading: { header: {
fontSize: 18, paddingHorizontal: spacing.m,
fontWeight: 'bold', paddingBottom: spacing.m,
marginBottom: 10, borderBottomWidth: 1,
marginTop: 10, borderBottomColor: colors.border,
textAlign: 'center', backgroundColor: colors.surface,
},
partnerTitle: {
fontWeight: '600',
fontSize: 16,
marginBottom: 10,
}, },
scrollContainer: { scrollContainer: {
// backgroundColor: color.background,
flexGrow: 1, flexGrow: 1,
padding: spacing.m,
paddingBottom: 30,
},
card: {
backgroundColor: colors.surface,
borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
paddingBottom: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
}, },
addButton: { addButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
padding: 10, flexDirection: 'row',
marginTop: 10,
borderRadius: 10,
alignItems: 'center', alignItems: 'center',
width: 140, justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 12,
alignSelf: 'flex-end', alignSelf: 'flex-end',
}, },
addText: { footer: {
color: 'white', padding: spacing.m,
fontWeight: '600', borderTopWidth: 1,
}, borderTopColor: colors.border,
deleteButton: { backgroundColor: colors.surface,
backgroundColor: '#FF5C5C',
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 140,
alignSelf: 'flex-end',
},
deleteText: {
color: 'white',
fontWeight: '600',
}, },
}); });
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native'; import { View, TouchableOpacity } from 'react-native';
import { import {
IdCard, IdCard,
FileText, FileText,
@@ -7,7 +7,12 @@ import {
ShieldCheck, ShieldCheck,
Building2, Building2,
ScrollText, ScrollText,
} from 'lucide-react-native'; // lucide icons CheckCircle,
Upload,
} from 'lucide-react-native';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface DocumentItem { interface DocumentItem {
key: string; key: string;
@@ -30,13 +35,15 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
formData, formData,
selectedOrg, selectedOrg,
}) => { }) => {
const { colors, spacing, shadows } = useTheme();
const documentItems: DocumentItem[] = [ const documentItems: DocumentItem[] = [
{ key: 'panCard', label: 'PAN Card', icon: 'pan' }, { key: 'panCard', label: 'PAN Card', icon: 'pan' },
{ key: 'aadharCard', label: 'Aadhar Card', icon: 'aadhar' }, { key: 'aadharCard', label: 'Aadhar Card', icon: 'aadhar' },
{ key: 'gstnCertificate', label: 'GSTN Certificate', icon: 'gst' }, { key: 'gstnCertificate', label: 'GSTN Certificate', icon: 'gst' },
{ key: 'constractorClass', label: 'Contractor Class File', icon: 'contractor' }, { key: 'constractorClass', label: 'Contractor Class', icon: 'contractor' },
{ key: 'msmefile', label: 'MSME Organization', icon: 'msme' }, { key: 'msmefile', label: 'MSME Organization', icon: 'msme' },
{ key: 'registrationCertificate', label: 'Registration Certificate', icon: 'registration' }, { key: 'registrationCertificate', label: 'Registration Cert.', icon: 'registration' },
]; ];
const shouldRenderItem = (itemKey: string): boolean => { const shouldRenderItem = (itemKey: string): boolean => {
@@ -52,54 +59,84 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
return false; return false;
} }
return true; // pan + aadhar + rest (unless filtered above) return true;
}; };
const renderIcon = (iconType: DocumentItem['icon']) => { const renderIcon = (iconType: DocumentItem['icon']) => {
const commonProps = { size: 26, color: '#fff', strokeWidth: 2 }; const commonProps = { size: 24, color: colors.buttonText, strokeWidth: 2 };
switch (iconType) { switch (iconType) {
case 'pan': case 'pan':
return <IdCard {...commonProps} />; // PAN → ID card feel [web:19] return <IdCard {...commonProps} />;
case 'aadhar': case 'aadhar':
return <IdCard {...commonProps} />; // Same icon, different label return <IdCard {...commonProps} />;
case 'gst': case 'gst':
return <FileBadge {...commonProps} />; // File with badge [web:20] return <FileBadge {...commonProps} />;
case 'contractor': case 'contractor':
return <ShieldCheck {...commonProps} />; // Safety / verification feel return <ShieldCheck {...commonProps} />;
case 'msme': case 'msme':
return <Building2 {...commonProps} />; // Organization / company return <Building2 {...commonProps} />;
case 'registration': case 'registration':
return <ScrollText {...commonProps} />; // Certificate / document scroll return <ScrollText {...commonProps} />;
default: default:
return <FileText {...commonProps} />; return <FileText {...commonProps} />;
} }
}; };
let animIndex = 0;
return ( return (
<> <>
<Text style={styles.sectionTitle}>Upload Required Documents</Text> <AppText variant="h3" color={colors.text} style={{ fontWeight: '700', textAlign: 'center', marginBottom: spacing.m }}>
Upload Documents
</AppText>
<View style={styles.uploadGrid}> <View style={{
flexDirection: 'row',
flexWrap: 'wrap',
}}>
{documentItems.map(item => { {documentItems.map(item => {
if (!shouldRenderItem(item.key)) return null; if (!shouldRenderItem(item.key)) return null;
const isUploaded = !!documents[item.key];
const currentIndex = animIndex++;
return ( return (
<TouchableOpacity <Animated.View
key={item.key} key={item.key}
style={styles.uploadCard} entering={FadeInUp.delay(80 * currentIndex).springify()}
onPress={() => handleDocumentPick(item.key)} style={{ width: '50%', padding: spacing.xs }}
> >
<View style={styles.iconCircle}>{renderIcon(item.icon)}</View> <TouchableOpacity
style={[
styles.uploadCard,
{ width: '100%' },
{
borderColor: isUploaded ? colors.primary : colors.border,
backgroundColor: isUploaded ? `${colors.primary}08` : colors.surface,
},
]}
onPress={() => handleDocumentPick(item.key)}
activeOpacity={0.7}
>
<View style={[styles.iconCircle, isUploaded && { backgroundColor: colors.primary }]}>
{isUploaded ? (
<CheckCircle size={24} color={colors.buttonText} strokeWidth={2.5} />
) : (
renderIcon(item.icon)
)}
</View>
<Text style={styles.cardTitle}>{item.label}</Text> <AppText variant="bodySmall" color={colors.text} style={{ fontWeight: '600', textAlign: 'center', marginBottom: 4 }}>
{item.label}
</AppText>
<Text style={styles.cardHint}> <AppText variant="caption" color={isUploaded ? colors.primary : colors.textSecondary} style={{ textAlign: 'center', fontSize: 10 }}>
{documents[item.key] {isUploaded
? `${documents[item.key]?.name} ✅` ? `${documents[item.key]?.name?.slice(0, 18)}...`
: 'Tap to upload PDF'} : 'Tap to upload PDF'}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
</Animated.View>
); );
})} })}
</View> </View>
@@ -1,11 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Text, View, StyleSheet } from 'react-native'; import { View, TouchableOpacity } from 'react-native';
import DatePicker from 'react-native-date-picker';
import DatePicker from 'react-native-date-picker' import MyInput from '../../../components/MyInput';
import MyInput from '../../../Component/MyInput';
import MyDropdown from '../../../Component/Dropdown/MyDropdown'; import MyDropdown from '../../../Component/Dropdown/MyDropdown';
import colors from '../../../Constant/Color';
import { DropdownOption } from '../types/types'; import { DropdownOption } from '../types/types';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props { interface Props {
formData: { formData: {
@@ -14,7 +14,7 @@ interface Props {
orgType: string; orgType: string;
registrationDetail: string | number; registrationDetail: string | number;
socialCategory: string; socialCategory: string;
expiryDate: string expiryDate: string;
}; };
handleChange: (key: string, value: string) => void; handleChange: (key: string, value: string) => void;
focusedField: string | null; focusedField: string | null;
@@ -26,7 +26,7 @@ interface Props {
allMSME: DropdownOption[]; allMSME: DropdownOption[];
dropdownStyle: object; dropdownStyle: object;
setSelectedOrg: (val: number) => void; setSelectedOrg: (val: number) => void;
selectedOrg: string | number selectedOrg: string | number;
} }
const StageOne: React.FC<Props> = ({ const StageOne: React.FC<Props> = ({
@@ -40,22 +40,27 @@ const StageOne: React.FC<Props> = ({
allSocialCategories, allSocialCategories,
dropdownStyle, dropdownStyle,
setSelectedOrg, setSelectedOrg,
selectedOrg selectedOrg,
}) => { }) => {
const { colors, spacing } = useTheme();
const [selectedAllMSME, setSelectedAllMSME] = useState() const [selectedAllMSME, setSelectedAllMSME] = useState();
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false);
const [date, setDate] = useState(new Date()) const [date, setDate] = useState(new Date());
const handleDateConfirm = (selectedDate: Date) => { const handleDateConfirm = (selectedDate: Date) => {
setOpen(false); setOpen(false);
setDate(selectedDate); setDate(selectedDate);
handleChange('expiryDate', selectedDate.toISOString().split('T')[0]); // Format as YYYY-MM-DD const day = String(selectedDate.getDate()).padStart(2, '0');
const month = String(selectedDate.getMonth() + 1).padStart(2, '0');
const year = selectedDate.getFullYear();
handleChange('expiryDate', `${day}/${month}/${year}`);
}; };
return ( return (
<View> <View>
<Text style={styles.label}>Registration & Type Details</Text> <AppText variant="caption" color={colors.primary} style={{ fontWeight: '700', letterSpacing: 0.3, textTransform: 'uppercase', marginBottom: spacing.s }}>
Registration & Type Details
</AppText>
<MyDropdown <MyDropdown
label="Type of Organization" label="Type of Organization"
@@ -68,24 +73,26 @@ const StageOne: React.FC<Props> = ({
setSelectedOrg(item.value); setSelectedOrg(item.value);
handleChange('orgType', item.value); handleChange('orgType', item.value);
}} }}
style={dropdownStyle}
onFocus={() => setFocusedField('orgType')} onFocus={() => setFocusedField('orgType')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'orgType'} focusedField={focusedField === 'orgType'}
/> />
{
selectedOrg !== 8 && {selectedOrg !== 8 && (
<MyInput <MyInput
icon="file" label="Registration Number"
placeholder="Registration Number *" placeholder="Enter Registration Number"
value={formData.registrationNumber} value={formData.registrationNumber}
onChangeText={text => handleChange('registrationNumber', text)} onChangeText={text => handleChange('registrationNumber', text)}
onFocus={() => setFocusedField('registrationNumber')} onFocus={() => setFocusedField('registrationNumber')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'registrationNumber'} focused={focusedField === 'registrationNumber'}
autoCapitalize='characters' /> autoCapitalize="characters"
} containerStyle={{ marginBottom: spacing.s }}
{selectedOrg && />
)}
{selectedOrg && (
<MyDropdown <MyDropdown
label="Contractor Class" label="Contractor Class"
data={contractorClassItems} data={contractorClassItems}
@@ -93,7 +100,6 @@ const StageOne: React.FC<Props> = ({
valueField="value" valueField="value"
placeholder="Select Class" placeholder="Select Class"
value={selectedOrg == 8 ? contractorClassItems[0]?.value : formData.contractorClass} value={selectedOrg == 8 ? contractorClassItems[0]?.value : formData.contractorClass}
style={dropdownStyle}
disable={selectedOrg == 8 ? true : false} disable={selectedOrg == 8 ? true : false}
onChange={item => { onChange={item => {
handleChange('contractorClass', item.value); handleChange('contractorClass', item.value);
@@ -101,9 +107,10 @@ const StageOne: React.FC<Props> = ({
onFocus={() => setFocusedField('contractorClass')} onFocus={() => setFocusedField('contractorClass')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'contractorClass'} focusedField={focusedField === 'contractorClass'}
/>} />
)}
{selectedOrg !== 8 && {selectedOrg !== 8 && (
<MyDropdown <MyDropdown
label="Registration Detail (MSME/NSIC etc.)" label="Registration Detail (MSME/NSIC etc.)"
data={allMsmeOrgs} data={allMsmeOrgs}
@@ -112,32 +119,15 @@ const StageOne: React.FC<Props> = ({
placeholder="Select Registration Type" placeholder="Select Registration Type"
value={formData.registrationDetail} value={formData.registrationDetail}
onChange={item => { onChange={item => {
setSelectedAllMSME(item.value) setSelectedAllMSME(item.value);
console.log('first', item) handleChange('registrationDetail', item.value);
handleChange('registrationDetail', item.value)
}} }}
style={dropdownStyle}
onFocus={() => setFocusedField('registrationDetail')} onFocus={() => setFocusedField('registrationDetail')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'registrationDetail'} focusedField={focusedField === 'registrationDetail'}
/>} />
{/* <MyDropdown )}
label="MSME Details"
data={allMSME}
labelField="label"
valueField="value"
placeholder="Select MSME Type"
value={''}
onChange={item => {
setSelectedAllMSME(item.value)
handleChange('allMsme', item.value)
}}
style={dropdownStyle}
onFocus={() => setFocusedField('allMsme')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'allMsme'}
/> */}
<MyDropdown <MyDropdown
label="Social Category" label="Social Category"
data={allSocialCategories} data={allSocialCategories}
@@ -146,50 +136,39 @@ const StageOne: React.FC<Props> = ({
placeholder="Select Social Category" placeholder="Select Social Category"
value={formData.socialCategory} value={formData.socialCategory}
onChange={item => handleChange('socialCategory', item.value)} onChange={item => handleChange('socialCategory', item.value)}
style={dropdownStyle}
onFocus={() => setFocusedField('socialCategory')} onFocus={() => setFocusedField('socialCategory')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'socialCategory'} focusedField={focusedField === 'socialCategory'}
/> />
{
selectedAllMSME !== 3 && selectedOrg !== 8 &&
<>
{selectedAllMSME !== 3 && selectedOrg !== 8 && (
<>
<TouchableOpacity onPress={() => setOpen(true)} activeOpacity={0.8}>
<View pointerEvents="none">
<MyInput <MyInput
icon="calendar-alt" label="Expiry Date"
placeholder="Expiry Date *" placeholder="Select Expiry Date"
value={formData.expiryDate} value={formData.expiryDate}
onChangeText={text => handleChange('expiryDate', text)} onChangeText={text => handleChange('expiryDate', text)}
onFocus={() => setFocusedField('expiryDate')} onFocus={() => setFocusedField('expiryDate')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'expiryDate'} focused={open || focusedField === 'expiryDate'}
rightIcon='wallet' editable={false}
editable={true}
rightIconPress={() => setOpen(true)}
/> />
</View>
</TouchableOpacity>
<DatePicker <DatePicker
modal modal
open={open} open={open}
date={date} date={date}
onConfirm={handleDateConfirm} onConfirm={handleDateConfirm}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
mode="date" // Ensure you're using date mode mode="date"
/> />
</> </>
} )}
</View> </View>
); );
}; };
const styles = StyleSheet.create({
label: {
fontSize: 16,
fontWeight: '600',
color: colors.black,
marginBottom: 10,
},
});
export default StageOne; export default StageOne;
@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { Text } from 'react-native'; import { View } from 'react-native';
import { DropdownOption } from '../types/types'; import { DropdownOption } from '../types/types';
import MyInput from '../../../Component/MyInput'; import MyInput from '../../../components/MyInput';
import MyDropdown from '../../../Component/Dropdown/MyDropdown'; import MyDropdown from '../../../Component/Dropdown/MyDropdown';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface AddressDetailsSectionProps { interface AddressDetailsSectionProps {
formData: { formData: {
@@ -21,7 +21,7 @@ interface AddressDetailsSectionProps {
cities: DropdownOption[]; cities: DropdownOption[];
fetchCity: (stateValue: number) => void; fetchCity: (stateValue: number) => void;
dropdownStyle?: object; dropdownStyle?: object;
styles?: any; // Optional styles prop for custom styling styles?: any;
} }
const StageThree: React.FC<AddressDetailsSectionProps> = ({ const StageThree: React.FC<AddressDetailsSectionProps> = ({
@@ -32,54 +32,51 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
states, states,
cities, cities,
fetchCity, fetchCity,
styles styles,
}) => { }) => {
const { colors, spacing } = useTheme();
const dropdownStyle = {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: '#fff',
marginBottom: 10,
};
return ( return (
<> <>
<Text style={styles.label}>Address Details</Text> <AppText variant="caption" color={colors.primary} style={styles.label}>
Address Details
</AppText>
<MyInput <MyInput
icon="home" label="Address"
placeholder="Enter Address *" placeholder="Enter Full Address"
value={formData.address} value={formData.address}
onChangeText={text => handleChange('address', text)} onChangeText={text => handleChange('address', text)}
onFocus={() => setFocusedField('address')} onFocus={() => setFocusedField('address')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'address'} focused={focusedField === 'address'}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
maxlength={10} label="Contact Number"
icon="phone" maxLength={10}
placeholder="Contact Number *" placeholder="Enter Contact Number"
value={formData.agentContact2} value={formData.agentContact2}
onChangeText={text => handleChange('agentContact2', text)} onChangeText={text => handleChange('agentContact2', text)}
onFocus={() => setFocusedField('agentContact2')} onFocus={() => setFocusedField('agentContact2')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'agentContact2'} focused={focusedField === 'agentContact2'}
keyboardType="numeric" keyboardType="numeric"
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="lock" label="Pin Code"
placeholder="Pin Code *" placeholder="Enter 6-digit Pin Code"
keyboardType="numeric" keyboardType="numeric"
maxlength={6} maxLength={6}
value={formData.pinCode} value={formData.pinCode}
onChangeText={text => handleChange('pinCode', text)} onChangeText={text => handleChange('pinCode', text)}
onFocus={() => setFocusedField('pinCode')} onFocus={() => setFocusedField('pinCode')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'pinCode'} focused={focusedField === 'pinCode'}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyDropdown <MyDropdown
@@ -94,7 +91,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
handleChange('city', ''); handleChange('city', '');
fetchCity(item.value); fetchCity(item.value);
}} }}
style={dropdownStyle}
onFocus={() => setFocusedField('state')} onFocus={() => setFocusedField('state')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'state'} focusedField={focusedField === 'state'}
@@ -108,7 +104,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
placeholder="Select City" placeholder="Select City"
value={formData.city} value={formData.city}
onChange={item => handleChange('city', item.value)} onChange={item => handleChange('city', item.value)}
style={dropdownStyle}
onFocus={() => setFocusedField('city')} onFocus={() => setFocusedField('city')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'city'} focusedField={focusedField === 'city'}
@@ -1,13 +1,14 @@
import React from 'react'; import React, { useState } from 'react';
import DatePicker from 'react-native-date-picker';
import { import {
Text,
TouchableOpacity, TouchableOpacity,
ActivityIndicator, ActivityIndicator,
View, View,
Alert,
} from 'react-native'; } from 'react-native';
import MyInput from '../../../Component/MyInput'; import MyInput from '../../../components/MyInput';
import { useTheme } from '../../../Hooks/useTheme';
// Adjust the path to your custom input import AppText from '../../../components/AppText';
interface Props { interface Props {
formData: { formData: {
@@ -21,12 +22,10 @@ interface Props {
handleChange: (key: string, value: string) => void; handleChange: (key: string, value: string) => void;
focusedField: string | null; focusedField: string | null;
setFocusedField: (field: string | null | any) => void; setFocusedField: (field: string | null | any) => void;
// PAN
panVerified: boolean; panVerified: boolean;
setPanVerified: (value: boolean) => void; setPanVerified: (value: boolean) => void;
verifyPanHandler: () => void; verifyPanHandler: () => void;
panVerifying: boolean; panVerifying: boolean;
// Aadhaar
aadhaarVerified: boolean; aadhaarVerified: boolean;
setAadhaarVerified: (value: boolean) => void; setAadhaarVerified: (value: boolean) => void;
sendAdharOTPHandler: (aadhaarNumber: string) => void; sendAdharOTPHandler: (aadhaarNumber: string) => void;
@@ -34,13 +33,12 @@ interface Props {
setadharotpsend: (value: boolean) => void; setadharotpsend: (value: boolean) => void;
verifyAdharOtpHandler: () => void; verifyAdharOtpHandler: () => void;
aadhaarVerifying: boolean; aadhaarVerifying: boolean;
// GST
verifyGSTHandler: () => void; verifyGSTHandler: () => void;
gstverifying: boolean; gstverifying: boolean;
color: any; color: any;
styles: any; styles: any;
formatDateInput: (text: string) => string; formatDateInput: (text: string) => string;
selectedOrg:string|number selectedOrg: string | number;
} }
const StageTwo: React.FC<Props> = ({ const StageTwo: React.FC<Props> = ({
@@ -64,61 +62,107 @@ const StageTwo: React.FC<Props> = ({
color, color,
styles, styles,
formatDateInput, formatDateInput,
selectedOrg selectedOrg,
}) => { }) => {
const { colors, spacing, shadows } = useTheme();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(new Date());
const handleDateConfirm = (selectedDate: Date) => {
setOpen(false);
setDate(selectedDate);
const day = String(selectedDate.getDate()).padStart(2, '0');
const month = String(selectedDate.getMonth() + 1).padStart(2, '0');
const year = selectedDate.getFullYear();
setPanVerified(false);
handleChange('dateOfIncorp', `${day}/${month}/${year}`);
};
const verifyButtonStyle = {
backgroundColor: colors.primary,
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
};
return ( return (
<> <>
{/* ---------- PAN Details ---------- */} {/* ── PAN Details ── */}
<Text style={styles.label}>PAN Details</Text> <AppText variant="caption" color={colors.primary} style={styles.label}>
PAN Details
</AppText>
<MyInput <MyInput
icon="file" label={selectedOrg == 8 ? "PAN Number" : "Organization PAN Number"}
placeholder={selectedOrg == 8 ? "Pan Number *" : "Organization PAN Number *"} placeholder={selectedOrg == 8 ? "Enter PAN Number" : "Enter Organization PAN"}
value={formData.orgPanNumber} value={formData.orgPanNumber}
onChangeText={(text) => { onChangeText={(text) => {
setPanVerified(false); setPanVerified(false);
handleChange('orgPanNumber', text); handleChange('orgPanNumber', text);
}} }}
isVarify={panVerified} isVerify={panVerified}
onFocus={() => setFocusedField('pan')} onFocus={() => setFocusedField('pan')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'pan'} focused={focusedField === 'pan'}
editable={!panVerified} editable={!panVerified}
autoCapitalize="characters" autoCapitalize="characters"
maxlength={10} maxLength={10}
containerStyle={{ marginBottom: spacing.s }}
/> />
<TouchableOpacity
onPress={() => !panVerified && setOpen(true)}
activeOpacity={0.8}
disabled={panVerified}
>
<View pointerEvents="none">
<MyInput <MyInput
icon="calendar" label={selectedOrg == 8 ? "Date of Birth" : "Date of Incorporation"}
placeholder={selectedOrg == 8 ? "Date of Birth *" :"Date of Incorporation *"} placeholder="DD/MM/YYYY"
value={formData.dateOfIncorp} value={formData.dateOfIncorp}
onChangeText={(text) => { onChangeText={(text) => {
const formatted = formatDateInput(text); const formatted = formatDateInput(text);
setPanVerified(false); setPanVerified(false);
handleChange('dateOfIncorp', formatted); handleChange('dateOfIncorp', formatted);
}} }}
isVarify={panVerified} isVerify={panVerified}
onFocus={() => setFocusedField('dob')} onFocus={() => setFocusedField('dob')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'dob'} focused={open || focusedField === 'dob'}
editable={!panVerified} editable={false}
keyboardType='numeric' keyboardType="numeric"
containerStyle={{ marginBottom: spacing.s }}
/>
</View>
</TouchableOpacity>
<DatePicker
modal
open={open}
date={date}
onConfirm={handleDateConfirm}
onCancel={() => setOpen(false)}
mode="date"
/> />
<MyInput <MyInput
icon="file-signature" label="PAN Name"
placeholder="PAN Name *" placeholder="Enter Name as on PAN"
value={formData.orgPanName} value={formData.orgPanName}
onChangeText={(text) => { onChangeText={(text) => {
setPanVerified(false); setPanVerified(false);
handleChange('orgPanName', text); handleChange('orgPanName', text);
}} }}
isVarify={panVerified} isVerify={panVerified}
onFocus={() => setFocusedField('panName')} onFocus={() => setFocusedField('panName')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'panName'} focused={focusedField === 'panName'}
editable={!panVerified} editable={!panVerified}
autoCapitalize="characters" autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/> />
{formData.orgPanNumber && {formData.orgPanNumber &&
@@ -126,32 +170,69 @@ const StageTwo: React.FC<Props> = ({
formData.orgPanName && formData.orgPanName &&
!panVerified && ( !panVerified && (
<TouchableOpacity <TouchableOpacity
style={{ style={verifyButtonStyle}
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
disabled={panVerifying} disabled={panVerifying}
onPress={verifyPanHandler} onPress={verifyPanHandler}
activeOpacity={0.8}
> >
<Text style={{ color: 'white', fontWeight: '600' }}> <AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
{panVerifying ? 'Verifying' : 'Verify PAN'} {panVerifying ? 'Verifying...' : 'Verify PAN'}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
)} )}
{/* ---------- Aadhaar Details ---------- */} {panVerified && (
<Text style={styles.label}>Aadhaar Details</Text> <TouchableOpacity
style={{
borderWidth: 1.5,
borderColor: '#E67E22',
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
backgroundColor: 'transparent',
}}
onPress={() => {
Alert.alert(
'Change PAN',
'Are you sure you want to change your PAN details? You will need to re-verify.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Change',
style: 'destructive',
onPress: () => {
setPanVerified(false);
handleChange('orgPanNumber', '');
handleChange('dateOfIncorp', '');
handleChange('orgPanName', '');
},
},
],
);
}}
activeOpacity={0.8}
>
<AppText variant="bodySmall" color={'#E67E22'} style={{ fontWeight: '700' }}>
Change PAN
</AppText>
</TouchableOpacity>
)}
{/* ── Aadhaar Details ── */}
<View style={{ marginTop: spacing.m, borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.m }}>
<AppText variant="caption" color={colors.primary} style={styles.label}>
Aadhaar Details
</AppText>
<MyInput <MyInput
icon="address-card" label="Aadhaar Number"
placeholder="Aadhaar Number *" placeholder="Enter 12-digit Aadhaar Number"
keyboardType="numeric" keyboardType="numeric"
maxlength={12} maxLength={12}
value={formData.aadhaarNumber} value={formData.aadhaarNumber}
onChangeText={(text) => { onChangeText={(text) => {
handleChange('aadhaarNumber', text); handleChange('aadhaarNumber', text);
@@ -159,31 +240,27 @@ const StageTwo: React.FC<Props> = ({
setadharotpsend(false); setadharotpsend(false);
handleChange('aadhaarOtp', ''); handleChange('aadhaarOtp', '');
}} }}
isVarify={aadhaarVerified} isVerify={aadhaarVerified}
onFocus={() => setFocusedField('adhar')} onFocus={() => setFocusedField('adhar')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'adhar'} focused={focusedField === 'adhar'}
editable={!aadhaarVerified} editable={!aadhaarVerified}
containerStyle={{ marginBottom: spacing.s }}
/> />
{formData.aadhaarNumber.length === 12 && !adharotpsend && ( {formData.aadhaarNumber.length === 12 && !adharotpsend && (
<TouchableOpacity <TouchableOpacity
style={{ style={verifyButtonStyle}
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
onPress={() => sendAdharOTPHandler(formData.aadhaarNumber)} onPress={() => sendAdharOTPHandler(formData.aadhaarNumber)}
disabled={aadhaarVerifying} disabled={aadhaarVerifying}
activeOpacity={0.8}
> >
{aadhaarVerifying ? ( {aadhaarVerifying ? (
<ActivityIndicator color="#fff" /> <ActivityIndicator color={colors.buttonText} size="small" />
) : ( ) : (
<Text style={{ color: 'white', fontWeight: '600' }}>Send OTP</Text> <AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
Send OTP
</AppText>
)} )}
</TouchableOpacity> </TouchableOpacity>
)} )}
@@ -191,43 +268,78 @@ const StageTwo: React.FC<Props> = ({
{adharotpsend && !aadhaarVerified && ( {adharotpsend && !aadhaarVerified && (
<> <>
<MyInput <MyInput
icon="lock" label="OTP"
placeholder="Enter OTP *" placeholder="Enter 6-digit OTP"
keyboardType="numeric" keyboardType="numeric"
maxlength={6} maxLength={6}
value={formData.aadhaarOtp} value={formData.aadhaarOtp}
onChangeText={(text) => handleChange('aadhaarOtp', text)} onChangeText={(text) => handleChange('aadhaarOtp', text)}
onFocus={() => setFocusedField('adharOtp')} onFocus={() => setFocusedField('adharOtp')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'adharOtp'} focused={focusedField === 'adharOtp'}
containerStyle={{ marginBottom: spacing.s }}
/> />
<TouchableOpacity <TouchableOpacity
style={{ style={verifyButtonStyle}
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
onPress={verifyAdharOtpHandler} onPress={verifyAdharOtpHandler}
disabled={aadhaarVerifying} disabled={aadhaarVerifying}
activeOpacity={0.8}
> >
<Text style={{ color: 'white', fontWeight: '600' }}> <AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
{aadhaarVerifying ? 'Verifying...' : 'Verify'} {aadhaarVerifying ? 'Verifying...' : 'Verify OTP'}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
</> </>
)} )}
{/* ---------- GST Section ---------- */} {aadhaarVerified && (
<TouchableOpacity
style={{
borderWidth: 1.5,
borderColor: '#E67E22',
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
backgroundColor: 'transparent',
}}
onPress={() => {
Alert.alert(
'Change Aadhaar',
'Are you sure you want to change your Aadhaar details? You will need to re-verify.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Change',
style: 'destructive',
onPress: () => {
setAadhaarVerified(false);
setadharotpsend(false);
handleChange('aadhaarNumber', '');
handleChange('aadhaarOtp', '');
},
},
],
);
}}
activeOpacity={0.8}
>
<AppText variant="bodySmall" color={'#E67E22'} style={{ fontWeight: '700' }}>
Change Aadhaar
</AppText>
</TouchableOpacity>
)}
</View>
{selectedOrg !== 11 && {/* ── GST Section ── */}
<> {selectedOrg !== 11 && (
<View style={{ marginTop: spacing.m, borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.m }}>
<MyInput <MyInput
icon="file-invoice" label="GSTN Number"
placeholder="GSTN No" placeholder="Enter GSTN Number"
value={formData.gstnNo} value={formData.gstnNo}
onChangeText={(text) => handleChange('gstnNo', text)} onChangeText={(text) => handleChange('gstnNo', text)}
onFocus={() => setFocusedField('gstNumber')} onFocus={() => setFocusedField('gstNumber')}
@@ -235,30 +347,10 @@ const StageTwo: React.FC<Props> = ({
focused={focusedField === 'gstNumber'} focused={focusedField === 'gstNumber'}
autoCapitalize="characters" autoCapitalize="characters"
/> />
</View>
{/* {formData?.gstnNo?.length === 15 && ( )}
<TouchableOpacity
style={{
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
onPress={verifyGSTHandler}
>
<Text style={{ color: 'white', fontWeight: '600' }}>
{gstverifying ? 'Verifying...' : 'Verify GST'}
</Text>
</TouchableOpacity>
)} */}
</>}
</> </>
); );
}; };
export default StageTwo; export default StageTwo;
@@ -1,12 +1,12 @@
import React, { useEffect } from 'react'; import React from 'react';
import { Text, View, StyleSheet } from 'react-native'; import { View } from 'react-native';
import MyInput from '../../../Component/MyInput'; import MyInput from '../../../components/MyInput';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props { interface Props {
formData: { formData: {
agentContact2: string; agentContact2: string;
organizationName: string; organizationName: string;
officialWebsite: string; officialWebsite: string;
@@ -14,7 +14,7 @@ interface Props {
email: string; email: string;
firstName: string; firstName: string;
middleName: string; middleName: string;
lastName: string lastName: string;
}; };
handleChange: (key: string, value: string) => void; handleChange: (key: string, value: string) => void;
focusedField: string | null; focusedField: string | null;
@@ -29,27 +29,29 @@ const StageZero: React.FC<Props> = ({
setFocusedField, setFocusedField,
styles, styles,
}) => { }) => {
const { colors, spacing } = useTheme();
return ( return (
<> <>
<Text style={styles.label}>Personal & Contact Details</Text> <AppText variant="caption" color={colors.primary} style={styles.label}>
{/* <View style={styles.row}> */} Personal & Contact Details
</AppText>
<MyInput <MyInput
icon="user" label="First Name"
placeholder="First Name *" placeholder="First Name"
value={formData.firstName} value={formData.firstName}
onChangeText={(text: string) => handleChange('firstName', text)} onChangeText={(text: string) => handleChange('firstName', text)}
onFocus={() => setFocusedField('firstname')} onFocus={() => setFocusedField('firstname')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'firstname'} focused={focusedField === 'firstname'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
<View style={{ width: 10 }} />
{
formData.middleName ?
{formData.middleName ? (
<MyInput <MyInput
icon="user" label="Middle Name"
placeholder="Middle Name" placeholder="Middle Name"
value={formData.middleName} value={formData.middleName}
onChangeText={(text: string) => handleChange('middleName', text)} onChangeText={(text: string) => handleChange('middleName', text)}
@@ -57,24 +59,25 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'middleName'} focused={focusedField === 'middleName'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
: null} ) : null}
<MyInput <MyInput
icon="user" label="Last Name"
placeholder="Last Name *" placeholder="Last Name"
value={formData.lastName} value={formData.lastName}
onChangeText={(text: string) => handleChange('lastName', text)} onChangeText={(text: string) => handleChange('lastName', text)}
onFocus={() => setFocusedField('lastName')} onFocus={() => setFocusedField('lastName')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'lastName'} focused={focusedField === 'lastName'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="envelope" label="Email"
placeholder="Email *" placeholder="Email Address"
keyboardType="email-address" keyboardType="email-address"
value={formData.email} value={formData.email}
onChangeText={(text: string) => handleChange('email', text)} onChangeText={(text: string) => handleChange('email', text)}
@@ -82,11 +85,12 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'email'} focused={focusedField === 'email'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="phone" label="Mobile Number"
placeholder="Mobile Number *" placeholder="Mobile Number"
keyboardType="phone-pad" keyboardType="phone-pad"
value={formData.mobileNumber} value={formData.mobileNumber}
onChangeText={(text: string) => handleChange('mobileNumber', text)} onChangeText={(text: string) => handleChange('mobileNumber', text)}
@@ -94,22 +98,24 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'mobileNumber'} focused={focusedField === 'mobileNumber'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="building" label="Organization Name"
placeholder="Organization Name *" placeholder="Enter Organization Name"
value={formData.organizationName} value={formData.organizationName}
onChangeText={(text: string) => handleChange('organizationName', text)} onChangeText={(text: string) => handleChange('organizationName', text)}
onFocus={() => setFocusedField('organizationName')} onFocus={() => setFocusedField('organizationName')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'organizationName'} focused={focusedField === 'organizationName'}
autoCapitalize="none" autoCapitalize="none"
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="globe" label="Official Website"
placeholder="Official Website (optional)" placeholder="Website (optional)"
value={formData.officialWebsite} value={formData.officialWebsite}
onChangeText={(text: string) => handleChange('officialWebsite', text)} onChangeText={(text: string) => handleChange('officialWebsite', text)}
onFocus={() => setFocusedField('officialWebsite')} onFocus={() => setFocusedField('officialWebsite')}
+275 -294
View File
@@ -1,9 +1,7 @@
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import {
View, View,
Text,
TextInput,
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
Alert, Alert,
@@ -13,16 +11,18 @@ import {
ScrollView, ScrollView,
ToastAndroid, ToastAndroid,
} from 'react-native'; } from 'react-native';
import colors from '../../Constant/Color'; import { User, Phone, Mail, Lock, CheckSquare, Square, Shield, UserPlus, Contact, Eye, EyeOff } from 'lucide-react-native';
import { User, Phone, Mail, Lock, Eye, EyeOff, ArrowLeft, CheckSquare, Square } from 'lucide-react-native'; import { useGetDesignation, useRegisterContractor } from '../../Services/ApiCall/Hooks';
import { useGetDesignation, useRegisterContractor } from '../../Services/BBPS/Hooks';
import MyDropdown from '../../Component/Dropdown/MyDropdown'; import MyDropdown from '../../Component/Dropdown/MyDropdown';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton'; import PrimaryButton from '../../components/PrimaryButton';
import MyInput from '../../components/MyInput';
import InsideHeader from '../../Component/Header/InsideHeader';
import TermsAndConditionsModal from '../../Component/TermsAndConditionsModal/TermsAndConditionsModal'; import TermsAndConditionsModal from '../../Component/TermsAndConditionsModal/TermsAndConditionsModal';
import AppText from '../../components/AppText';
import { Formik } from 'formik'; import { Formik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import Animated, { FadeInUp } from 'react-native-reanimated'; import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
import { InsideHeader } from '../../Component/Index'; import { useTheme } from '../../Hooks/useTheme';
export type ContractorFormData = { export type ContractorFormData = {
firstName: string; firstName: string;
@@ -40,7 +40,7 @@ export type ContractorFormData = {
type DesignationOption = { type DesignationOption = {
label: string; label: string;
value: string; // string id from API value: string;
}; };
const SignupSchema = Yup.object().shape({ const SignupSchema = Yup.object().shape({
@@ -48,7 +48,7 @@ const SignupSchema = Yup.object().shape({
firstName: Yup.string().required('First name is required.'), firstName: Yup.string().required('First name is required.'),
middleName: Yup.string().optional(), middleName: Yup.string().optional(),
lastName: Yup.string().required('Last name is required.'), lastName: Yup.string().required('Last name is required.'),
designation: Yup.string().required('Please select a designation.'), // string designation: Yup.string().required('Please select a designation.'),
mobileNumber: Yup.string() mobileNumber: Yup.string()
.matches(/^[6-9]\d{9}$/, 'Invalid mobile number.') .matches(/^[6-9]\d{9}$/, 'Invalid mobile number.')
.required('Mobile number is required.'), .required('Mobile number is required.'),
@@ -65,6 +65,7 @@ const SignupSchema = Yup.object().shape({
}); });
const Signup = () => { const Signup = () => {
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const [designation, setDesignation] = useState<DesignationOption[]>([]); const [designation, setDesignation] = useState<DesignationOption[]>([]);
const { data, isLoading: isDesignationLoading } = useGetDesignation(); const { data, isLoading: isDesignationLoading } = useGetDesignation();
@@ -74,6 +75,7 @@ const Signup = () => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
useEffect(() => { useEffect(() => {
const list = (data as any)?.data; const list = (data as any)?.data;
@@ -81,89 +83,15 @@ const Signup = () => {
const formattedDesignation: DesignationOption[] = list.map((item: any) => ({ const formattedDesignation: DesignationOption[] = list.map((item: any) => ({
label: item.designationName, label: item.designationName,
value: String(item.designationid), // e.g. "3" value: String(item.designationid),
})); }));
setDesignation(formattedDesignation); setDesignation(formattedDesignation);
}, [data]); }, [data]);
const dropdownStyle = {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: colors.white,
marginBottom: 10,
};
const renderInputWithIcon = (
icon: 'user' | 'phone' | 'mail',
placeholder: string,
value: string,
onChangeText: (text: string) => void,
onBlur: () => void,
error?: string,
touched?: boolean,
props: object = {},
) => {
const IconComponent = icon === 'user' ? User : icon === 'phone' ? Phone : Mail;
return (
<>
<View style={styles.inputWrapper}>
<IconComponent size={18} color={colors.grayText} style={styles.icon} />
<TextInput
placeholder={placeholder}
style={styles.input}
placeholderTextColor={colors.placeholder}
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
{...props}
/>
</View>
{touched && error && <Text style={styles.errorText}>{error}</Text>}
</>
);
};
const renderPasswordInput = (
placeholder: string,
value: string,
onChangeText: (text: string) => void,
onBlur: () => void,
show: boolean,
setShow: (show: boolean) => void,
error?: string,
touched?: boolean,
) => (
<>
<View style={styles.inputWrapper}>
<Lock size={18} color={colors.grayText} style={styles.icon} />
<TextInput
placeholder={placeholder}
style={styles.input}
placeholderTextColor={colors.placeholder}
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
secureTextEntry={!show}
/>
<TouchableOpacity onPress={() => setShow(!show)}>
{show ? <Eye size={18} color={colors.grayText} /> : <EyeOff size={18} color={colors.grayText} />}
</TouchableOpacity>
</View>
{touched && error && <Text style={styles.errorText}>{error}</Text>}
</>
);
const handleSubmitForm = (values: any) => { const handleSubmitForm = (values: any) => {
console.log('values', values);
if (!isCheck) { if (!isCheck) {
Alert.alert('Check terms and condition'); Alert.alert('Terms Required', 'Please accept the terms and conditions to proceed.');
return; return;
} }
@@ -175,7 +103,7 @@ const Signup = () => {
mobNo: values.mobileNumber.trim(), mobNo: values.mobileNumber.trim(),
emailAdd: values.email.trim(), emailAdd: values.email.trim(),
contractorType: values.bidderType, contractorType: values.bidderType,
designation: values.designation ? Number(values.designation) : 0, // convert string -> number designation: values.designation ? Number(values.designation) : 0,
password: values.password.trim(), password: values.password.trim(),
otherDesignation: null, otherDesignation: null,
conPassword: values.confirmPassword.trim(), conPassword: values.confirmPassword.trim(),
@@ -183,6 +111,7 @@ const Signup = () => {
registerContractor(formData, { registerContractor(formData, {
onSuccess: (res: any) => { onSuccess: (res: any) => {
console.log('Register Res : ', res)
if (res?.status === '201') { if (res?.status === '201') {
ToastAndroid.show(`${res.message}`, ToastAndroid.SHORT); ToastAndroid.show(`${res.message}`, ToastAndroid.SHORT);
navigation.pop(); navigation.pop();
@@ -191,27 +120,28 @@ const Signup = () => {
} }
}, },
onError: (error: any) => { onError: (error: any) => {
console.log('Register Res : ', error)
ToastAndroid.show(error.message, ToastAndroid.SHORT); ToastAndroid.show(error.message, ToastAndroid.SHORT);
}, },
}); });
}; };
return ( return (
<View style={styles.container}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<SafeAreaView style={styles.container}> <SafeAreaView style={{ flex: 1 }}>
<InsideHeader title="Sign Up" showArrow /> <InsideHeader title="Create Account" showArrow />
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.container} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
> >
<Formik <Formik
initialValues={{ initialValues={{
bidderType: '', bidderType: 'Indian',
firstName: '', firstName: '',
middleName: '', middleName: '',
lastName: '', lastName: '',
designation: '', // string, not 0 designation: '',
mobileNumber: '', mobileNumber: '',
email: '', email: '',
password: '', password: '',
@@ -232,174 +162,239 @@ const Signup = () => {
<ScrollView <ScrollView
contentContainerStyle={styles.scrollContainer} contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
bounces={false} showsVerticalScrollIndicator={false}
> >
<Animated.View entering={FadeInUp.duration(450).delay(80)}> {/* ═══ 1. Bidder Type Toggle ═══ */}
<Text style={styles.label}>Bidder Type</Text> <Animated.View entering={FadeInUp.duration(400).delay(100)} style={{ marginBottom: spacing.m }}>
<View style={styles.toggleContainer}> <View style={styles.sectionLabelRow}>
<UserPlus size={14} color={colors.primary} />
<AppText variant="caption" color={colors.primary} style={styles.sectionLabel}>
Bidder Type
</AppText>
</View>
<View style={[styles.toggleContainer, shadows.light]}>
{['Indian', 'Foreign'].map(type => ( {['Indian', 'Foreign'].map(type => (
<TouchableOpacity <TouchableOpacity
key={type} key={type}
style={[ style={[
styles.toggleButton, styles.toggleButton,
values.bidderType === type && styles.selectedToggle, values.bidderType === type && [styles.toggleButtonActive, { backgroundColor: colors.primary }],
]} ]}
onPress={() => setFieldValue('bidderType', type)} onPress={() => setFieldValue('bidderType', type)}
activeOpacity={0.8}
> >
<Text <AppText
style={[ variant="bodySmall"
styles.toggleText, color={values.bidderType === type ? colors.buttonText : colors.textSecondary}
values.bidderType === type && styles.selectedToggleText, style={{ fontWeight: values.bidderType === type ? '700' : '500' }}
]}
> >
{type} {type} Contractor
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
{touched.bidderType && errors.bidderType && ( {touched.bidderType && errors.bidderType && (
<Text style={styles.errorText}>{errors.bidderType}</Text> <AppText variant="caption" color={colors.error} style={styles.errorText}>
{errors.bidderType}
</AppText>
)} )}
</Animated.View> </Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(120)}>
<View style={styles.row}> {/* ═══ 2. Personal Information ═══ */}
<View style={styles.halfInput}> <Animated.View entering={FadeInUp.duration(450).delay(200)} style={[styles.card, shadows.medium]}>
{renderInputWithIcon( <View style={styles.cardHeader}>
'user', <User size={16} color={colors.primary} />
'First Name *', <AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
values.firstName, Personal Information
handleChange('firstName'), </AppText>
() => handleBlur('firstName'),
errors.firstName,
touched.firstName,
)}
</View>
<View style={styles.halfInput}>
{renderInputWithIcon(
'user',
'Middle Name',
values.middleName,
handleChange('middleName'),
() => handleBlur('middleName'),
errors.middleName,
touched.middleName,
)}
</View>
</View> </View>
{renderInputWithIcon( <MyInput
'user', label="First Name"
'Last Name *', placeholder="Enter First Name"
values.lastName, value={values.firstName}
handleChange('lastName'), onChangeText={handleChange('firstName')}
() => handleBlur('lastName'), onBlur={handleBlur('firstName')}
errors.lastName, error={touched.firstName && errors.firstName ? errors.firstName : undefined}
touched.lastName, rightIcon={<User size={18} color={colors.textSecondary} />}
)} containerStyle={{ marginBottom: spacing.s }}
</Animated.View> />
<MyInput
label="Middle Name"
placeholder="Enter Middle Name (Optional)"
value={values.middleName}
onChangeText={handleChange('middleName')}
onBlur={handleBlur('middleName')}
error={touched.middleName && errors.middleName ? errors.middleName : undefined}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Last Name"
placeholder="Enter Last Name"
value={values.lastName}
onChangeText={handleChange('lastName')}
onBlur={handleBlur('lastName')}
error={touched.lastName && errors.lastName ? errors.lastName : undefined}
containerStyle={{ marginBottom: spacing.s }}
/>
<Animated.View entering={FadeInUp.duration(450).delay(160)}>
<Text style={styles.label}>Designation</Text>
<MyDropdown <MyDropdown
label="Select Designation" label="Designation"
labelField="label" labelField="label"
valueField="value" valueField="value"
data={designation} data={designation}
value={values.designation} value={values.designation}
onChange={(item: any) => { onChange={(item: any) => { setFieldValue('designation', item.value); }}
setFieldValue('designation', item.value); placeholder={isDesignationLoading ? 'Loading...' : 'Select Designation'}
}}
placeholder={isDesignationLoading ? 'Loading...' : 'Choose Designation'}
style={dropdownStyle}
/> />
{touched.designation && errors.designation && ( {touched.designation && errors.designation && (
<Text style={styles.errorText}>{errors.designation}</Text> <AppText variant="caption" color={colors.error} style={styles.errorText}>
{errors.designation}
</AppText>
)} )}
</Animated.View> </Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(200)}>
{renderInputWithIcon(
'phone',
'Mobile Number *',
values.mobileNumber,
handleChange('mobileNumber'),
() => handleBlur('mobileNumber'),
errors.mobileNumber,
touched.mobileNumber,
{
keyboardType: 'phone-pad',
maxLength: 10,
},
)}
{renderInputWithIcon( {/* ═══ 3. Contact Details ═══ */}
'mail', <Animated.View entering={FadeInUp.duration(450).delay(300)} style={[styles.card, shadows.medium]}>
'Email *', <View style={styles.cardHeader}>
values.email, <Contact size={16} color={colors.primary} />
handleChange('email'), <AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
() => handleBlur('email'), Contact Details
errors.email, </AppText>
touched.email,
{
keyboardType: 'email-address',
autoCapitalize: 'none',
},
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(240)}>
{renderPasswordInput(
'Password *',
values.password,
handleChange('password'),
() => handleBlur('password'),
showPassword,
setShowPassword,
errors.password,
touched.password,
)}
{renderPasswordInput(
'Confirm Password *',
values.confirmPassword,
handleChange('confirmPassword'),
() => handleBlur('confirmPassword'),
showConfirmPassword,
setShowConfirmPassword,
errors.confirmPassword,
touched.confirmPassword,
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(280)}>
<View style={styles.termsRow}>
<TouchableOpacity onPress={() => setCheckModal(true)}>
{isCheck ? (
<CheckSquare size={20} color={colors.primary} />
) : (
<Square size={20} color={colors.black} />
)}
</TouchableOpacity>
<Text style={styles.termsText}>
By continuing, I agree to the Terms and Conditions
</Text>
</View> </View>
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(320)} style={styles.buttonWrapper}> <MyInput
<PrimaryButton label="Mobile Number"
title={isRegistering ? 'Submitting...' : 'Submit'} placeholder="Enter Mobile Number"
disabled={isRegistering} value={values.mobileNumber}
onPress={() => handleSubmit()} onChangeText={handleChange('mobileNumber')}
onBlur={handleBlur('mobileNumber')}
error={touched.mobileNumber && errors.mobileNumber ? errors.mobileNumber : undefined}
keyboardType="phone-pad"
maxLength={10}
rightIcon={<Phone size={18} color={colors.textSecondary} />}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Email Address"
placeholder="Enter Email Address"
value={values.email}
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
error={touched.email && errors.email ? errors.email : undefined}
keyboardType="email-address"
autoCapitalize="none"
rightIcon={<Mail size={18} color={colors.textSecondary} />}
containerStyle={{ marginBottom: spacing.s }}
/> />
</Animated.View> </Animated.View>
{/* ═══ 4. Security ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(400)} style={[styles.card, shadows.medium]}>
<View style={styles.cardHeader}>
<Shield size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
Security
</AppText>
</View>
<MyInput
label="Password"
placeholder="••••••••"
value={values.password}
onChangeText={handleChange('password')}
onBlur={handleBlur('password')}
secureTextEntry={!showPassword}
error={touched.password && errors.password ? errors.password : undefined}
rightIcon={
showPassword
? <Eye size={18} color={colors.textSecondary} />
: <EyeOff size={18} color={colors.textSecondary} />
}
rightIconPress={() => setShowPassword(prev => !prev)}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Confirm Password"
placeholder="••••••••"
value={values.confirmPassword}
onChangeText={handleChange('confirmPassword')}
onBlur={handleBlur('confirmPassword')}
secureTextEntry={!showConfirmPassword}
error={touched.confirmPassword && errors.confirmPassword ? errors.confirmPassword : undefined}
rightIcon={
showConfirmPassword
? <Eye size={18} color={colors.textSecondary} />
: <EyeOff size={18} color={colors.textSecondary} />
}
rightIconPress={() => setShowConfirmPassword(prev => !prev)}
containerStyle={{ marginBottom: spacing.s }}
/>
</Animated.View>
{/* ═══ Terms & Conditions ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(500)}>
<TouchableOpacity
style={[styles.termsCard, shadows.light]}
onPress={() => setCheckModal(true)}
activeOpacity={0.8}
>
{isCheck ? (
<CheckSquare size={22} color={colors.primary} strokeWidth={2.5} />
) : (
<Square size={22} color={colors.textSecondary} strokeWidth={2} />
)}
<AppText variant="bodySmall" color={colors.textSecondary} style={{ flex: 1 }}>
I agree to the{' '}
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700', textDecorationLine: 'underline' }}>
Terms and Conditions
</AppText>
</AppText>
</TouchableOpacity>
</Animated.View>
{/* ═══ Submit Button ═══ */}
<Animated.View entering={FadeInDown.duration(400).delay(600)} style={styles.buttonWrapper}>
<PrimaryButton
title="Create Account"
loading={isRegistering}
onPress={handleSubmit}
variant="primary"
/>
</Animated.View>
{/* ═══ Login Link ═══ */}
<Animated.View entering={FadeInUp.duration(400).delay(650)}>
<TouchableOpacity
onPress={() => navigation.pop()}
style={styles.loginRow}
activeOpacity={0.7}
>
<AppText variant="bodySmall" color={colors.textSecondary}>
Already have an account?{' '}
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
Sign In
</AppText>
</AppText>
</TouchableOpacity>
</Animated.View>
{checkModal && ( {checkModal && (
<TermsAndConditionsModal <TermsAndConditionsModal
visible={checkModal} visible={checkModal}
onClose={() => setCheckModal(false)} onClose={() => setCheckModal(false)}
onChecked={() => setIsCheck(true)} onChecked={() => { setIsCheck(true); setCheckModal(false); }}
/> />
)} )}
</ScrollView> </ScrollView>
@@ -413,99 +408,85 @@ const Signup = () => {
export default Signup; export default Signup;
const styles = StyleSheet.create({
container: { // ── Styles ────────────────────────────────────────────────────────────
flex: 1, const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
backgroundColor: colors.white,
},
scrollContainer: { scrollContainer: {
flexGrow: 1, flexGrow: 1,
paddingHorizontal: 16, padding: spacing.m,
paddingTop: 12, paddingBottom: 30,
paddingBottom: 24,
justifyContent: 'flex-start',
}, },
label: { sectionLabelRow: {
fontSize: 15, flexDirection: 'row',
fontWeight: '600', alignItems: 'center',
marginTop: 6, gap: 6,
marginBottom: 4, marginBottom: 6,
color: colors.darkText, },
sectionLabel: {
fontWeight: '700',
fontSize: typography.size.xs,
letterSpacing: 0.5,
textTransform: 'uppercase',
}, },
toggleContainer: { toggleContainer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', borderRadius: 12,
marginBottom: 4, padding: 3,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
height: 46,
}, },
toggleButton: { toggleButton: {
flex: 1, flex: 1,
paddingVertical: 10, justifyContent: 'center',
borderRadius: 10,
borderWidth: 1,
borderColor: colors.primary,
alignItems: 'center', alignItems: 'center',
marginHorizontal: 4, borderRadius: 9,
backgroundColor: colors.white,
}, },
selectedToggle: { toggleButtonActive: {
backgroundColor: colors.primary, elevation: 2,
borderColor: colors.primary,
}, },
toggleText: { card: {
fontSize: 14, backgroundColor: colors.surface,
color: colors.grayText, borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
}, },
selectedToggleText: { cardHeader: {
color: colors.white,
fontWeight: '600',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 6,
},
halfInput: {
width: '48%',
},
inputWrapper: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.white, gap: 8,
borderRadius: 12, marginBottom: spacing.s,
borderWidth: 1, paddingBottom: spacing.xs,
borderColor: colors.border, borderBottomWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 10, borderBottomColor: colors.border,
marginTop: 8,
height: 52,
}, },
icon: { cardTitle: {
marginRight: 10, fontWeight: '700',
}, fontSize: typography.size.s,
input: { letterSpacing: 0.3,
flex: 1,
fontSize: 15,
color: colors.darkText,
paddingVertical: 0,
}, },
errorText: { errorText: {
color: colors.error, marginTop: 4,
fontSize: 12, fontSize: typography.size.xs,
marginTop: 3, fontWeight: '500',
marginLeft: 4,
}, },
termsRow: { termsCard: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginTop: 14, gap: 10,
marginHorizontal: 2, backgroundColor: colors.surface,
}, borderRadius: 12,
termsText: { padding: 14,
flex: 1, marginBottom: spacing.m,
fontSize: 13, borderWidth: 1,
color: colors.darkText, borderColor: colors.border,
marginLeft: 8,
}, },
buttonWrapper: { buttonWrapper: {
marginTop: 18, marginBottom: spacing.s,
},
loginRow: {
alignItems: 'center',
marginBottom: spacing.m,
}, },
}); });
+43 -54
View File
@@ -1,12 +1,11 @@
import React, { useEffect, useRef, useState, useCallback } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native'; import { Animated, StyleSheet, View } from 'react-native';
import { useNavigation, useIsFocused } from '@react-navigation/native'; import { useNavigation, useIsFocused } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useStore } from 'react-redux'; // For hydration check
import { showToast } from '../../Constant/ShowDailog'; import { showErrorAlert, showToast } from '../../Constant/ShowDailog';
import { useRagistrationState } from '../../Services/BBPS/Hooks'; import { useRagistrationState } from '../../Services/ApiCall/Hooks';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
// Navigation type // Navigation type
@@ -24,34 +23,14 @@ const Splash: React.FC = () => {
const isFocused = useIsFocused(); const isFocused = useIsFocused();
const fadeAnim = useRef(new Animated.Value(0)).current; const fadeAnim = useRef(new Animated.Value(0)).current;
const reduxToken = useSelector((state: any) => state.token?.token); const reduxToken = useSelector((state: any) => state.token?.token);
console.log("user Token",reduxToken) const isPersistRehydrated = useSelector((state: any) => state?._persist?.rehydrated);
const store = useStore();
const { mutateAsync: registrationStateMutate } = useRagistrationState(); const { mutateAsync: registrationStateMutate } = useRagistrationState();
const [isHydrated, setIsHydrated] = useState(false); const hasSessionToken = Boolean(reduxToken && typeof reduxToken === 'object' && reduxToken.token);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { const routeByOrgType = useCallback((params: { otherId: any; orgType: any }) => {
let unsubscribe: any; const orgType = String(params.orgType ?? '');
const checkHydration = () => {
const token = (store.getState() as any).token?.token;
if (token !== undefined) {
setIsHydrated(true);
unsubscribe?.();
}
};
unsubscribe = store.subscribe(checkHydration);
checkHydration(); // Check immediately
return () => unsubscribe?.();
}, []);
const routeByOrgType = (params: { otherId: any; orgType: any }) => {
const orgType = String(params.orgType);
if (orgType === '4' || orgType === '5' || orgType === '6') { if (orgType === '4' || orgType === '5' || orgType === '6') {
navigation.replace('StageThree', params); navigation.replace(NavigationString.StageThreeScreen, params);
return; return;
} }
@@ -61,12 +40,27 @@ const Splash: React.FC = () => {
} }
navigation.replace('Dashboard'); navigation.replace('Dashboard');
}; }, [navigation]);
const handleGetData = async () => { const handleGetData = useCallback(async () => {
try { try {
const res = await registrationStateMutate(); const res = await registrationStateMutate();
console.log("ragister",res) console.log("splash", res);
if (res?.status === false) {
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
});
return;
}
if (res?.data?.message === 'No Response From Server' && res?.data?.success === false) {
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
});
return;
}
if (res?.statusCode === 404 || res?.statusCode === 401) { if (res?.statusCode === 404 || res?.statusCode === 401) {
navigation.replace('Registration'); navigation.replace('Registration');
return; return;
@@ -75,8 +69,6 @@ const Splash: React.FC = () => {
if (res?.statusCode === 200 && res?.data) { if (res?.statusCode === 200 && res?.data) {
const { registrationStatus, contOtherId, contOrganization } = res.data; const { registrationStatus, contOtherId, contOrganization } = res.data;
const params = { otherId: contOtherId, orgType: contOrganization }; const params = { otherId: contOtherId, orgType: contOrganization };
console.log('Registration status:', registrationStatus);
switch (registrationStatus) { switch (registrationStatus) {
case null: case null:
case undefined: case undefined:
@@ -90,14 +82,17 @@ const Splash: React.FC = () => {
navigation.replace(NavigationString.Home); navigation.replace(NavigationString.Home);
break; break;
default: default:
navigation.replace(NavigationString.Registration); navigation.replace(NavigationString.Login);
break; break;
} }
return; return;
} }
console.error('API error:', res); console.error('API error:', res);
showToast(res?.message || 'Something went wrong'); showErrorAlert(res?.error || 'Something went wrong. Please try re-logging in.');
navigation.replace('Registration'); navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
});
} catch (error: any) { } catch (error: any) {
console.error('Registration status error:', error); console.error('Registration status error:', error);
showToast(error?.message || 'Something went wrong'); showToast(error?.message || 'Something went wrong');
@@ -105,18 +100,19 @@ const Splash: React.FC = () => {
if (error?.response?.status === 404) { if (error?.response?.status === 404) {
navigation.replace('Registration'); navigation.replace('Registration');
} else { } else {
await AsyncStorage.removeItem('token'); await AsyncStorage.removeItem('persist:root');
navigation.replace('Signin'); navigation.replace(NavigationString.Login);
} }
} }
}; }, [navigation, registrationStateMutate, routeByOrgType]);
useEffect(() => { useEffect(() => {
if (!isFocused || !isHydrated) return; if (!isFocused || !isPersistRehydrated) {
return;
}
const runSplash = async () => { const runSplash = async () => {
try { try {
setIsLoading(true);
Animated.timing(fadeAnim, { Animated.timing(fadeAnim, {
toValue: 1, toValue: 1,
duration: 500, duration: 500,
@@ -124,7 +120,7 @@ const Splash: React.FC = () => {
}).start(); }).start();
if (!reduxToken.token) { if (!hasSessionToken) {
navigation.reset({ navigation.reset({
index: 0, index: 0,
routes: [{ name: NavigationString.Login }], routes: [{ name: NavigationString.Login }],
@@ -132,7 +128,7 @@ const Splash: React.FC = () => {
return; return;
} }
if (reduxToken.userType == 'tender') { if (reduxToken.userType === 'tender') {
await handleGetData(); await handleGetData();
} else { } else {
navigation.reset({ navigation.reset({
@@ -144,18 +140,16 @@ const Splash: React.FC = () => {
} catch (error) { } catch (error) {
navigation.reset({ navigation.reset({
index: 0, index: 0,
routes: [{ name: NavigationString.Login || 'Signin' }], routes: [{ name: NavigationString.Login }],
}); });
} finally {
setIsLoading(false);
} }
}; };
const timeoutId = setTimeout(runSplash, 200); const timeoutId = setTimeout(runSplash, 200);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [isFocused, isHydrated]); }, [fadeAnim, handleGetData, hasSessionToken, isFocused, isPersistRehydrated, navigation, reduxToken?.userType]);
if (!isHydrated) { if (!isPersistRehydrated) {
return ( return (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<Animated.Image <Animated.Image
@@ -175,11 +169,6 @@ const Splash: React.FC = () => {
style={[styles.logo, { opacity: fadeAnim }]} style={[styles.logo, { opacity: fadeAnim }]}
resizeMode="contain" resizeMode="contain"
/> />
{isLoading && (
<View style={{ marginTop: 20 }}>
{/* Optional spinner */}
</View>
)}
</View> </View>
</View> </View>
); );
@@ -1,33 +1,39 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { View, Text, TextInput, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator, Alert, } from 'react-native'; import {
View,
Text,
TextInput,
ScrollView,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
Alert,
} from 'react-native';
import RNFS from 'react-native-fs'; import RNFS from 'react-native-fs';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import axios from 'axios'; import axios, { AxiosError } from 'axios';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { InsideHeader } from '../../../Component/Index';
import colors from '../../../Constant/Color'; import colors from '../../../Constant/Color';
import { GET_ENVELOPE_C, SUBMIT_ENVELOPE_C_UPLOAD } from '../../../Services/BBPS/ApiUrls'; import { GET_ENVELOPE_C, SUBMIT_ENVELOPE_C_UPLOAD } from '../../../Services/ApiCall/ApiUrls';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { encrypt, encryptWithRSA } from '../../../Services/Encryption/encrypt';
// Excel column keys /* -------------------- UTILS -------------------- */
const QTY_KEY = '__EMPTY_1'; interface OfficerKey {
const RATE_KEY = '__EMPTY_2'; officersId: number;
const VALUE_KEY = '__EMPTY_3'; officerPublicKey: string;
const TOTAL_LABEL = 'TOTAL'; officerType: string;
}
const EnvelopeCExcelPreview = (tenderData: any) => {
const token = useSelector((state: any) => state.token.token);
const navigation = useNavigation() as any;
const [data, setData] = useState<any[]>([]);
const [columns, setColumns] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => { interface SaveEnvelopeOfficerDetail {
loadExcelFromApi(); officerId: number;
}, []); officerEncryptedKey: string;
officerType: string;
}
// ArrayBuffer → Base64 // Convert ArrayBuffer → Base64
const arrayBufferToBase64 = (buffer: ArrayBuffer) => { const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
let binary = ''; let binary = '';
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
@@ -37,147 +43,381 @@ const EnvelopeCExcelPreview = (tenderData: any) => {
return global.btoa(binary); return global.btoa(binary);
}; };
// Calculate TOTAL VALUE
const calculateTotal = (rows: any[]) =>
rows.reduce((sum, row) => {
if (typeof row['DEMO DEPARTEMENT'] !== 'number') return sum;
const val = Number(row[VALUE_KEY]);
return sum + (isNaN(val) ? 0 : val);
}, 0);
// LOAD EXCEL // Find column dynamically by keywords
const findColumnKey = (columns: string[], keywords: string[]) =>
columns.find(col =>
keywords.some(k => col.toLowerCase().includes(k.toLowerCase()))
);
// Detect TOTAL row (any column contains TOTAL)
const isTotalRow = (row: any) =>
Object.values(row).some(v => String(v).toUpperCase() === 'TOTAL');
/* -------------------- COMPONENT -------------------- */
const EnvelopeCExcelPreview = (props: any) => {
const tenderData = props;
console.log("Tender Data", tenderData)
const token = useSelector((state: any) => state.token.token);
const navigation = useNavigation() as any;
const [data, setData] = useState<any[]>([]);
const [columns, setColumns] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadExcelFromApi();
}, []);
/* -------------------- LOAD EXCEL -------------------- */
const loadExcelFromApi = async () => { const loadExcelFromApi = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await axios.get(`${GET_ENVELOPE_C}${tenderData?.tenderId?.tenderId}`, {
console.log('\n📊 LOADING EXCEL FROM API');
console.log('═══════════════════════════════════════════════════════');
console.log('🔗 API URL:', `${GET_ENVELOPE_C}${tenderData?.tenderId?.tenderId}`);
console.log('🆔 Tender ID:', tenderData?.tenderId?.tenderId);
console.log('🔑 Token:', token.token ? 'Present' : 'Missing');
const response = await axios.get(
`${GET_ENVELOPE_C}${tenderData?.tenderId?.tenderId}`,
{
responseType: 'arraybuffer', responseType: 'arraybuffer',
headers: { headers: {
Authentication: `Bearer ${token.token}`, Authentication: `Bearer ${token.token}`,
}, },
}); }
console.log("excel response",response) );
console.log('\n✅ API RESPONSE RECEIVED');
console.log('───────────────────────────────────────────────────────');
console.log('📡 Status:', response.status);
console.log('📡 Status Text:', response.statusText);
console.log('📦 Response Data Size (KB):', (response.data.byteLength / 1024).toFixed(2));
const base64 = arrayBufferToBase64(response.data); const base64 = arrayBufferToBase64(response.data);
const workbook = XLSX.read(base64, { type: 'base64' }); const workbook = XLSX.read(base64, { type: 'base64' });
const sheet = workbook.Sheets[workbook.SheetNames[0]]; const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: '' }); const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: '' });
console.log('📊 PARSED DATA - Total Rows:', rows.length);
setColumns(Object.keys(rows[0] || {})); setColumns(Object.keys(rows[0] || {}));
setData(rows); setData(rows);
await RNFS.writeFile(
`${RNFS.DocumentDirectoryPath}/envelopeC.xlsx`, console.log('✅ EXCEL LOADED SUCCESSFULLY\n');
base64,
'base64'
); } catch (e: any) {
} catch (e) { console.log('\n❌ ERROR LOADING EXCEL');
Alert.alert('Error', 'Failed to load Excel'); console.error('Error:', e.message);
Alert.alert('Error', 'Failed to load Excel: ' + e.message);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
// UPDATE CELL (QTY / RATE) + AUTO VALUE + AUTO TOTAL
/* -------------------- UPDATE CELL -------------------- */
const updateCell = (rowIndex: number, key: string, value: string) => { const updateCell = (rowIndex: number, key: string, value: string) => {
const updated = [...data]; const updated = [...data];
const row = updated[rowIndex]; const row = updated[rowIndex];
// TOTAL row locked
if (row['DEMO DEPARTEMENT'] === TOTAL_LABEL) return;
// Allow only QTY & RATE edit if (isTotalRow(row)) return;
if (key === QTY_KEY || key === RATE_KEY) {
const qtyKey: any = findColumnKey(columns, ['qty', 'quantity']);
const rateKey: any = findColumnKey(columns, ['rate', 'price']);
const valueKey = findColumnKey(columns, ['value', 'amount', 'total']);
if (key === qtyKey || key === rateKey) {
row[key] = value; row[key] = value;
const qty = Number(row[QTY_KEY]);
const rate = Number(row[RATE_KEY]);
// 🔥 Auto VALUE = QTY × RATE const qty = Number(row[qtyKey]);
row[VALUE_KEY] = const rate = Number(row[rateKey]);
if (valueKey) {
row[valueKey] =
!isNaN(qty) && !isNaN(rate) ? qty * rate : 0; !isNaN(qty) && !isNaN(rate) ? qty * rate : 0;
} }
// 🔥 Update TOTAL
const totalRowIndex = updated.findIndex(
r => r['DEMO DEPARTEMENT'] === TOTAL_LABEL
);
if (totalRowIndex !== -1) {
updated[totalRowIndex][VALUE_KEY] = calculateTotal(updated);
} }
// Recalculate TOTAL
const totalRowIndex = updated.findIndex(isTotalRow);
if (totalRowIndex !== -1 && valueKey) {
updated[totalRowIndex][valueKey] = updated.reduce((sum, r) => {
if (isTotalRow(r)) return sum;
const val = Number(r[valueKey]);
return sum + (isNaN(val) ? 0 : val);
}, 0);
}
setData(updated); setData(updated);
}; };
// SAVE UPDATED EXCEL
/* -------------------- SAVE & UPLOAD EXCEL -------------------- */
const buildOfficerEncryptedKeys = (envelopeKey: string): SaveEnvelopeOfficerDetail[] => {
const officerKeys: OfficerKey[] = tenderData?.tenderId?.envelopeKeyDetailsResDTO?.departmentOfficerPublicKeys || [];
console.log('\n🔐 ENCRYPTING ENVELOPE KEY FOR OFFICERS');
console.log('═══════════════════════════════════════════════════════');
console.log('📌 Envelope Key to encrypt:', envelopeKey);
console.log('👥 Number of officers:', officerKeys.length);
const encryptedKeys: SaveEnvelopeOfficerDetail[] = [];
for (let i = 0; i < officerKeys.length; i++) {
const officer = officerKeys[i];
console.log(`\n🔄 Officer ${i + 1}/${officerKeys.length}`);
console.log(' ID:', officer.officersId);
console.log(' Type:', officer.officerType);
try {
const encryptedKey = encryptWithRSA(envelopeKey, officer.officerPublicKey);
if (!encryptedKey) {
console.error(` ❌ FAILED to encrypt key for officer ${officer.officersId}`);
} else {
console.log(` ✅ SUCCESS - Encrypted key length: ${encryptedKey.length}`);
}
encryptedKeys.push({
officerId: officer.officersId,
officerEncryptedKey: encryptedKey || '',
officerType: officer.officerType,
});
} catch (error) {
console.error(` ❌ ERROR encrypting for officer ${officer.officersId}:`, error);
encryptedKeys.push({
officerId: officer.officersId,
officerEncryptedKey: '',
officerType: officer.officerType,
});
}
}
console.log('\n✅ ALL OFFICER KEYS ENCRYPTED');
console.log('═══════════════════════════════════════════════════════\n');
return encryptedKeys;
};
// ✅ NEW: Encrypt Excel file content (binary) after creation
const encryptExcelFile = async (filePath: string, encryptionKey: string): Promise<string> => {
console.log('\n🔐 ENCRYPTING EXCEL FILE CONTENT');
console.log('═══════════════════════════════════════════════════════');
console.log('📁 File Path:', filePath);
console.log('🔑 Encryption Key:', encryptionKey);
try {
// Read the Excel file as Base64
const fileContent = await RNFS.readFile(filePath, 'base64');
console.log('📖 File read successfully, Base64 length:', fileContent.length);
// Encrypt the entire file content
console.log('🔒 Encrypting file content...');
const encryptedContent: any = encrypt(fileContent, encryptionKey);
console.log('✅ File content encrypted, Encrypted length:', encryptedContent?.length);
// Save encrypted content to a new file
const encryptedFilePath = `${RNFS.DocumentDirectoryPath}/envelopeC_encrypted.xlsx`;
await RNFS.writeFile(encryptedFilePath, encryptedContent, 'utf8');
console.log('💾 Encrypted file saved at:', encryptedFilePath);
console.log('✅ EXCEL FILE ENCRYPTION COMPLETED');
console.log('═══════════════════════════════════════════════════════\n');
return encryptedFilePath;
} catch (error) {
console.error('❌ ERROR ENCRYPTING EXCEL FILE:', error);
throw error;
}
};
const saveUpdatedExcel = async () => { const saveUpdatedExcel = async () => {
try { try {
setLoading(true); setLoading(true);
console.log('\n💾 SAVING UPDATED EXCEL');
console.log('═══════════════════════════════════════════════════════');
const ENCRYPTION_KEY = tenderData?.tenderId?.envelopeKeyDetailsResDTO?.envelopeCGenerateKey || '';
if (!ENCRYPTION_KEY) {
console.error('❌ ENCRYPTION KEY NOT FOUND');
Alert.alert('Error', 'Encryption key not available');
setLoading(false);
return;
}
console.log('🔑 Encryption Key:', ENCRYPTION_KEY);
// Step 1: Create normal Excel workbook with user data (NOT ENCRYPTED YET)
console.log('\n📝 STEP 1: Creating Excel workbook with user data');
console.log('───────────────────────────────────────────────────────');
const worksheet = XLSX.utils.json_to_sheet(data); const worksheet = XLSX.utils.json_to_sheet(data);
const workbook = XLSX.utils.book_new(); const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// Generate binary string
const binary = XLSX.write(workbook, { const binary = XLSX.write(workbook, {
bookType: 'xlsx', bookType: 'xlsx',
type: 'binary', type: 'binary',
}); });
// Convert binary string to buffer
const buffer = new Uint8Array(binary.length); const buffer = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) { for (let i = 0; i < binary.length; i++) {
buffer[i] = binary.charCodeAt(i) & 0xff; buffer[i] = binary.charCodeAt(i) & 0xff;
} }
const filePath = `${RNFS.DocumentDirectoryPath}/envelopeC_updated.xlsx`; // Step 2: Save the plain Excel file
console.log('\n📝 STEP 2: Saving plain Excel file');
console.log('───────────────────────────────────────────────────────');
const plainFilePath = `${RNFS.DocumentDirectoryPath}/envelopeC_plain.xlsx`;
await RNFS.writeFile(plainFilePath, buffer.toString(), 'ascii');
console.log('✅ Plain Excel saved at:', plainFilePath);
// Write as binary
await RNFS.writeFile(filePath, buffer.toString(), 'ascii');
// const fileBinary = await RNFS.readFile(filePath, 'ascii');
// console.log('Excel binary content:', fileBinary); // Step 3: Encrypt the entire Excel file
// Alert.alert('Success', 'Excel saved successfully'); console.log('\n📝 STEP 3: Encrypting the Excel file');
console.log('───────────────────────────────────────────────────────');
const encryptedFilePath = await encryptExcelFile(plainFilePath, ENCRYPTION_KEY);
console.log('✅ Encrypted Excel file ready');
// Step 4: Build officer encrypted keys
console.log('\n📝 STEP 4: Building officer encrypted keys');
console.log('───────────────────────────────────────────────────────');
const officerEncryptedKeys = buildOfficerEncryptedKeys(ENCRYPTION_KEY);
// Step 5: Prepare FormData for upload
console.log('\n📝 STEP 5: Building FormData');
console.log('───────────────────────────────────────────────────────');
const formData = new FormData(); const formData = new FormData();
const requestDto = {
// requestDto payload
formData.append(
'requestDto',
JSON.stringify({
tenderId: tenderData?.tenderId?.tenderId, tenderId: tenderData?.tenderId?.tenderId,
deptId: tenderData?.tenderId?.deptId, deptId: tenderData?.tenderId?.deptId,
typesOfTender: tenderData?.tenderId?.typesOfTender, typesOfTender: tenderData?.tenderId?.typesOfTender,
fieldValue: '', fieldValue: '',
fieldValueInWord: 'zero', fieldValueInWord: 'zero',
}) deviceType: 'MOBILE',
); fileName: 'envelopeC_encrypted.xlsx',
saveEnvelopesOfficersDetailsResDTO: officerEncryptedKeys,
randomKeyGenerated: ENCRYPTION_KEY,
};
// binary file
formData.append('requestDto', JSON.stringify(requestDto));
// Use the ENCRYPTED file for upload
formData.append('file', { formData.append('file', {
uri: `file://${filePath}`, uri: `file://${encryptedFilePath}`,
name: 'envelopeC_updated.xlsx', name: 'envelopeC_encrypted.xlsx',
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}); } as any);
const res = await axios.post(SUBMIT_ENVELOPE_C_UPLOAD, formData, {
console.log('📋 Request DTO:', JSON.stringify(requestDto, null, 2));
console.log('📎 Encrypted File Path:', encryptedFilePath);
// Step 6: Upload to server
console.log('\n📝 STEP 6: Uploading encrypted file to server');
console.log('───────────────────────────────────────────────────────');
console.log('FormData prepared:', formData);
const response = await axios.post(SUBMIT_ENVELOPE_C_UPLOAD, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
Authentication: `Bearer ${token.token}`, Authentication: `Bearer ${token.token}`,
}, },
}); });
console.log('✅ UPLOAD SUCCESSFUL');
console.log('Response:', response.data);
console.log('═══════════════════════════════════════════════════════\n');
// Clean up temporary files
await RNFS.unlink(plainFilePath).catch(e => console.log('Plain file cleanup:', e.message));
await RNFS.unlink(encryptedFilePath).catch(e => console.log('Encrypted file cleanup:', e.message));
Alert.alert('Success', 'Excel uploaded successfully');
navigation.goBack(); navigation.goBack();
console.log("res of updated excel file : ", res)
} catch (e) {
console.error(e); } catch (e: any) {
Alert.alert('Error', 'Save failed'); console.error('❌ Error saving/uploading Excel:', {
message: e?.message,
response: e?.response?.data,
status: e?.response?.status,
statusText: e?.response?.statusText,
});
Alert.alert('Error', e?.response?.data?.message || 'Save failed');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
/* -------------------- UI -------------------- */
if (loading) { if (loading) {
return ( return (
@@ -187,43 +427,43 @@ const EnvelopeCExcelPreview = (tenderData: any) => {
); );
} }
const qtyKey = findColumnKey(columns, ['qty', 'quantity']);
const rateKey = findColumnKey(columns, ['rate', 'price']);
const valueKey = findColumnKey(columns, ['value', 'amount', 'total']);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ScrollView horizontal contentContainerStyle={{ padding: 10 }}>
<ScrollView horizontal contentContainerStyle={{ paddingHorizontal: 10, paddingTop: 20, backgroundColor: colors.white }}>
<View> <View>
{/* HEADER */} {/* HEADER */}
<View style={styles.row}> <View style={styles.row}>
{columns.map(col => ( {columns.map(col => (
<Text key={col} style={styles.headerCell}> <Text key={col} style={styles.headerCell}>{col}</Text>
{col}
</Text>
))} ))}
</View> </View>
{/* DATA */} {/* DATA */}
<ScrollView style={{ maxHeight: 420 }}> <ScrollView style={{ maxHeight: 420 }}>
{data.map((row, rowIndex) => ( {data.map((row, rowIndex) => (
<View key={rowIndex} style={styles.row}> <View key={rowIndex} style={styles.row}>
{columns.map(key => { {columns.map(key => {
const isTotal = row['DEMO DEPARTEMENT'] === TOTAL_LABEL; const editable =
const isEditable = !isTotalRow(row) && (key === qtyKey || key === rateKey);
!isTotal && (key === QTY_KEY || key === RATE_KEY);
return ( return (
<TextInput <TextInput
key={key} key={key}
value={String(row[key])} value={String(row[key])}
editable={isEditable} editable={editable}
keyboardType={ keyboardType={editable ? 'numeric' : 'default'}
key === QTY_KEY || key === RATE_KEY
? 'numeric'
: 'default'
}
style={[ style={[
styles.cell, styles.cell,
isTotal && styles.totalCell, isTotalRow(row) && styles.totalCell,
key === VALUE_KEY && styles.valueCell, key === valueKey && styles.valueCell,
]} ]}
onChangeText={text => onChangeText={text =>
updateCell(rowIndex, key, text) updateCell(rowIndex, key, text)
@@ -237,19 +477,26 @@ const EnvelopeCExcelPreview = (tenderData: any) => {
</View> </View>
</ScrollView> </ScrollView>
{tenderData?.buttonEnable && (
<TouchableOpacity style={styles.button} onPress={saveUpdatedExcel}> <TouchableOpacity style={styles.button} onPress={saveUpdatedExcel}>
<Text style={styles.buttonText}>Save Updated Excel</Text> <Text style={styles.buttonText}>Save Updated Excel</Text>
</TouchableOpacity> </TouchableOpacity>
)}
</View> </View>
); );
}; };
export default EnvelopeCExcelPreview; export default EnvelopeCExcelPreview;
/* -------------------- STYLES -------------------- */
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' }, container: { flex: 1, backgroundColor: '#fff' },
loader: { flex: 1, justifyContent: 'center', alignItems: 'center' }, loader: { flex: 1, justifyContent: 'center', alignItems: 'center' },
title: { fontSize: 18, fontWeight: '700', marginBottom: 10 },
row: { flexDirection: 'row' }, row: { flexDirection: 'row' },
headerCell: { headerCell: {
minWidth: 140, minWidth: 140,
@@ -272,7 +519,7 @@ const styles = StyleSheet.create({
fontWeight: '700', fontWeight: '700',
}, },
button: { button: {
marginTop: 15, margin: 15,
backgroundColor: colors.primary, backgroundColor: colors.primary,
padding: 14, padding: 14,
borderRadius: 8, borderRadius: 8,
+194
View File
@@ -0,0 +1,194 @@
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { InsideHeader } from '../../Component/Index';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { useNavigation, useRoute } from '@react-navigation/native';
import { AlertCircle, CheckCircle } from 'lucide-react-native';
import DeviceInfo from 'react-native-device-info';
import { apiCall } from '../../Axios/Axios';
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
import Geolocation from 'react-native-geolocation-service';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Platform } from 'react-native';
import { clampRGBA } from 'react-native-reanimated/lib/typescript/Colors';
const DeviceBinding = () => {
const { colors, spacing, typography } = useTheme();
const navigation = useNavigation<any>();
const route = useRoute<any>();
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const { message, showBindButton } = route.params || {
message: "Device is not mapped or not active. Please register and activate your device.",
showBindButton: false
};
const getLocation = (): Promise<{ latitude: string, longitude: string }> => {
return new Promise(async (resolve) => {
try {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
const result = await request(permission);
if (result === RESULTS.GRANTED) {
Geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude.toString(),
longitude: position.coords.longitude.toString(),
});
},
(error) => {
console.log("Location Error: ", error);
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback to standard
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
} else {
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback
}
} catch (err) {
console.log("Permission Error: ", err);
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback
}
});
};
const handleBindDevice = async () => {
setLoading(true);
try {
const deviceId = await DeviceInfo.getUniqueId();
const systemName = DeviceInfo.getSystemName();
const systemVersion = DeviceInfo.getSystemVersion();
const deviceOs = `${systemName} ${systemVersion}`;
const lastIp = await DeviceInfo.getIpAddress();
const brand = DeviceInfo.getBrand();
const model = DeviceInfo.getModel();
const deviceName = `${brand} ${model}`;
const browser = "Mobile App";
const { latitude, longitude } = await getLocation();
const location = `${longitude},${latitude}`;
const payload = {
browser,
deviceOs,
deviceId,
lastIp,
deviceName,
location,
};
const response: any = await apiCall('post', `${BASE_URL}tenderBidding/deviceBinding`, payload);
if (response?.status === 'SUCCESS' || response?.statusCode === 200) {
setSuccess(true);
// Optionally navigate back after a delay
setTimeout(() => {
navigation.goBack();
}, 2000);
}
} catch (error) {
console.log("Error binding device: ", error);
} finally {
setLoading(false);
}
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
},
iconContainer: {
marginBottom: spacing.l,
},
title: {
marginBottom: spacing.m,
textAlign: 'center',
},
message: {
textAlign: 'center',
marginBottom: spacing.xl,
lineHeight: 24,
},
button: {
backgroundColor: colors.primary,
paddingVertical: spacing.m,
paddingHorizontal: spacing.xl,
borderRadius: 12,
width: '100%',
alignItems: 'center',
marginBottom: spacing.m,
},
secondaryButton: {
backgroundColor: 'transparent',
paddingVertical: spacing.m,
width: '100%',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
}
});
return (
<View style={styles.container}>
<InsideHeader title="Device Binding" showArrow />
<View style={styles.content}>
<View style={styles.iconContainer}>
{success ? (
<CheckCircle size={64} color={colors.success} />
) : (
<AlertCircle size={64} color={colors.error} />
)}
</View>
<AppText variant="h3" color={colors.text} style={styles.title}>
{success ? "Binding Successful" : "Device Not Bound"}
</AppText>
<AppText variant="body" color={colors.textSecondary} style={styles.message}>
{success ? "Your device binding request has been submitted successfully." : message}
</AppText>
{showBindButton && !success && (
<TouchableOpacity
style={styles.button}
onPress={handleBindDevice}
disabled={loading}
>
{loading ? (
<ActivityIndicator size="small" color={colors.buttonText} />
) : (
<AppText variant="button" color={colors.buttonText}>
Bind This Device
</AppText>
)}
</TouchableOpacity>
)}
<TouchableOpacity
style={success ? styles.button : styles.secondaryButton}
onPress={() => navigation.goBack()}
>
<AppText variant="button" color={success ? colors.buttonText : colors.text}>
{success ? "Finish" : "Go Back"}
</AppText>
</TouchableOpacity>
</View>
</View>
);
};
export default DeviceBinding;
+79
View File
@@ -0,0 +1,79 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { InsideHeader } from '../../Component/Index';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { useNavigation, useRoute } from '@react-navigation/native';
import { AlertCircle } from 'lucide-react-native';
const DscBinding = () => {
const { colors, spacing, typography } = useTheme();
const navigation = useNavigation<any>();
const route = useRoute<any>();
// Fallback message just in case
const message = route.params?.message || "DSC is not mapped. Please map your DSC on the web portal before proceeding.";
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
},
iconContainer: {
marginBottom: spacing.l,
},
title: {
marginBottom: spacing.m,
textAlign: 'center',
},
message: {
textAlign: 'center',
marginBottom: spacing.xl,
lineHeight: 24,
},
button: {
backgroundColor: colors.primary,
paddingVertical: spacing.m,
paddingHorizontal: spacing.xl,
borderRadius: 12,
width: '100%',
alignItems: 'center',
}
});
return (
<View style={styles.container}>
<InsideHeader title="DSC Binding" showArrow />
<View style={styles.content}>
<View style={styles.iconContainer}>
<AlertCircle size={64} color={colors.error} />
</View>
<AppText variant="h3" color={colors.text} style={styles.title}>
DSC Not Bound
</AppText>
<AppText variant="body" color={colors.textSecondary} style={styles.message}>
{message}
</AppText>
<TouchableOpacity
style={styles.button}
onPress={() => navigation.goBack()}
>
<AppText variant="button" color={colors.buttonText}>
Go Back
</AppText>
</TouchableOpacity>
</View>
</View>
);
};
export default DscBinding;
+34 -15
View File
@@ -1,23 +1,22 @@
import React, { useCallback, useState, useEffect } from 'react'; import React, { useCallback, useState, useEffect } from 'react';
import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native'; import { View, FlatList, ActivityIndicator, RefreshControl, Text, StyleSheet } from 'react-native';
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useFetchLiveAuction, useFetchLiveTender } from '../../Services/BBPS/Hooks'; import { useFetchLiveAuction, useFetchLiveTender } from '../../Services/ApiCall/Hooks';
import { SearchInput } from '../../Component/Serach/SeacrhInput'; import { SearchInput } from '../../Component/Serach/SeacrhInput';
import { Skelton } from '../../Component/Index'; import { Skelton } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender'; import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
import LiveTenderTile from './LiveTenderTile'; import LiveTenderTile from './LiveTenderTile';
import { useTheme } from '../../Hooks/useTheme'; // Using Theme
const LiveTender = () => { const LiveTender = () => {
const navigation: any = useNavigation(); const navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch()
const { colors } = useTheme(); // Hook
const user = useSelector((state: any) => state?.token.token); const user = useSelector((state: any) => state?.token.token);
console.log("userdata",user)
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchLiveTender(); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchLiveTender();
const [showRefreshBanner, setShowRefreshBanner] = useState(false); const [showRefreshBanner, setShowRefreshBanner] = useState(false);
useEffect(() => { useEffect(() => {
@@ -49,6 +48,7 @@ const LiveTender = () => {
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: any) => { ({ item, index }: any) => {
if (item.tenderStatus === "Cancelled") return null;
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.tenderId === item.tenderId) const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.tenderId === item.tenderId)
return <LiveTenderTile return <LiveTenderTile
contractorId={user?.id} contractorId={user?.id}
@@ -69,13 +69,20 @@ const LiveTender = () => {
); );
if (isLoading) { if (isLoading) {
return <Skelton />; return <View style={{ flex: 1, backgroundColor: colors.background }}>
<Skelton />
</View>;
} }
console.log("data live : ", data )
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<SearchInput onChange={() => { }} placeholder="Search...." /> {/* Liquid Background Blobs */}
<View style={styles.blobContainer}>
<View style={[styles.blob, { top: -50, right: -50, backgroundColor: colors.primary, opacity: 0.05 }]} />
<View style={[styles.blob, { top: 200, left: -100, backgroundColor: colors.secondary, opacity: 0.05 }]} />
<View style={[styles.blob, { bottom: -50, right: -20, backgroundColor: colors.primary, opacity: 0.05 }]} />
</View>
<SearchInput onChange={() => { }} placeholder="Search Tenders..." filter={false} />
{showRefreshBanner && ( {showRefreshBanner && (
<Animated.View <Animated.View
@@ -90,7 +97,7 @@ const LiveTender = () => {
paddingHorizontal: 14, paddingHorizontal: 14,
marginTop: 6, marginTop: 6,
borderRadius: 20, borderRadius: 20,
backgroundColor: '#FFFFFF', backgroundColor: colors.surface,
shadowColor: '#000', shadowColor: '#000',
shadowOpacity: 0.06, shadowOpacity: 0.06,
shadowRadius: 8, shadowRadius: 8,
@@ -105,7 +112,7 @@ const LiveTender = () => {
<Text <Text
style={{ style={{
fontSize: 13, fontSize: 13,
color: '#374151', color: colors.text,
fontWeight: '500', fontWeight: '500',
}} }}
> >
@@ -127,7 +134,7 @@ const LiveTender = () => {
<RefreshControl <RefreshControl
refreshing={isRefetching} refreshing={isRefetching}
onRefresh={refetch} onRefresh={refetch}
tintColor="transparent" tintColor={colors.primary}
colors={[colors.primary]} colors={[colors.primary]}
/> />
} }
@@ -136,11 +143,23 @@ const LiveTender = () => {
windowSize={7} windowSize={7}
removeClippedSubviews removeClippedSubviews
updateCellsBatchingPeriod={50} updateCellsBatchingPeriod={50}
onRefresh={refetch}
refreshing={isRefetching}
/> />
</View> </View>
); );
}; };
const styles = StyleSheet.create({
blobContainer: {
...StyleSheet.absoluteFillObject,
zIndex: -1,
overflow: 'hidden',
},
blob: {
position: 'absolute',
width: 300,
height: 300,
borderRadius: 150,
},
});
export default LiveTender; export default LiveTender;

Some files were not shown because too many files have changed in this diff Show More