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
# screenshots whenever they are needed.
# 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/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 { 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 { AlertNotificationRoot } from 'react-native-alert-notification';
import { PersistGate } from 'redux-persist/integration/react';
@@ -11,21 +11,24 @@ import { NavigationContainer } from '@react-navigation/native';
import colors from './src/Constant/Color';
import { TextEncoder } from 'text-encoding';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import AppStatusBar from './src/components/AppStatusBar';
LogBox.ignoreAllLogs(true);
import { navigationRef } from './src/Navigation/NavigationService';
const App = () => {
const queryClient = new QueryClient();
const insets = useSafeAreaInsets()
global.TextEncoder = TextEncoder;
return (
<NavigationContainer >
<NavigationContainer ref={navigationRef}>
<QueryClientProvider client={queryClient}>
<GestureHandlerRootView style={{ flex: 1 }}>
<AlertNotificationRoot>
<Provider store={store}>
<PersistGate persistor={persistor}>
<View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}>
<StatusBar barStyle="dark-content" />
<AppStatusBar />
<Routes />
</View>
</PersistGate>
@@ -38,3 +41,4 @@ const 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"
# 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
> **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
@@ -48,7 +48,7 @@ Then, and every time you update your native dependencies, run:
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
# 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!
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:
@@ -79,19 +79,19 @@ You've successfully run and modified your React Native App. :partying_face:
### 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're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started).
- 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](http://reactnative.dev/docs/getting-started).
# 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
To learn more about React Native, take a look at the following resources:
- [React Native Website](https://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.
- [Learn the Basics](https://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.
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
- [React Native Website](http://reactnative.dev) - learn more about React Native.
- [Getting Started](http://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
- [Learn the Basics](http://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
- [Blog](http://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
- [`@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
build.scans.enabled=false
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")
//
// 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 = []
/* Hermes Commands */
@@ -99,7 +99,7 @@ android {
}
release {
// 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
minifyEnabled enableProguardInReleaseBuilds
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_VIDEO" />
<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
android:name=".MainApplication"
@@ -8,14 +8,12 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
* Returns the name of the main component registered from JavaScript.
*/
override fun getMainComponentName(): String = "procure"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
* Returns the instance of the [ReactActivityDelegate].
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
@@ -22,13 +22,13 @@
<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.
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"/>
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:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
+3 -3
View File
@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# 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.
# 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
# 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
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./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.
# 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 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
# distributed under the License is distributed on an "AS IS" BASIS,
@@ -57,10 +57,10 @@
# Darwin, MinGW, and NonStop.
#
# (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.
#
# 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
# shellcheck disable=SC2034
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
# 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 obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@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|
# 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(
installer,
config[:reactNativePath],
+1 -1
View File
@@ -32,7 +32,7 @@
<true/>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string/>
<string>We need your location to securely bind this device for tender bidding verification.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
+18 -2
View File
@@ -1,3 +1,4 @@
const path = require("path");
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
const defaultConfig = getDefaultConfig(__dirname);
@@ -5,7 +6,7 @@ const { assetExts, sourceExts } = defaultConfig.resolver;
/**
* Metro configuration
* https://reactnative.dev/docs/metro
* http://reactnative.dev/docs/metro
*
* @type {import('metro-config').MetroConfig}
*/
@@ -17,7 +18,22 @@ const config = {
},
resolver: {
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",
"lint": "eslint .",
"start": "react-native start",
"test": "jest"
"test": "jest",
"create:users": "node scripts/create-users.js"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/blur": "^4.4.1",
"@react-native-documents/picker": "^11.0.3",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-native-vector-icons/material-icons": "^12.4.0",
"@react-navigation/bottom-tabs": "^7.9.0",
"@react-navigation/native": "^7.1.26",
"@react-navigation/stack": "^7.6.13",
"@reduxjs/toolkit": "^2.11.2",
"@stomp/stompjs": "^7.2.1",
"@tanstack/react-query": "^5.90.12",
"axios": "^1.13.2",
"buffer": "^6.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-native": "^5.0.0",
"exceljs": "^4.4.0",
"formik": "^2.4.9",
"lottie-react-native": "^7.3.4",
"lucide-react-native": "^0.562.0",
"moment": "^2.30.1",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/blur": "4.4.1",
"@react-native-documents/picker": "11.0.3",
"@react-native-masked-view/masked-view": "0.2.9",
"@react-native-vector-icons/material-icons": "12.4.0",
"@react-navigation/bottom-tabs": "7.9.0",
"@react-navigation/drawer": "^7.8.1",
"@react-navigation/native": "^7.1.28",
"@react-navigation/stack": "7.6.13",
"@reduxjs/toolkit": "2.11.2",
"@stomp/stompjs": "7.2.1",
"@tanstack/react-query": "5.90.12",
"axios": "1.13.2",
"buffer": "6.0.3",
"crypto-js": "^4.2.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-native": "5.0.0",
"exceljs": "4.4.0",
"formik": "2.4.9",
"lottie-react-native": "7.3.4",
"lucide-react-native": "0.562.0",
"moment": "2.30.1",
"react": "19.0.0",
"react-native": "0.79.0",
"react-native-alert-notification": "^0.4.2",
"react-native-app-intro-slider": "^4.0.4",
"react-native-blob-util": "^0.24.6",
"react-native-bouncy-checkbox": "^4.1.2",
"react-native-date-picker": "^5.0.13",
"react-native-element-dropdown": "^2.12.4",
"react-native-fs": "^2.20.0",
"react-native-alert-notification": "0.4.2",
"react-native-app-intro-slider": "4.0.4",
"react-native-blob-util": "0.24.6",
"react-native-bouncy-checkbox": "4.1.2",
"react-native-date-picker": "5.0.13",
"react-native-device-info": "^15.0.2",
"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-gifted-charts": "^1.4.70",
"react-native-haptic-feedback": "^2.3.3",
"react-native-get-random-values": "1.11.0",
"react-native-gifted-charts": "1.4.70",
"react-native-haptic-feedback": "2.3.3",
"react-native-linear-gradient": "^2.8.3",
"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-responsive-screen": "^1.4.2",
"react-native-safe-area-context": "^5.6.2",
"react-native-screens": "^4.12.0",
"react-native-skeleton-placeholder": "^5.2.4",
"react-native-svg": "^15.12.0",
"react-native-svg-transformer": "^1.5.1",
"react-native-webview": "^13.16.0",
"react-redux": "^9.2.0",
"redux-persist": "^6.0.0",
"sockjs-client": "^1.6.1",
"text-encoding": "^0.7.0",
"react-native-responsive-screen": "1.4.2",
"react-native-safe-area-context": "5.6.2",
"react-native-screens": "4.12.0",
"react-native-skeleton-placeholder": "5.2.4",
"react-native-svg": "15.12.0",
"react-native-svg-transformer": "1.5.1",
"react-native-webview": "13.16.0",
"react-redux": "9.2.0",
"redux-persist": "6.0.0",
"sockjs-client": "1.6.1",
"text-encoding": "0.7.0",
"uuid": "13.0.0",
"xlsx": "^0.18.5",
"yup": "^1.7.1"
"yup": "1.7.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "^18.0.1",
"@react-native-community/cli-platform-android": "18.0.0",
"@babel/core": "7.25.2",
"@babel/preset-env": "7.25.3",
"@babel/runtime": "7.25.0",
"@react-native-community/cli": "18.0.1",
"@react-native-community/cli-platform-android": "^18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0",
"@react-native/babel-preset": "0.79.0",
"@react-native/eslint-config": "0.79.0",
"@react-native/metro-config": "0.79.0",
"@react-native/typescript-config": "0.79.0",
"@types/jest": "^29.5.13",
"@types/react": "^19.0.0",
"@types/react-test-renderer": "^19.0.0",
"@types/sockjs-client": "^1.5.4",
"@types/text-encoding": "^0.0.40",
"eslint": "^8.19.0",
"jest": "^29.6.3",
"@types/crypto-js": "^4.2.2",
"@types/jest": "29.5.13",
"@types/react": "19.0.0",
"@types/react-test-renderer": "19.0.0",
"@types/sockjs-client": "1.5.4",
"@types/text-encoding": "0.0.40",
"eslint": "^8.57.0",
"jest": "29.6.3",
"jsencrypt": "^3.5.4",
"prettier": "2.8.8",
"react-native-bundle-visualizer": "^3.1.3",
"react-native-bundle-visualizer": "3.1.3",
"react-test-renderer": "19.0.0",
"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 { setToken } from '../Redux/Slices/Token';
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 = {}>(
method: 'get' | 'post' | 'patch' | 'delete' | 'put',
@@ -9,9 +13,28 @@ export const apiCall = async <R, D = {}>(
params?: any,
contentType?: string
): 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({
method,
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;
} catch (error: any) {
const errorMessage = error?.response?.data?.error;
// console.log(error?.response?.data.error, '---------------------------api error message');
// ===== ERROR =====
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({}));
console.groupEnd(); // 🔚 MAIN GROUP
return {
success: false,
message: 'JWT Error - Token expired or malformed',
} as R;
}
const data = {
data: {
success: false,
message: error?.response?.data?.message || 'No Response From Server',
}
}
return data as any
console.groupEnd(); // 🔚 MAIN GROUP
return {
success: false,
message:
error?.response?.data?.message || 'No Response From Server',
...(error?.response?.data || {})
} as R;
}
};
+1 -1
View File
@@ -10,7 +10,7 @@ interface CountdownTimerProps {
size?: number
}
const CountdownTimer: React.FC<CountdownTimerProps> = React.memo(({ endDate, myStyle = {}, size = 20 }) => {
const CountdownTimer: React.FC<CountdownTimerProps> = React.memo(({ endDate, myStyle = {}, size = 20 }) => {
const calculateTimeLeft = useCallback(() => {
if (!endDate) return null;
+76 -51
View File
@@ -1,37 +1,8 @@
import React from 'react';
import { StyleSheet, Text, View, StyleProp, ViewStyle, TextStyle } from 'react-native';
import { Dropdown } from 'react-native-element-dropdown';
import { Shield } from 'lucide-react-native';
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',
};
import { Shield, ChevronDown } from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
type DropdownItem = {
[key: string]: any;
@@ -74,9 +45,22 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
dropdownStyle,
...rest
}) => {
const { colors, spacing, typography, shadows } = useTheme();
return (
<View style={[{ zIndex: 4000 }, containerStyle]}>
{label && <Text style={styles.label}>{label}</Text>}
<View style={[{ zIndex: 4000, marginBottom: spacing.m }, containerStyle]}>
{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
data={data}
labelField={labelField}
@@ -86,23 +70,70 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
onChange={onChange}
style={[
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,
{ borderColor: focusedField ? colors.primary : '#e0e0e0' },
]}
containerStyle={[styles.dropdownContainer, dropdownStyle]}
selectedTextStyle={[styles.selectedText, selectedTextStyle]}
placeholderStyle={[styles.placeholderText, placeholderStyle]}
containerStyle={[
styles.dropdownContainer,
{
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}
onBlur={onBlur}
search
searchPlaceholder="Search Class"
searchPlaceholder="Search..."
inputSearchStyle={{
borderRadius: 8,
backgroundColor: colors.background,
borderColor: colors.border,
color: colors.text
}}
renderLeftIcon={() => (
<Shield
style={{ marginRight: 10 }}
color={focusedField ? colors.primary : '#aaa'}
color={focusedField ? colors.primary : colors.warmGray}
size={20}
/>
)}
renderRightIcon={() => (
<ChevronDown
size={20}
color={focusedField ? colors.primary : colors.warmGray}
/>
)}
{...rest}
/>
</View>
@@ -113,25 +144,19 @@ export default MyDropdown;
const styles = StyleSheet.create({
label: {
marginVertical: 10,
color: colors.darkText,
// marginVertical: 10, handled inline
},
dropdown: {
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 12,
borderColor: colors.border,
backgroundColor: colors.white,
// handled inline with theme
},
dropdownContainer: {
borderRadius: 12,
borderColor: colors.border,
// handled inline
marginTop: 4,
},
selectedText: {
color: colors.darkText,
// handled inline
},
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 { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ArrowLeft } from 'lucide-react-native';
import colors from '../../Constant/Color';
import { useTheme } from '../../Hooks/useTheme';
import ImagePath from '../../Constant/ImagePath';
import { useNavigation } from '@react-navigation/native';
import FontsFamily from '../../Constant/FontsFamily';
import NavigationString from '../../Constant/NavigationString';
const { width } = Dimensions.get('window');
@@ -18,6 +17,7 @@ interface HeaderProps {
const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
const navigation: any = useNavigation();
const insets = useSafeAreaInsets();
const { colors, spacing, typography, shadows } = useTheme();
return (
<View
@@ -25,6 +25,8 @@ const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
styles.container,
{
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}
activeOpacity={0.7}
>
<ArrowLeft size={26} color={colors.black} strokeWidth={2.5} />
<ArrowLeft size={24} color={colors.primary} strokeWidth={2.5} />
</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>
{/* Optional: Add Logo or Right Action */}
<TouchableOpacity onPress={() => { navigation.navigate(NavigationString.Home) }}>
<Image
source={ImagePath.Icon.Logo2}
@@ -60,25 +70,21 @@ export default InsideHeader;
const styles = StyleSheet.create({
container: {
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,
paddingBottom: 10,
},
row: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 10,
paddingHorizontal: 16,
},
leftRow: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
backButton: {
@@ -88,16 +94,16 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
// backgroundColor: 'rgba(0,0,0,0.03)' // Optional touch target bg
},
title: {
fontSize: 20,
color: colors.black,
fontFamily: FontsFamily.poppinsSemiBold,
flex: 1,
},
logo: {
height: 60,
width: 60,
height: 48, // Slightly smaller for cleaner look
width: 48,
opacity: 0.9,
},
});
@@ -1,5 +1,5 @@
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 LinearGradient from 'react-native-linear-gradient';
import { AlertCircle, Clock, RefreshCw, Activity } from 'lucide-react-native';
@@ -10,7 +10,6 @@ import FontsFamily from '../../Constant/FontsFamily';
const ServerDown = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<ScrollView
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}>
+9 -17
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 LinearGradient from 'react-native-linear-gradient';
import colors from '../../Constant/Color';
@@ -7,33 +7,25 @@ import colors from '../../Constant/Color';
type PrimaryButtonProps = {
title: string;
onPress: () => void | any;
style?: ViewStyle;
style?: ViewStyle;
disabled?: boolean;
};
const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, disabled }) => {
return (
<LinearGradient
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]}
<View
style={[styles.primaryButton, style]}
>
<TouchableOpacity
<TouchableOpacity
style={styles.touchable}
onPress={onPress}
onPress={onPress}
activeOpacity={0.82}
disabled={disabled}
>
<Text style={styles.buttonText}>{title}</Text>
</TouchableOpacity>
</LinearGradient>
</View>
);
};
@@ -46,14 +38,14 @@ const styles = StyleSheet.create({
paddingHorizontal: 28,
marginBottom: 20,
minHeight: 54,
// Orange-themed shadows
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.3,
shadowRadius: 12,
elevation: 10,
overflow: 'hidden',
},
touchable: {
+39 -34
View File
@@ -7,7 +7,7 @@ import Animated, {
withTiming,
interpolateColor,
} from "react-native-reanimated";
import colors from "../../Constant/Color";
import { useTheme } from "../../Hooks/useTheme";
export interface SearchType {
placeholder?: string;
@@ -15,7 +15,7 @@ export interface SearchType {
label?: string;
filter?: boolean;
filterPress?: () => void;
debounceDelay?: number; // 👈 optional
debounceDelay?: number;
}
const AnimatedView = Animated.createAnimatedComponent(View);
@@ -26,8 +26,9 @@ export const SearchInput: React.FC<SearchType> = ({
label,
filter = true,
filterPress,
debounceDelay = 500, // 👈 default debounce
debounceDelay = 500,
}) => {
const { colors, shadows } = useTheme();
const isFocused = useSharedValue(0);
const [text, setText] = useState('');
const debounceRef = useRef<NodeJS.Timeout | null>(null);
@@ -36,18 +37,17 @@ export const SearchInput: React.FC<SearchType> = ({
const borderColor = interpolateColor(
isFocused.value,
[0, 1],
[colors.grayText + '50', colors.grayText]
[colors.overlay, colors.primary] // Use theme overlay/primary
);
return {
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) }],
shadowOpacity: withTiming(isFocused.value ? 0.1 : 0),
};
});
// 🔥 Debounce logic
// Debounce logic
useEffect(() => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
@@ -66,34 +66,47 @@ export const SearchInput: React.FC<SearchType> = ({
return (
<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}>
<AnimatedView style={[styles.inputWrapper, inputContainerStyle]}>
<AnimatedView style={[
styles.inputWrapper,
{
backgroundColor: colors.glass,
...shadows.light // Use theme shadow
},
inputContainerStyle
]}>
<Search
size={18}
color={colors.grayText}
color={colors.primary} // Theme primary
style={styles.searchIcon}
/>
<TextInput
style={styles.input}
style={[styles.input, { color: colors.text }]}
placeholder={placeholder}
value={text}
onChangeText={setText} // 👈 update local state only
placeholderTextColor={colors.grayText}
onChangeText={setText}
placeholderTextColor={colors.textSecondary}
onFocus={() => (isFocused.value = withTiming(1))}
onBlur={() => (isFocused.value = withTiming(0))}
selectionColor={colors.grayText}
selectionColor={colors.primary}
/>
</AnimatedView>
{filter && (
<TouchableOpacity
activeOpacity={0.7}
style={styles.filterButton}
style={[
styles.filterButton,
{
backgroundColor: colors.surface,
borderColor: colors.overlay
}
]}
onPress={filterPress}
>
<Filter size={22} color={colors.black} />
<Filter size={20} color={colors.primary} />
</TouchableOpacity>
)}
</View>
@@ -110,7 +123,6 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
color: colors.black,
letterSpacing: 0.3,
},
row: {
@@ -122,39 +134,32 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'row',
alignItems: 'center',
height: 48,
backgroundColor: '#F9FAFB',
borderRadius: 12,
paddingHorizontal: 12,
height: 50, // Slightly taller pill
borderRadius: 25, // Full Pill
paddingHorizontal: 16,
borderWidth: 1,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowRadius: 4,
elevation: 2,
},
searchIcon: {
marginRight: 8,
marginRight: 10,
},
input: {
flex: 1,
height: '100%',
fontSize: 15,
color: colors.black,
fontWeight: '500',
paddingVertical: Platform.OS === 'ios' ? 0 : 8,
},
filterButton: {
height: 48,
width: 48,
backgroundColor: '#FFFFFF',
height: 50,
width: 50,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 12,
borderRadius: 25, // Circle
borderWidth: 1,
borderColor: colors.grayText + '30',
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 2,
shadowRadius: 4,
},
});
-1
View File
@@ -73,7 +73,6 @@ const SideDrawer: React.FC<Props> = ({ visible, onClose }) => {
{/* ================= Header / Profile ================= */}
<View style={styles.profileHeader}>
{/* <TouchableOpacity style={styles.closeBtn} onPress={() => {
console.log('sdfadsfdsf');
}}>
<X size={20} color="#111827" />
</TouchableOpacity> */}
+99 -23
View File
@@ -1,34 +1,110 @@
import { View, Dimensions } from 'react-native';
import React from 'react';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
import React, { useEffect, useRef } from 'react';
import { View, Animated, StyleSheet, Dimensions } from 'react-native';
import { useTheme } from '../../Hooks/useTheme';
const Skeleton = () => {
const { width } = Dimensions.get('screen');
const cardWidth = width - 30; // full width with some padding
// 1. Create a Reusable "Bone" Component
const SkeletonBone = ({ width, height, style, borderRadius = 8 }) => {
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 (
<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) => (
<SkeletonPlaceholder key={index}>
<SkeletonPlaceholder.Item
width={cardWidth}
marginBottom={20}
borderRadius={12}
overflow="hidden"
>
{/* Image */}
<SkeletonPlaceholder.Item width={cardWidth} height={150} borderRadius={12} />
<View
key={index}
style={{
marginBottom: 16,
marginHorizontal: 16,
borderRadius: 20,
backgroundColor: colors.surface,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
}}
>
{/* Header Row: Tag + Price */}
<View style={styles.row}>
<SkeletonBone width={80} height={24} borderRadius={12} />
<SkeletonBone width={60} height={24} borderRadius={4} />
</View>
{/* Title */}
<SkeletonPlaceholder.Item marginTop={10} width={cardWidth * 0.7} height={18} borderRadius={6} />
{/* Title (2 lines) */}
<View style={styles.section}>
<SkeletonBone width="90%" height={18} borderRadius={4} style={{ marginBottom: 6 }} />
<SkeletonBone width="60%" height={18} borderRadius={4} />
</View>
{/* Subtitle / Price */}
<SkeletonPlaceholder.Item marginTop={6} width={cardWidth * 0.5} height={14} borderRadius={6} />
</SkeletonPlaceholder.Item>
</SkeletonPlaceholder>
{/* Info Grid (2 cols) */}
<View style={styles.row}>
<SkeletonBone width="45%" height={40} borderRadius={8} />
<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>
);
};
export default Skeleton;
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
section: {
marginBottom: 16,
},
});
export default Skeleton;
+117 -79
View File
@@ -1,12 +1,14 @@
import React, { useRef, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Animated,
} from 'react-native';
import colors from '../Constant/Color';
import React, { useMemo } from 'react';
import { View, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
import { useTheme } from '../Hooks/useTheme';
import AppText from '../components/AppText';
import { Check } from 'lucide-react-native';
type StepProgressBarProps = {
currentStep: number;
@@ -14,96 +16,132 @@ type StepProgressBarProps = {
onStepPress?: (stepIndex: number) => void;
};
const STEP_LABELS = ['Info', 'Register', 'KYC', 'Address', 'Docs'];
const StepProgressBar: React.FC<StepProgressBarProps> = ({
currentStep,
steps,
onStepPress = () => {},
onStepPress = () => { },
}) => {
const animations = useRef<Animated.Value[]>(
steps.map(() => new Animated.Value(1))
).current;
useEffect(() => {
animations.forEach((anim, index) => {
Animated.timing(anim, {
toValue: index === currentStep ? 1.2 : 1,
duration: 300,
useNativeDriver: true,
}).start();
});
}, [currentStep]);
const { colors, spacing, typography, shadows } = useTheme();
const styles = useMemo(() => makeStyles(colors, spacing), [colors, spacing]);
const totalSteps = steps.length;
return (
<View style={styles.container}>
{steps.map((label, index) => {
const isActive = index === currentStep;
const isCompleted = index < currentStep;
{/* 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>
return (
<TouchableOpacity
key={index}
onPress={() => onStepPress(index)}
style={styles.stepContainer}
activeOpacity={0.8}
>
<Animated.View
style={[
styles.circle,
isCompleted && styles.completedCircle,
isActive && styles.activeCircle,
{
transform: [{ scale: animations[index] }],
},
]}
/>
<Text
style={[
styles.label,
isCompleted && { color: '#4caf50' },
isActive && { color: '#2196f3' },
]}
>
{label}
</Text>
</TouchableOpacity>
);
})}
{/* Step circles */}
<View style={styles.stepsRow}>
{steps.map((_, index) => {
const isActive = index === currentStep;
const isCompleted = index < currentStep;
return (
<View key={index} style={styles.stepItem}>
<View
style={[
styles.circle,
{
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 },
]}
>
{isCompleted ? (
<Check size={14} color={colors.buttonText} strokeWidth={3} />
) : (
<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>
);
};
export default StepProgressBar;
const styles = StyleSheet.create({
const makeStyles = (colors: any, spacing: any) => StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 20,
flexWrap: 'wrap',
justifyContent: 'center',
paddingVertical: spacing.m,
paddingHorizontal: spacing.s,
},
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',
width: 70,
width: 52,
},
circle: {
width: 50,
height: 5,
borderRadius: 25,
width: 28,
height: 28,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.primary,
},
completedCircle: {
backgroundColor: '#4caf50',
},
activeCircle: {
backgroundColor: '#2196f3',
},
label: {
// marginTop: 6,
// fontSize: 12,
textAlign: 'center',
color: '#555',
borderWidth: 2,
},
});
@@ -1,14 +1,20 @@
import React, { ReactNode } from 'react';
import { View, Text, Modal, ScrollView, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
import { CheckCircle, X } from 'lucide-react-native'; // Updated icons
import colors from '../../Constant/Color';
import React, { ReactNode, useMemo } from 'react';
import { View, Modal, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
import { CheckCircle, X, FileText, Shield, Users, Brain, Lock, RefreshCw, Scale } from 'lucide-react-native';
import Animated, { FadeInUp, FadeInDown, FadeIn } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
type ModalProps = {
visible: boolean;
onClose: () => void;
onChecked: () => void
}
onChecked: () => void;
};
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 (
<Modal
animationType="slide"
@@ -17,185 +23,282 @@ const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChe
onRequestClose={onClose}
>
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.header}>
<Text style={styles.title}>Terms & Conditions</Text>
<Text style={styles.subtitle}>Last updated: {new Date().toLocaleDateString()}</Text>
{/* ═══ Header ═══ */}
<Animated.View
entering={FadeInDown.duration(400).springify()}
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>
<TouchableOpacity
onPress={onClose}
style={styles.closeButton}
activeOpacity={0.7}
>
<X size={20} color={colors.buttonText} />
</TouchableOpacity>
</View>
</Animated.View>
<View style={styles.content}>
<Section title="1. Acceptance of Terms">
<Text style={styles.paragraph}>
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.
</Text>
</Section>
{/* ═══ Content ═══ */}
<ScrollView
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.
If you do not agree with any part of these terms, you must not use our services.
</AppText>
</Section>
<Section title="2. User Responsibilities">
<Text style={styles.paragraph}>
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
of any unauthorized use of your account.
</Text>
</Section>
<Section
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
for all activities that occur under your account. You agree to notify us immediately
of any unauthorized use of your account.
</AppText>
</Section>
<Section title="3. Intellectual Property">
<Text style={styles.paragraph}>
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
reproduce, distribute, or create derivative works without our express permission.
</Text>
</Section>
<Section
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
the property of our company and protected by intellectual property laws. You may not
reproduce, distribute, or create derivative works without our express permission.
</AppText>
</Section>
<Section title="4. Limitation of Liability">
<Text style={styles.paragraph}>
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
advised of the possibility of such damages.
</Text>
</Section>
<Section
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
damages resulting from the use or inability to use our services, even if we have been
advised of the possibility of such damages.
</AppText>
</Section>
<Section title="5. Privacy Policy">
<Text style={styles.paragraph}>
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
to understand our practices.
</Text>
</Section>
<Section
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
collect, use, and protect your personal information. Please review our Privacy Policy
to understand our practices.
</AppText>
</Section>
<Section title="6. Changes to Terms">
<Text style={styles.paragraph}>
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
after such modifications constitutes your acceptance of the new terms.
</Text>
</Section>
<Section
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
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.
</AppText>
</Section>
<Section title="7. Governing Law">
<Text style={styles.paragraph}>
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.
</Text>
</Section>
<Section
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
jurisdiction in which our company is registered, without regard to its conflict of law provisions.
</AppText>
</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.
</Text>
</View>
</AppText>
</Animated.View>
{/* Accept Button */}
<TouchableOpacity
{/* Spacer for button */}
<View style={{ height: 100 }} />
</ScrollView>
{/* ═══ Accept Button ═══ */}
<Animated.View
entering={FadeInDown.delay(300).springify()}
style={styles.acceptButtonWrapper}
>
<TouchableOpacity
style={styles.acceptButton}
onPress={() => {
onClose();
onChecked();
}}
activeOpacity={0.85}
>
<CheckCircle color={colors.white} size={28} />
<Text style={styles.acceptButtonText}>I Accept</Text>
<CheckCircle color={colors.buttonText} size={22} strokeWidth={2.5} />
<AppText variant="button" color={colors.buttonText} style={{ marginLeft: 10 }}>
I Accept
</AppText>
</TouchableOpacity>
</ScrollView>
</Animated.View>
</View>
</Modal>
);
};
// Reusable Section component
// ── Section sub-component ─────────────────────────────────────────────
type SectionProps = {
title: string;
icon: ReactNode;
children: ReactNode;
styles: any;
colors: any;
spacing: any;
index: number;
};
const Section: React.FC<SectionProps> = ({ title, children }) => (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{title}</Text>
const Section: React.FC<SectionProps> = ({ title, icon, children, styles, colors, spacing, index }) => (
<Animated.View
entering={FadeInUp.delay(100 + index * 60).springify()}
style={styles.section}
>
<View style={styles.sectionHeader}>
{icon}
<AppText variant="bodySmall" color={colors.text} style={styles.sectionTitle}>
{title}
</AppText>
</View>
{children}
</View>
</Animated.View>
);
const styles = StyleSheet.create({
export default TermsAndConditionsModal;
// ── Styles ────────────────────────────────────────────────────────────
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.lightGray, // Updated with your colors
},
scrollContainer: {
paddingBottom: 120, // Space for accept button
flexGrow: 1,
backgroundColor: colors.background,
},
header: {
backgroundColor: colors.primary, // Orange header
paddingVertical: 32,
paddingHorizontal: 24,
borderBottomLeftRadius: 24,
borderBottomRightRadius: 24,
marginBottom: 24,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.25,
shadowRadius: 12,
elevation: 8,
backgroundColor: colors.primary,
paddingTop: spacing.xl,
paddingBottom: spacing.l,
paddingHorizontal: spacing.l,
borderBottomLeftRadius: 20,
borderBottomRightRadius: 20,
},
title: {
fontSize: 28,
fontWeight: '800',
color: colors.white,
marginBottom: 4,
headerRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
},
subtitle: {
fontSize: 14,
color: 'rgba(255,255,255,0.85)',
closeButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255,255,255,0.2)',
justifyContent: 'center',
alignItems: 'center',
},
content: {
paddingHorizontal: 24,
scrollContainer: {
padding: spacing.l,
paddingBottom: 40,
},
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: {
fontSize: 20,
fontWeight: '700',
color: colors.darkText,
marginBottom: 12,
fontSize: typography.size.s,
},
paragraph: {
fontSize: 16,
lineHeight: 25,
color: colors.grayText,
},
footerText: {
fontSize: 15,
fontStyle: 'italic',
color: colors.lightText,
marginTop: 24,
textAlign: 'center',
lineHeight: 22,
},
acceptButton: {
footerText: {
fontStyle: 'italic',
textAlign: 'center',
lineHeight: 20,
marginTop: spacing.m,
},
acceptButtonWrapper: {
position: 'absolute',
bottom: 40,
left: 24,
right: 24,
bottom: 0,
left: 0,
right: 0,
padding: spacing.m,
paddingBottom: spacing.xl,
backgroundColor: colors.surface,
borderTopWidth: 1,
borderTopColor: colors.border,
},
acceptButton: {
backgroundColor: colors.primary,
borderRadius: 16,
borderRadius: 14,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 18,
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,
paddingVertical: 16,
},
});
export default TermsAndConditionsModal;
+7 -1
View File
@@ -6,6 +6,7 @@ import {
TouchableOpacity,
StyleSheet,
Pressable,
ActivityIndicator,
} from 'react-native';
import colors from '../Color';
@@ -17,6 +18,7 @@ type ConfirmDialogProps = {
cancelText?: string;
onConfirm: () => void;
onClose: () => void;
loading:boolean
};
const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
@@ -27,6 +29,7 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
cancelText = 'Cancel',
onConfirm,
onClose,
loading,
}) => {
return (
<Modal
@@ -59,7 +62,10 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
onClose();
}}
>
<Text style={styles.confirmText}>{confirmText}</Text>
{ !loading ?<Text style={styles.confirmText}>{confirmText}</Text>:<ActivityIndicator
color={colors.primary}
size={30}
/>}
</TouchableOpacity>
</View>
</Pressable>
+11 -4
View File
@@ -5,9 +5,12 @@ import Diamond from '../Images/diamond.svg'
import Hill from '../Images/Svg/Hill.svg'
const Logo = require('../Images/logo.png')
const Logo2 = require('../Images/logo2.png')
const Icon = {
Logo,
Logo2
Logo,
Logo2
}
const ImagePath = {
Icon,
@@ -15,7 +18,11 @@ const ImagePath = {
OnBoard2,
OnBoard3,
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;
+6 -2
View File
@@ -21,7 +21,11 @@ const NavigationString = {
LiveTenderDetail:'LiveTenderDetail',
AuctionDetail: 'AuctionDetail',
Profile:'Profile',
ViewTenderDetail:'ViewTenderDetail',
ResetPassword:'ResetPassword'
ResetPassword:'ResetPassword',
KycWebViewScreen:'KycWebViewScreen',
StageThreeScreen:'StageThree',
DecryptionScreen:'DecryptionScreen',
DeviceBinding: 'DeviceBinding',
DscBinding: 'DscBinding'
}
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

+171 -96
View File
@@ -1,124 +1,173 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Dimensions,
Animated,
Platform,
} from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import * as LucideIcons from 'lucide-react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
interpolate,
Extrapolation
} from 'react-native-reanimated';
import Screens from '../Screens';
import colors from '../Constant/Color';
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 TAB_BAR_HEIGHT = 60;
const TAB_BAR_HEIGHT = 70; // Slightly taller for premium feel
const TAB_BUTTON_WIDTH = width / 4;
const Tab = createBottomTabNavigator();
const TabButton = ({ route, isFocused, onPress }: any) => {
const icons: Record<string, { outline: any; filled: any }> = {
Home: { outline: "Home", filled: "Home" },
const { colors } = useTheme();
const icons: Record<string, { outline: keyof typeof LucideIcons; filled: keyof typeof LucideIcons }> = {
Home: { outline: "House", filled: "House" },
Auction: { outline: "Gavel", filled: "Gavel" },
Tender: { outline: "FileText", filled: "FileText" },
Profile: { outline: "User", filled: "User" },
};
const IconName = LucideIcons[isFocused ? icons[route.name].filled : icons[route.name].outline];
const scaleAnim = useRef(new Animated.Value(1)).current;
const translateYAnim = useRef(new Animated.Value(0)).current;
const opacityAnim = useRef(new Animated.Value(1)).current;
// Lucide icons don't always have 'filled' variants in the set, so we might use the same icon
// or rely on color/strokeWidth to indicate state.
const iconKey = icons[route.name]?.outline || "HelpCircle"; // Fallback
const IconName = LucideIcons[iconKey] as React.ElementType;
const scale = useSharedValue(0);
const translateY = useSharedValue(0);
useEffect(() => {
if (isFocused) {
Animated.parallel([
Animated.sequence([
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();
scale.value = withSpring(1, { damping: 12, stiffness: 100 });
translateY.value = withSpring(-4, { damping: 12, stiffness: 100 });
} else {
Animated.parallel([
Animated.spring(scaleAnim, {
toValue: 1,
friction: 5,
tension: 150,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0.6,
duration: 150,
useNativeDriver: true,
}),
]).start();
scale.value = withTiming(0, { duration: 200 });
translateY.value = withSpring(0, { damping: 12, stiffness: 100 });
}
}, [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 (
<TouchableOpacity
onPress={onPress}
style={[styles.tabButton, { width: TAB_BUTTON_WIDTH }]}
style={styles.tabButton}
activeOpacity={0.8}
>
<Animated.View
style={[
styles.iconContainer,
{
transform: [{ scale: scaleAnim }, { translateY: translateYAnim }],
opacity: opacityAnim,
},
]}
>
{IconName && (
<IconName
size={20}
color={isFocused ? colors.primary : '#888'}
strokeWidth={2}
/>
)}
</Animated.View>
<View style={styles.tabContent}>
<Animated.View style={animatedIconStyle}>
{IconName && (
<IconName
size={24}
color={isFocused ? colors.primary : colors.text}
strokeWidth={isFocused ? 2.5 : 2}
/>
)}
</Animated.View>
<Text
style={[
styles.label,
{
color: isFocused ? colors.primary : '#888',
fontWeight: isFocused ? '600' : '400',
},
]}
>
{route.name === 'BrandDay' ? 'Coupons' : route.name}
</Text>
{isFocused && (
<Animated.Text
style={[
styles.label,
animatedTextStyle,
{
color: colors.primary,
fontFamily: typography.fonts.medium,
fontWeight: undefined, // Remove fontWeight as we use fontFamily
},
]}
numberOfLines={1}
>
{route.name === 'BrandDay' ? 'Coupons' : route.name}
</Animated.Text>
)}
</View>
</TouchableOpacity>
);
};
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 (
<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) => {
const isFocused = state.index === index;
@@ -136,8 +185,9 @@ const CustomTabBar = ({ state, descriptors, navigation }: any) => {
};
export default function BottomTab() {
const { colors } = useTheme();
return (
<View style={styles.container}>
<View style={[styles.container, { backgroundColor: colors.background }]}>
<Tab.Navigator
tabBar={(props) => <CustomTabBar {...props} />}
screenOptions={{
@@ -148,9 +198,9 @@ export default function BottomTab() {
<Tab.Screen name="Home" component={Screens.Home} />
<Tab.Screen name={NavigationString.Auction} component={Screens.Auction} />
<Tab.Screen name={NavigationString.Tender} component={Screens.Tender} />
{/* <Tab.Screen name={'EditableExcel'} component={EditableExcel} /> */}
<Tab.Screen name={NavigationString.Profile} component={Screens.ProfileTab} />
{/* <Tab.Screen name={'EditableExcel'} component={EditableExcel} /> */}
<Tab.Screen name={NavigationString.Profile} component={Screens.ProfileTab} />
</Tab.Navigator>
</View>
);
@@ -159,29 +209,54 @@ export default function BottomTab() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
tabBar: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
flexDirection: 'row',
height: TAB_BAR_HEIGHT,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#eee',
elevation: 5,
height: 70,
borderRadius: 35,
elevation: 20, // stronger Android elevation
shadowColor: '#000', // iOS shadow color
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: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: '100%',
},
iconContainer: {
marginBottom: 4,
justifyContent: 'center',
tabContent: {
flexDirection: 'row',
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: {
fontSize: 11,
textAlign: 'center',
includeFontPadding: false,
fontSize: 11, // Slightly smaller text
textAlign: 'left',
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 colors from '../Constant/Color';
import BottomTab from './Bottom';
import Login from '../Screens/Login/Login';
export default function MainStack() {
const Stack = createStackNavigator();
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 (
<Stack.Navigator
screenOptions={{
cardStyle: { backgroundColor: colors.white },
...fadeTransition
}}
// ...
>
<Stack.Screen name={NavigationString.Splash} component={Screens.Splash} 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.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.PurchaseList} component={Screens.PurchaseList} 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.LiveTenderDetail} component={Screens.LiveTenderDetail} 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>
);
}
+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;
+2 -1
View File
@@ -16,5 +16,6 @@ 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;
+8 -6
View File
@@ -1,12 +1,13 @@
import {configureStore} from '@reduxjs/toolkit';
import {combineReducers} from 'redux';
import {persistStore, persistReducer} from 'redux-persist';
import { configureStore } from '@reduxjs/toolkit';
import { combineReducers } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {tokenSlice} from './Slices/Token';
import {auctionSlice} from './Slices/SelectedAuction';
import { tokenSlice } from './Slices/Token';
import { auctionSlice } from './Slices/SelectedAuction';
import selectPurchaseAuctionReducer from './Slices/selectPurchaseAuction'
import selectPurchaseTenderReducer from './Slices/SelectedPurchaseTender';
import { tenderSlice } from './Slices/SelectedTender';
import { themeSlice } from './Slices/ThemeSlice';
const rootReducer = combineReducers({
token: tokenSlice.reducer,
@@ -14,11 +15,12 @@ const rootReducer = combineReducers({
selectPurchaseAuction: selectPurchaseAuctionReducer,
selectPurchaseTender: selectPurchaseTenderReducer,
tender: tenderSlice.reducer,
theme: themeSlice.reducer,
});
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['token'],
whitelist: ['token', 'theme'],
};
const persistReducers = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
+219 -108
View File
@@ -1,26 +1,34 @@
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 { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/BBPS/Hooks';
import colors from '../../Constant/Color';
import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
import LiveAuction from './LiveAuction';
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 NavigationString from '../../Constant/NavigationString';
import FontsFamily from '../../Constant/FontsFamily';
import { useSelector } from 'react-redux';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const HORIZONTAL_PADDING = 32;
const SWITCH_INSETS = 2;
const TAB_HEIGHT = 44;
const SWITCH_INSETS = 3;
const TAB_HEIGHT = 48;
const Auction = () => {
const { colors, spacing, shadows } = useTheme();
const Navigation = useNavigation() as any;
const { data: liveAuctionData = {} }: any = useFetchLiveAuction();
const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction();
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const reduxToken = useSelector((state: any) => state.token?.token);
const liveCount = Array.isArray(liveAuctionData?.result)
? liveAuctionData?.pages?.[0]?.totalCount
@@ -30,15 +38,14 @@ const Auction = () => {
? purchaseAuctionData.data.length
: 0;
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('live');
const translateX = useRef(new Animated.Value(0)).current;
const TAB_WIDTH = useMemo(() => {
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) => {
setSelectedTab(tab);
Animated.spring(translateX, {
@@ -48,147 +55,251 @@ const Auction = () => {
}).start();
};
const cartCount = SelectedPurchaseList.length;
const styles = makeStyles(colors, spacing, shadows, TAB_WIDTH, TAB_HEIGHT);
return (
<View style={styles.container}>
<InsideHeader title="Auction" showArrow />
{/* Tab Row */}
<View style={styles.topRow}>
{/* Segmented Control */}
<View
style={[
styles.switchWrapper,
{
width: TAB_WIDTH * 2 + SWITCH_INSETS * 2,
},
{ width: TAB_WIDTH * 2 + SWITCH_INSETS * 2 },
]}
>
{/* Sliding pill */}
<Animated.View
pointerEvents="none"
style={[
styles.activeTab,
{
width: TAB_WIDTH,
transform: [{ translateX }],
},
{ width: TAB_WIDTH, transform: [{ translateX }] },
]}
/>
{/* Live Tab */}
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
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={[
styles.tabText,
selectedTab === 'live' && styles.activeText,
selectedTab === 'live' && (styles.activeTabText as any),
]}
>
Live ({liveCount})
</Text>
Live {liveCount ? `(${liveCount})` : ''}
</AppText>
</TouchableOpacity>
{/* Purchase Tab */}
<TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8}
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={[
styles.tabText,
selectedTab === 'purchase' && styles.activeText,
selectedTab === 'purchase' && (styles.activeTabText as any),
]}
>
Purchased ({purchaseCount})
</Text>
</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} />
Purchased {purchaseCount ? `(${purchaseCount})` : ''}
</AppText>
</TouchableOpacity>
</View>
</View>
{/* Content */}
{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>
);
};
export default Auction;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.white,
},
topRow: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
paddingHorizontal: 16,
},
switchWrapper: {
flexDirection: 'row',
marginVertical: 14,
backgroundColor: '#f1f5f9',
borderRadius: 18,
padding: 2,
height: TAB_HEIGHT,
},
tab: {
height: TAB_HEIGHT - 4,
alignItems: 'center',
justifyContent: 'center',
zIndex: 2,
},
tabText: {
fontSize: 13,
color: '#64748b',
lineHeight: 18,
fontFamily: FontsFamily.poppinsSemiBold,
},
activeText: {
color: colors.primary,
},
activeTab: {
position: 'absolute',
top: 2,
left: 2,
height: TAB_HEIGHT - 4,
backgroundColor: '#fff',
borderRadius: 16,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 6,
elevation: 3,
},
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any, TAB_WIDTH: number, TAB_HEIGHT: number) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
/* ── Tab Row ── */
topRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
},
switchWrapper: {
flexDirection: 'row',
backgroundColor: colors.surface,
borderRadius: 16,
padding: SWITCH_INSETS,
height: TAB_HEIGHT,
...shadows.light,
},
tab: {
height: TAB_HEIGHT - SWITCH_INSETS * 2,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 5,
zIndex: 2,
},
tabText: {
fontWeight: '600',
letterSpacing: 0.3,
} as any,
activeTabText: {
fontWeight: '700',
} as any,
activeTab: {
position: 'absolute',
top: SWITCH_INSETS,
left: SWITCH_INSETS,
height: TAB_HEIGHT - SWITCH_INSETS * 2,
backgroundColor: colors.background,
borderRadius: 13,
...shadows.medium,
},
/* ── 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,
});
+190 -164
View File
@@ -1,181 +1,207 @@
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 { RefreshCw } from 'lucide-react-native';
import LiveAuctionTile from './LiveAuctionTile';
import { useFetchLiveAuction } from '../../Services/BBPS/Hooks';
import { useFetchLiveAuction } from '../../Services/ApiCall/Hooks';
import { SearchInput } from '../../Component/Serach/SeacrhInput';
import { Skelton } from '../../Component/Index';
import colors from '../../Constant/Color';
import SkeltonList from '../../Component/Skelton/SkeltonList';
import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { apiCall } from '../../Axios/Axios';
import { GET_MY_AUCTION } from '../../Services/BBPS/ApiUrls';
import SkeltonList from '../../Component/Skelton/SkeltonList';
import Skeleton from '../../Component/Skelton/Skelton';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const LiveAuction = () => {
const Dispatch = useDispatch()
const onEndReachedCalledDuringMomentum = useRef(true);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const [showRefreshBanner, setShowRefreshBanner] = useState(false);
const [allBoolean, setAllBoolean] = useState({ liveAuctionData: [], isLoading: false })
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch, isPending }: any = useFetchLiveAuction();
const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const user = useSelector((state: any) => state.token?.token);
const onEndReachedCalledDuringMomentum = useRef(true);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const [showRefreshBanner, setShowRefreshBanner] = useState(false);
// My Methods
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isRefetching,
refetch,
isPending,
}: any = useFetchLiveAuction();
const getMyAuction = async () => {
try {
setAllBoolean(prev => ({ ...prev, isLoading: true }))
const res = await apiCall<any>('get', `${GET_MY_AUCTION}`,);
if (res?.statusCode === 200) {
const purchasedIds = new Set(res.data.map((item: any) => item.auctionId));
const filteredLive = data?.result.map((item: any) => ({ ...item, active: !purchasedIds.has(item.auctionId) }));
setAllBoolean(prev => ({ ...prev, liveAuctionData: filteredLive }));
}
} catch (error) {
console.error('getMyAuction error', error);
} finally {
setAllBoolean(prev => ({ ...prev, isLoading: false }));
}
};
const renderItem = useCallback(
({ item, index }: any) => {
const isPurchase = SelectedPurchaseList.findIndex(
(prev: any) => prev.auctionNumber === item.auctionNumber,
);
return (
<LiveAuctionTile
data={item}
index={index}
isPurchasedByContractor={item.contractorId?.includes(
String(user.contractorId),
)}
auctionId={item.auctionId}
auctionDispatchDate={item.auctionDispatchDate}
isPurchased={isPurchase !== -1}
purchaseEnd={item.purchaseEnd}
auctionOfWork={item.auctionOfWork}
auctionFees={item.auctionFees}
auctionNumber={item.auctionNumber}
onPurchasePress={() => {
console.log('[LiveAuction] purchase toggled:', item?.auctionNumber);
Dispatch(togglePurchaseAuction(item));
}}
onDetailsPress={() => {}}
/>
);
},
[SelectedPurchaseList],
);
const renderItem = useCallback(
({ item, index }: any) => {
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.auctionNumber === item.auctionNumber)
return <LiveAuctionTile
data={item}
index={index}
auctionId={item.auctionId}
auctionDispatchDate={item.auctionDispatchDate}
isPurchased={isPurchase !== -1 ? true : false}
purchaseEnd={item.purchaseEnd}
auctionOfWork={item.auctionOfWork}
auctionFees={item.auctionFees}
auctionNumber={item.auctionNumber}
onPurchasePress={() => { Dispatch(togglePurchaseAuction(item)) }}
onDetailsPress={() => { }}
/>
},
[SelectedPurchaseList]
);
const loadMore = useCallback(() => {
if (onEndReachedCalledDuringMomentum.current) return;
if (!hasNextPage || isFetchingNextPage) return;
fetchNextPage();
onEndReachedCalledDuringMomentum.current = true;
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const renderFooter = useCallback(() => {
if (!isFetchingNextPage) return null;
return (
<View style={{ paddingVertical: 12 }}>
<ActivityIndicator size="small" color={colors.primary} />
</View>
);
}, [isFetchingNextPage]);
useEffect(() => {
if (isRefetching) {
setShowRefreshBanner(true);
} else {
const timer = setTimeout(() => {
setShowRefreshBanner(false);
}, 1000);
return () => clearTimeout(timer);
}
}, [isRefetching]);
useEffect(() => {
if (data?.result?.length > 0) {
getMyAuction()
}
}, [data])
if (isLoading) {
return <Skelton />;
}
const loadMore = useCallback(() => {
if (onEndReachedCalledDuringMomentum.current) return;
if (!hasNextPage || isFetchingNextPage) return;
console.log('[LiveAuction] loadMore: fetching next page');
fetchNextPage();
onEndReachedCalledDuringMomentum.current = true;
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const renderFooter = useCallback(() => {
if (!isFetchingNextPage) return null;
return (
<View style={{ flex: 1 }}>
<SearchInput onChange={() => { }} placeholder="Search...." filter={false} />
{showRefreshBanner && (
<Animated.View
entering={FadeInDown.duration(200)}
exiting={FadeOutUp.duration(500)}
style={{
alignSelf: 'center',
flexDirection: 'row',
alignItems: 'center',
gap: 8,
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} />
<Text
style={{
fontSize: 13,
color: '#374151',
fontWeight: '500',
}}
>
Refreshing auctions
</Text>
</Animated.View>
)}
{
isPending || allBoolean.isLoading ? <Skeleton /> :
<FlatList
data={allBoolean.liveAuctionData ?? []}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 50, paddingTop: 10 }}
onEndReached={loadMore}
onEndReachedThreshold={0.1}
onMomentumScrollBegin={() => {
onEndReachedCalledDuringMomentum.current = false;
}}
ListFooterComponent={renderFooter}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
tintColor="transparent"
colors={[colors.primary]}
/>
}
initialNumToRender={6}
maxToRenderPerBatch={6}
windowSize={7}
removeClippedSubviews
/>
}
</View>
<View style={styles.footerLoader}>
<ActivityIndicator size="small" color={colors.primary} />
<AppText variant="caption" color={colors.textSecondary}>
Loading more…
</AppText>
</View>
);
}, [isFetchingNextPage, colors]);
useEffect(() => {
if (isRefetching) {
console.log('[LiveAuction] refetch started');
setShowRefreshBanner(true);
} else {
console.log('[LiveAuction] refetch completed');
const timer = setTimeout(() => setShowRefreshBanner(false), 1000);
return () => clearTimeout(timer);
}
}, [isRefetching]);
useEffect(() => {
console.log('[LiveAuction] data updated:', {
count: data?.result?.length ?? 0,
hasNextPage,
isFetchingNextPage,
isPending,
});
}, [data, hasNextPage, isFetchingNextPage, isPending]);
if (isLoading) {
return <Skelton />;
}
return (
<View style={styles.root}>
{/* Search */}
<SearchInput onChange={() => {}} placeholder="Search auctions…" filter={false} />
{/* Refresh Banner */}
{showRefreshBanner && (
<Animated.View
entering={FadeInDown.duration(200)}
exiting={FadeOutUp.duration(400)}
style={[
styles.refreshBanner,
{
backgroundColor: colors.surface,
...shadows.medium,
},
]}
>
<ActivityIndicator size="small" color={colors.primary} />
<AppText variant="caption" color={colors.text} style={styles.refreshText as any}>
Refreshing auctions…
</AppText>
</Animated.View>
)}
{/* List */}
{isPending ? (
<SkeltonList />
) : (
<FlatList
data={data?.result ?? []}
renderItem={renderItem}
keyExtractor={(item: any) => item?.auctionId?.toString()}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContent}
onEndReached={loadMore}
onEndReachedThreshold={0.1}
onMomentumScrollBegin={() => {
onEndReachedCalledDuringMomentum.current = false;
}}
ListFooterComponent={renderFooter}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
tintColor="transparent"
colors={[colors.primary]}
/>
}
initialNumToRender={6}
maxToRenderPerBatch={6}
windowSize={7}
removeClippedSubviews
/>
)}
</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,
},
});
+252 -217
View File
@@ -1,13 +1,26 @@
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 colors from '../../Constant/Color';
import { LiveAuctionTileProps } from './types/types';
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,
index = 0,
isPurchased = false,
onPurchasePress,
auctionDispatchDate,
@@ -15,230 +28,252 @@ const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({
purchaseEnd,
auctionFees,
auctionNumber,
isPurchasedByContractor,
}) => {
const { colors, spacing, shadows } = useTheme();
const [showDetail, setShowDetail] = React.useState(false);
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 (
<View style={styles.card}>
<AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
</View>
);
}
return (
<View style={styles.cardWrapper}>
{
!showDetail ?
<>
<View
style={[
styles.statusTag,
{ backgroundColor: isPurchased ? colors.error : purchaseEnded ? '#94a3b8' : '#4ade80' },
]}
<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>
<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}
/>
<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>
<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>
{/* Work Title */}
<Text style={styles.workTitle} numberOfLines={2}>
{auctionOfWork ?? 'General Auction Work'}
</Text>
{/* ── 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>
{/* 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)} />
}
</View>
{/* 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);
const styles = StyleSheet.create({
cardWrapper: {
marginVertical: 10,
marginHorizontal: 16,
backgroundColor: '#fff',
borderRadius: 24,
flexDirection: 'row',
overflow: 'hidden',
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
card: {
backgroundColor: colors.surface,
borderRadius: 18,
borderWidth: 1,
borderColor: colors.border,
marginVertical: 7,
padding: spacing.m,
...shadows.medium,
},
// Modern shadow
shadowColor: '#000',
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',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
badge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary + '25',
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 12,
gap: 6,
},
badgeText: {
fontSize: 12,
fontWeight: '700',
color: colors.primary,
},
priceText: {
fontSize: 20,
fontWeight: '800',
color: '#0f172a',
},
workTitle: {
fontSize: 16,
fontWeight: '600',
color: '#334155',
lineHeight: 24,
marginBottom: 16,
},
infoGrid: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#f0f4f8',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 16,
marginBottom: 18,
},
infoItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
infoTextGroup: {
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,
height: 46,
borderRadius: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
},
btnActive: {
backgroundColor: '#4ade80', // light green
},
btnPurchased: {
backgroundColor: colors.error,
borderWidth: 2,
borderColor: colors.error,
},
btnDisabled: {
backgroundColor: '#e2e8f0',
},
mainButtonText: {
fontSize: 14,
fontWeight: '800',
},
});
/* Top row */
topRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
},
numberRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
},
auctionNo: {
fontWeight: '700',
} as any,
fee: {
fontWeight: '800',
} as any,
/* Title */
title: {
fontWeight: '700',
lineHeight: 24,
marginBottom: spacing.m,
} as any,
/* Date strip */
dateStrip: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 12,
paddingVertical: spacing.s + 2,
paddingHorizontal: spacing.m,
marginBottom: spacing.m,
},
dateCol: {
flex: 1,
},
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',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
},
ctaText: {
fontWeight: '800',
letterSpacing: 0.8,
} as any,
});
+333 -282
View File
@@ -1,63 +1,66 @@
import React, { memo, useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, StatusBar, Modal } from 'react-native';
import { Briefcase, Calendar, FileText, Download, Eye, Layers, ArrowLeft, Clock } from 'lucide-react-native';
import { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/BBPS/Hooks';
import {
View,
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 { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/BBPS/ApiUrls';
import { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
import ReactNativeBlobUtil from 'react-native-blob-util';
import AuctionItemDetail from './AuctionItemDetail';
import { Loader } from '../../../Component/Index';
import colors from '../../../Constant/Color';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import moment from 'moment';
interface Props { visible: boolean; onClose: () => void; data?: any }
const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets()
const { data: auctionData, isLoading } = useGetAuciton({ auctionId: data?.auctionId })
const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData)
const [allBoolean, setAllBoolean] = useState({ showItemDetail: false, selectedItem: {}, fileLoading: false });
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId })
const { mutate } = useAuctionItem()
const inset = useSafeAreaInsets();
const { colors, spacing, shadows } = useTheme();
const { data: auctionData } = useGetAuciton({ auctionId: data?.auctionId });
const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData);
const [allBoolean, setAllBoolean] = useState({
showItemDetail: false,
selectedItem: {},
fileLoading: false,
});
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId });
const { mutate } = useAuctionItem();
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');
};
const isLive = data?.purchaseEnd ? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss')) : false;
const Section = ({ title, icon: Icon, children }: any) => (
<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 isLive = data?.purchaseEnd
? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss'))
: false;
const handleDownload = async (id: number, fileName: string) => {
try {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: true }));
setAllBoolean(prev => ({ ...prev, fileLoading: true }));
const { dirs } = ReactNativeBlobUtil.fs;
const path = `${dirs.DownloadDir}/${fileName}.pdf`;
// console.log('Saving to:', path);
const downloadUrl = `${AUCTION_DOCUMENT_DOWNLOAD}?documentId=${id}&isAmendment=false`;
// console.log('Download URL:', downloadUrl);
const response = await ReactNativeBlobUtil.config({
await ReactNativeBlobUtil.config({
fileCache: false,
addAndroidDownloads: {
useDownloadManager: true,
@@ -68,292 +71,340 @@ const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
description: 'Downloading document',
mediaScannable: true,
},
}).fetch('GET', downloadUrl, {
Accept: 'application/pdf',
});
const stat = await ReactNativeBlobUtil.fs.stat(path);
console.log('File size:', stat.size);
}).fetch('GET', downloadUrl, { Accept: 'application/pdf' });
} catch (error: any) {
console.log('Error:', error?.message || error);
} finally {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: false }));
setAllBoolean(prev => ({ ...prev, fileLoading: false }));
}
};
useEffect(() => {
mutate({ auctionNumber: data?.auctionNumber, status: data?.auctionPattern }, {
onSuccess: (res) => {
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments }))
}, onError: (err) => {
console.log(err, '-----auction item error');
}
});
mutate(
{ auctionNumber: data?.auctionNumber, status: data?.auctionPattern },
{
onSuccess: (res: any) => {
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments }));
},
onError: (err: any) => {
console.log(err, '-----auction item error');
},
},
);
}, [data.auctionNumber, auctionDocuments]);
const isAmended = auctionData?.auctionStatus === 'Amended';
const kd = (key: any, amended: any) => isAmended ? amended : key;
const styles = makeStyles(colors, spacing, shadows);
return (
<Modal
visible={visible}
transparent={false}
animationType="none"
animationType="slide"
statusBarTranslucent
onRequestClose={onClose}
>
<View style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
{/* Full Header */}
<View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" />
<View style={styles.root}>
{/* ── Header ── */}
<View style={[styles.header, { paddingTop: inset.top + 6 }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn} activeOpacity={0.7}>
<ArrowLeft size={22} color={colors.text} strokeWidth={2.5} />
</TouchableOpacity>
<View style={styles.headerTitleContainer}>
<Text style={styles.headerTitleText}>Auction Overview</Text>
<Text style={styles.headerIdText}>Ref: {data?.auctionNumber || 'N/A'}</Text>
<View style={styles.headerCenter}>
<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 style={styles.liveIndicator}>
<View style={[styles.dot, { backgroundColor: isLive ? '#22c55e' : '#94a3b8' }]} />
<Text style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</Text>
<View style={[styles.statusChip, { borderColor: isLive ? `${colors.success}50` : colors.border }]}>
<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>
{/* Scrollable Body */}
{/* ── Body ── */}
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
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 */}
<Section title="Auction Details" icon={Briefcase}>
<View style={styles.grid}>
<InfoBox label="Auction Name" value={auctionData?.auctionOfWork} />
<InfoBox label="Category Of Auction " value={auctionData?.auctionCategories} />
<InfoBox label="Auction Fees (In Figure) " value={auctionData?.auctionFees} />
<InfoBox label="Auction Fees (In Words)" value={auctionData?.auctionFeesInWord} />
<InfoBox label="Auction Keyword" value={auctionData?.auctionKeywordName} />
</View>
</Section>
{/* BASIC DETAILS */}
<SectionCard title="Basic Details" icon={Layers} colors={colors} spacing={spacing} shadows={shadows}>
<InfoRow label="Auction No." value={data?.auctionNumber} colors={colors} />
<InfoRow label="Pattern" value={data?.auctionPattern || 'Forward'} colors={colors} />
<InfoRow label="Mode" value={data?.auctionMode || 'Open'} colors={colors} />
<InfoRow label="Category" value={data?.categoryOfAuction || 'General'} colors={colors} />
<InfoRow label="EMD Type" value={data?.emd || 'Online'} colors={colors} last />
</SectionCard>
{/* 3. Timeline Section */}
<Section title="Key Dates" icon={Calendar}>
<View style={styles.timelineContainer}>
<View style={styles.timelineRow}>
<View style={styles.iconCircle}><Clock size={16} color="#64748b" /></View>
<View>
<Text style={styles.label}>Auction Purchase (Start)</Text>
<Text style={styles.value}>{formatDate(data?.auctionDispatchDate)}</Text>
</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>
{/* AUCTION DETAILS */}
<SectionCard title="Auction Details" icon={Briefcase} colors={colors} spacing={spacing} shadows={shadows}>
<InfoRow label="Name" value={auctionData?.auctionOfWork} colors={colors} multiline />
<InfoRow label="Category" value={auctionData?.auctionCategories} colors={colors} />
<InfoRow label="Fees" value={auctionData?.auctionFees ? `₹ ${auctionData.auctionFees}` : '—'} colors={colors} valueColor={colors.primary} />
<InfoRow label="Fees (Words)" value={auctionData?.auctionFeesInWord} colors={colors} multiline />
<InfoRow label="Keyword" value={auctionData?.auctionKeywordName} colors={colors} last />
</SectionCard>
{/* 4. Item Table Section */}
<Section title="Auction Items" icon={FileText}>
<View style={styles.tableWrapper}>
<View style={styles.tableHead}>
<Text style={[styles.tableLabel, { flex: 2 }]}>Item</Text>
<Text style={styles.tableLabel}>Time</Text>
<Text style={[styles.tableLabel, { textAlign: 'right' }]}>View</Text>
</View>
{/* Dummy Item Row */}
{aucitonDetail?.auctionItems?.map((item: any, index: number) => (
<View style={styles.tableRow}>
<View style={{ flex: 2 }}>
<Text style={styles.itemMain}>{item.productName}</Text>
<Text style={styles.itemSub}>Category: {item?.categoryName}</Text>
{/* KEY DATES */}
<SectionCard title="Key Dates" icon={Calendar} colors={colors} spacing={spacing} shadows={shadows}>
<InfoRow label="Purchase Start" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.startDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.startDateTime))} colors={colors} />
<InfoRow label="Purchase End" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.endDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.endDateTime))} colors={colors} valueColor={colors.error} />
<InfoRow label="Bidding Start" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.startDateTime, auctionData?.amendedkeyDates?.auctionBidding?.startDateTime))} colors={colors} />
<InfoRow label="Bidding End" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.endDateTime, auctionData?.amendedkeyDates?.auctionBidding?.endDateTime))} colors={colors} valueColor={colors.error} last />
</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>
<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
style={[styles.eyeBtn, { backgroundColor: `${colors.primary}15` }]}
onPress={() => setAllBoolean(prev => ({ ...prev, showItemDetail: true, selectedItem: item }))}
>
<Eye size={16} color={colors.primary} strokeWidth={2} />
</TouchableOpacity>
</View>
))}
</View>
</Section>
</SectionCard>
)}
{/* 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' />
{/* 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>
) : (
<Download size={20} color="#94a3b8" />
)}
</TouchableOpacity>
))}
</Section>
<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>
))}
</SectionCard>
)}
</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>
</Modal >
</Modal>
);
};
const styles = StyleSheet.create({
fullModal: {
margin: 0,
backgroundColor: '#fff',
},
safeArea: {
flex: 1,
backgroundColor: '#fff',
},
header: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
backgroundColor: '#fff',
},
backBtn: {
padding: 8,
marginRight: 8,
},
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' },
export default memo(AuctionFullDetail);
scrollView: { flex: 1, backgroundColor: '#f8fafc' },
scrollContent: { padding: 16, paddingBottom: 30 },
/* ─── Sub-components ─── */
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 },
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>
);
};
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 },
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,
},
]}>
<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>
);
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 },
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
root: {
flex: 1,
backgroundColor: colors.background,
},
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' },
/* Header */
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.m,
paddingBottom: spacing.m,
backgroundColor: colors.surface,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
},
backBtn: {
width: 36,
height: 36,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
headerCenter: {
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,
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',
justifyContent: 'center',
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
},
btnDisabled: { backgroundColor: '#cbd5e1' },
mainBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' },
});
scrollContent: {
padding: spacing.m,
paddingBottom: 50,
gap: spacing.m,
},
export default memo(AuctionFullDetail);
/* 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 { Package, IndianRupee, FileText, Download, ArrowLeft, Info, } from 'lucide-react-native';
import colors from '../../../Constant/Color';
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 { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/BBPS/ApiUrls';
import { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
import { Loader } from '../../../Component/Index';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
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.sectionHeader}>
<View style={styles.iconCircle}>
<Icon size={18} color={colors.primary} strokeWidth={2.5} />
</View>
<Text style={styles.sectionTitle}>{title}</Text>
<AppText variant="caption" bold style={styles.sectionTitle}>{title}</AppText>
</View>
{children}
</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%' }]}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</Text>
<Text style={styles.priceWords}>{words}</Text>
<AppText variant="caption" color={styles.label.color} bold style={styles.label}>{label}</AppText>
<AppText variant="h3" bold style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</AppText>
<AppText variant="caption" style={styles.priceWords}>{words}</AppText>
</View>
);
const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets()
const { colors, typography } = useTheme();
const styles = useMemo(() => createStyles(colors, typography), [colors, typography]);
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) => {
try {
@@ -41,10 +46,7 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const path = `${dirs.DownloadDir}/${fileName}.pdf`;
// console.log('Saving to:', path);
const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`;
// console.log('Download URL:', downloadUrl);
const response = await ReactNativeBlobUtil.config({
fileCache: false,
@@ -62,7 +64,6 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
});
const stat = await ReactNativeBlobUtil.fs.stat(path);
console.log(' File size:', stat.size);
} catch (error: any) {
console.log('Error:', error?.message || error);
@@ -84,11 +85,11 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
{/* Navbar */}
<View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" />
<ArrowLeft size={24} color={colors.text} />
</TouchableOpacity>
<View>
<Text style={styles.headerTitle}>Item Information</Text>
<Text style={styles.headerSub}>Detail View</Text>
<AppText variant="body" bold style={styles.headerTitle}>Item Information</AppText>
<AppText variant="caption" style={styles.headerSub}>Detail View</AppText>
</View>
</View>
@@ -97,69 +98,66 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<Section title="Product Specification" icon={Package}>
<Section title="Product Specification" icon={Package} styles={styles} colors={colors}>
<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}>
<Text style={styles.categoryText}>{data.categoryName}</Text>
<AppText variant="caption" bold style={styles.categoryText}>{data?.categoryName}</AppText>
</View>
</View>
<View style={styles.specGrid}>
<View style={styles.specItem}>
<Text style={styles.label}>Sub Category</Text>
<Text style={styles.specValue}>{data.subCategoryName}</Text>
<AppText variant="caption" bold style={styles.label}>Sub Category</AppText>
<AppText variant="bodySmall" bold style={styles.specValue}>{data?.subCategoryName}</AppText>
</View>
<View style={styles.specItem}>
<Text style={styles.label}>Duration</Text>
<Text style={styles.specValue}>{data.timeDurationType}</Text>
<AppText variant="caption" bold style={styles.label}>Duration</AppText>
<AppText variant="bodySmall" bold style={styles.specValue}>{data?.timeDurationType}</AppText>
</View>
</View>
</Section>
<Section title="Reserve & Bid Values" icon={IndianRupee}>
<Section title="Reserve & Bid Values" icon={IndianRupee} styles={styles} colors={colors}>
<PriceTile
label="Bidding Price"
fig={data.reservePrice || data.emdAmount}
words={data.reservePriceInWords || data.emdAmountInWords}
fig={data?.reservePrice || data?.emdAmount}
words={data?.reservePriceInWords || data?.emdAmountInWords}
full
styles={styles}
/>
<View style={styles.hDivider} />
<PriceTile
label="Auction Start Value"
fig={data.reservePrice || data.auctionStartValueFigure}
words={data.reservePriceInWords || data.auctionStartValueWords}
fig={data?.reservePrice || data?.auctionStartValueFigure}
words={data?.reservePriceInWords || data?.auctionStartValueWords}
full
styles={styles}
/>
<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 title="Brief Description" icon={Info}>
<Text style={styles.descContent}>{data.briefDescription}</Text>
<Section title="Brief Description" icon={Info} styles={styles} colors={colors}>
<AppText variant="bodySmall" style={styles.descContent}>{data?.briefDescription}</AppText>
</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}>
<Text style={styles.tableLabel}>Sr No.</Text>
<Text style={[styles.tableLabel, { flex: 2 }]}>Document Name</Text>
<Text style={styles.tableLabel}>Action</Text>
<AppText variant="caption" bold style={styles.tableLabel}>Sr No.</AppText>
<AppText variant="caption" bold style={[styles.tableLabel, { flex: 2 }]}>Document Name</AppText>
<AppText variant="caption" bold style={styles.tableLabel}>Action</AppText>
</View>
{itemData?.map((doc: any, index: any) => (
<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 }}>
<Text style={styles.docTitle} numberOfLines={1}>{doc.fileName}</Text>
<AppText variant="bodySmall" bold style={styles.docTitle} numberOfLines={1}>{doc.fileName}</AppText>
</View>
<View style={styles.downloadIconBox}>
{allBoolean.fileLoading ? (
<View >
<Loader size='small' color='#94a3b8' />
<Loader size='small' color={colors.textSecondary} />
</View>
) : (
<Download size={20} color={colors.primary} />
@@ -174,26 +172,26 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
);
};
const styles = StyleSheet.create({
fullModal: { margin: 0, backgroundColor: '#f8fafc' },
safeArea: { flex: 1 },
const createStyles = (colors: any, typography: any) => StyleSheet.create({
fullModal: { margin: 0, backgroundColor: colors.background },
safeArea: { flex: 1, backgroundColor: colors.background },
header: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
backgroundColor: '#fff',
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
borderBottomColor: colors.border,
},
backBtn: { padding: 8, marginRight: 8 },
headerTitle: { fontSize: 18, fontWeight: '800', color: '#0f172a' },
headerSub: { fontSize: 12, color: '#94a3b8', fontWeight: '600', textTransform: 'uppercase' },
headerTitle: { fontSize: 18, color: colors.text },
headerSub: { fontSize: 12, color: colors.textSecondary, textTransform: 'uppercase' },
scrollView: { flex: 1 },
scrollContent: { padding: 16, paddingBottom: 50 },
sectionCard: {
backgroundColor: '#fff',
backgroundColor: colors.surface,
borderRadius: 24,
padding: 20,
marginBottom: 16,
@@ -218,55 +216,54 @@ const styles = StyleSheet.create({
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: '#334155',
color: colors.text,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
// Product Identity
identityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
productNameText: { fontSize: 20, fontWeight: '900', color: '#1e293b', flex: 1 },
categoryBadge: { backgroundColor: '#f1f5f9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
categoryText: { fontSize: 11, fontWeight: '800', color: '#64748b', textTransform: 'uppercase' },
productNameText: { fontSize: 20, color: colors.text, flex: 1 },
categoryBadge: { backgroundColor: colors.border, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
categoryText: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase' },
specGrid: { flexDirection: 'row', gap: 24, marginTop: 5 },
specItem: { flex: 1 },
specValue: { fontSize: 15, fontWeight: '700', color: '#334155', marginTop: 2 },
specValue: { fontSize: 15, color: colors.text, marginTop: 2 },
// Pricing
priceTile: { marginBottom: 16 },
label: { fontSize: 11, color: '#94a3b8', fontWeight: '700', textTransform: 'uppercase', marginBottom: 4 },
priceFig: { fontSize: 22, fontWeight: '900', color: colors.black },
priceWords: { fontSize: 13, color: '#64748b', fontStyle: 'italic', marginTop: 2 },
hDivider: { height: 1, backgroundColor: '#f1f5f9', marginBottom: 16 },
label: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase', marginBottom: 4 },
priceFig: { fontSize: 22, color: colors.text },
priceWords: { fontSize: 13, color: colors.textSecondary, fontStyle: 'italic', marginTop: 2 },
hDivider: { height: 1, backgroundColor: colors.border, marginBottom: 16 },
row: { flexDirection: 'row', justifyContent: 'space-between' },
// Description
descContent: { fontSize: 15, color: '#475569', lineHeight: 22, fontWeight: '500' },
descContent: { fontSize: 15, color: colors.textSecondary, lineHeight: 22, fontFamily: typography.fonts.medium },
// Table
docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', marginBottom: 10 },
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 },
docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 },
docIndex: { flex: 1, fontSize: 14, fontWeight: '700', color: '#64748b' },
docTitle: { fontSize: 14, fontWeight: '700', color: '#1e293b' },
downloadIconBox: { backgroundColor: '#fff', padding: 8, borderRadius: 10, borderWidth: 1, borderColor: '#e2e8f0' },
docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: colors.border, marginBottom: 10 },
tableLabel: { fontSize: 11, color: colors.textSecondary, flex: 1 },
docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background, padding: 12, borderRadius: 12 },
docIndex: { flex: 1, fontSize: 14, color: colors.textSecondary },
docTitle: { fontSize: 14, color: colors.text },
downloadIconBox: { backgroundColor: colors.surface, padding: 8, borderRadius: 10, borderWidth: 1, borderColor: colors.border },
// Footer
footer: {
position: 'absolute',
bottom: 0, width: '100%',
backgroundColor: '#fff',
backgroundColor: colors.surface,
padding: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1, borderTopColor: '#f1f5f9'
borderTopWidth: 1, borderTopColor: colors.border
},
primaryBtn: {
backgroundColor: colors.primary,
height: 56, borderRadius: 18,
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);
@@ -25,10 +25,8 @@ const FilterModal: React.FC<Props> = ({ visible, onClose, setFilter, filter }) =
if (pickerType === 'FROM') {
setFromDate(date);
console.log('FROM date set');
} else {
setToDate(date);
console.log('TO date set');
}
setOpenPicker(false);
+3 -3
View File
@@ -3,7 +3,7 @@ import { View, Text, StyleSheet, FlatList, RefreshControl, ActivityIndicator } f
import LiveAuctionTile from './LiveAuctionTile';
import { useNavigation } from '@react-navigation/native';
import moment from 'moment';
import { useFetchLiveAuctionPage, useFetchPurchaseAuction } from '../../Services/BBPS/Hooks';
import { useFetchLiveAuctionPage, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
import PurchaseAuctionTile from './PurchaseAuctionTile';
import { useDispatch } from 'react-redux';
import { setAuction } from '../../Redux/Slices/SelectedAuction';
@@ -36,7 +36,7 @@ const PurchaseAuction = () => {
function isAuctionEnd(item: any): boolean {
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;
@@ -64,7 +64,7 @@ const PurchaseAuction = () => {
return (
<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 />
:
<FlatList
+443 -243
View File
@@ -1,291 +1,491 @@
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import { InsideHeader, Loader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native';
import React, { useState } from 'react';
import {
View,
FlatList,
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 { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/BBPS/Hooks';
import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/ApiCall/Hooks';
import { showSuccessAlert } from '../../Constant/ShowDailog';
import { clearPurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import Skeleton from '../../Component/Skelton/Skelton';
const dummyAuctionList = [
{
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,
},
];
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const PurchaseAuctionList = () => {
const Navigation = useNavigation()
const Dispatch = useDispatch()
const [loading, setLoading] = useState(false);
const [list, setList] = useState([]);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const LoginUser = useSelector((state: any) => state.token.token);
const { mutate, isPending } = useOfflineAuctionPurchase()
const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList();
const Navigation: any = useNavigation();
const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [loading, setLoading] = useState(false);
const [list, setList] = useState<any[]>([]);
const calculateTotalAmount = (item: any) => {
const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const LoginUser = useSelector((state: any) => state.token.token);
const payableAmount =
Number(item.auctionFees) + Number(item.portalCharge) + gstAmount;
return payableAmount
}
const { mutate, isPending } = useOfflineAuctionPurchase();
const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList();
const handleOfflinePurchase = async () => {
const body = {
auctionDetails: list.map((b: any) => ({
auctionDispatchDate: b?.auctionDispatchDate,
auctionDispatcherNumber: b?.auctionDispatcherNumber,
auctionFees: b?.auctionFees,
auctionId: b?.auctionId,
auctionNumber: b?.auctionNumber,
auctionPattern: b?.auctionPattern,
gst: b?.gst,
gstAmount: b?.gstAmount,
payableAmount: calculateTotalAmount(b),
portalCharge: b?.portalCharge
})),
orderNo: ''
};
const calculateTotalAmount = (item: any) => {
const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return Number(item.auctionFees) + Number(item.portalCharge) + gstAmount;
};
try {
mutate(body, {
onSuccess: (res) => {
if (res.statusCode == 200) {
showSuccessAlert(res.message)
Dispatch(clearPurchaseAuction())
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
}
},
onError: (error) => {
console.log(error, "================purchse Error");
}
})
const grandTotal = list.reduce(
(sum: number, item: any) => sum + calculateTotalAmount(item),
0,
);
} catch (error) {
console.log("purchase offline error", error);
}
}
const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true);
setTimeout(() => {
setLoading(false);
type === 'online' ? Alert.alert('Online Purchase Successful!') : handleOfflinePurchase();
}, 1200);
const handleOfflinePurchase = async () => {
const body = {
auctionDetails: list.map((b: any) => ({
auctionDispatchDate: b?.auctionDispatchDate,
auctionDispatcherNumber: b?.auctionDispatcherNumber,
auctionFees: b?.auctionFees,
auctionId: b?.auctionId,
auctionNumber: b?.auctionNumber,
auctionPattern: b?.auctionPattern,
gst: b?.gst,
gstAmount: b?.gstAmount,
payableAmount: calculateTotalAmount(b),
portalCharge: b?.portalCharge,
})),
orderNo: '',
};
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 auctionNumbers: string[] = SelectedPurchaseList.map(
(item: { auctionNumber: string }) => item.auctionNumber
);
purchaseListMutation({ auctionNumbers, contractorId: LoginUser.contractorId }, {
onSuccess: (res) => {
if (res.statusCode == 200) {
setList(res.data)
}
},
onError: (error) => {
console.log(error, "=================purchase list error");
}
})
try {
mutate(body, {
onSuccess: res => {
if (res.statusCode === 200) {
showSuccessAlert(res.message);
Dispatch(clearPurchaseAuction());
Navigation.dispatch(
CommonActions.reset({ index: 0, routes: [{ name: NavigationString.Home }] }),
);
}
},
onError: error => console.log(error, 'purchase Error'),
});
} catch (error) {
console.log('purchase offline error', error);
}
useFocusEffect(
React.useCallback(() => {
getPurchaseList()
}, [SelectedPurchaseList])
};
const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true);
setTimeout(() => {
setLoading(false);
type === 'online' ? undefined : handleOfflinePurchase();
}, 1200);
};
const getPurchaseList = () => {
const auctionNumbers: string[] = SelectedPurchaseList.map(
(item: { auctionNumber: string }) => item.auctionNumber,
);
purchaseListMutation(
{ auctionNumbers, contractorId: LoginUser.contractorId },
{
onSuccess: res => {
if (res.statusCode === 200) setList(res.data);
},
onError: error => console.log(error, 'purchase list error'),
},
);
};
useFocusEffect(
React.useCallback(() => {
getPurchaseList();
}, [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 (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<InsideHeader title="Purchase List" showArrow />
<View style={styles.screen}>
{purchaseListPending ? <Skeleton /> : <FlatList
data={list}
renderItem={renderItem}
keyExtractor={(item) => item.auctionId.toString()}
contentContainerStyle={styles.container}
/>}
<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={styles.bottomButtonContainer}>
<TouchableOpacity
style={[styles.button, { marginRight: 8 }]}
onPress={() => handlePurchase('online')}
>
<Text style={styles.buttonText}>Purchase Online</Text>
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, { marginLeft: 8 }]}
onPress={() => handlePurchase('offline')}
>
{
isPending ? <Loader size='small' color={colors.white} /> : <>
<Text style={styles.buttonText}>Purchase Offline</Text>
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
</>
}
</TouchableOpacity>
<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 (
<View style={styles.screen}>
<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}
renderItem={renderItem}
keyExtractor={item => item.auctionId?.toString()}
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
/>
)}
{/* Bottom Buttons */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.button, styles.onlineButton, { borderColor: colors.primary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('online')}
>
<Wifi size={18} color={colors.primary} />
<AppText variant="button" color={colors.primary} style={styles.btnText as any}>
Online
</AppText>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.offlineButton, { backgroundColor: colors.buttonPrimary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('offline')}
>
{isPending ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<WifiOff size={18} color={colors.buttonText} />
<AppText variant="button" color={colors.buttonText} style={styles.btnText as any}>
Offline
</AppText>
</>
)}
</TouchableOpacity>
</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>
);
};
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: {
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: {
padding: 16,
paddingBottom: 120,
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
paddingBottom: 120,
},
/* Card */
card: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 16,
marginBottom: 16,
elevation: 5,
shadowColor: '#000',
shadowOpacity: 0.12,
shadowOffset: { width: 0, height: 3 },
shadowRadius: 8,
backgroundColor: colors.surface,
borderRadius: 20,
marginBottom: spacing.m,
overflow: 'hidden',
...shadows.medium,
},
cardAccent: {
height: 4,
backgroundColor: colors.primary,
},
cardBody: {
padding: spacing.m,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: spacing.s,
},
title: {
fontSize: 16,
fontWeight: '700',
color: '#0F172A',
indexBadge: {
width: 32,
height: 32,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
marginTop: 2,
},
pattern: {
fontSize: 14,
color: colors.primary,
fontWeight: '600',
patternBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
alignSelf: 'flex-start',
paddingHorizontal: spacing.s,
paddingVertical: 2,
borderRadius: 8,
marginTop: 4,
},
cardRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginBottom: 4,
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginVertical: spacing.s,
},
text: {
fontSize: 14,
color: '#555',
feeSection: {
borderRadius: 12,
padding: spacing.s + 2,
marginBottom: spacing.s,
},
totalRow: {
marginTop: 10,
backgroundColor: colors.primary,
borderRadius: 12,
paddingVertical: 10,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 8,
feeHeader: {
letterSpacing: 1.2,
marginBottom: spacing.xs + 2,
fontWeight: '700',
} as any,
totalBanner: {
borderRadius: 14,
paddingVertical: 12,
paddingHorizontal: spacing.m,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
total: {
fontSize: 15,
fontWeight: '700',
color: '#fff',
totalLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
bottomButtonContainer: {
position: 'absolute',
bottom: 0,
left: 0,
flexDirection: 'row',
backgroundColor: colors.white,
width: '100%',
paddingVertical: 20,
paddingHorizontal: 10,
elevation: 5
/* Footer */
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flexDirection: 'row',
paddingHorizontal: spacing.m,
paddingTop: spacing.s + 4,
paddingBottom: spacing.m + 4,
backgroundColor: colors.surface,
gap: spacing.s,
...shadows.strong,
},
button: {
flex: 1,
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
borderRadius: 16,
gap: spacing.s,
},
buttonText: {
color: '#fff',
fontWeight: '700',
fontSize: 14,
onlineButton: {
backgroundColor: colors.surface,
borderWidth: 1.5,
},
offlineButton: {
...shadows.medium,
},
btnText: {
letterSpacing: 1.2,
} as any,
/* Loading overlay */
loadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.2)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
});
loadingCard: {
borderRadius: 20,
padding: spacing.l,
alignItems: 'center',
...shadows.strong,
},
});
+275 -240
View File
@@ -1,263 +1,298 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native';
import React, { useMemo } from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import moment from 'moment';
import CountdownTimer from '../../Component/Counter/CountdownTimer';
import colors from '../../Constant/Color';
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 scale = useSharedValue(1);
const elevation = useSharedValue(8);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
elevation: elevation.value, // Android-safe
};
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePressIn = () => {
scale.value = withTiming(0.97, { duration: 120 });
elevation.value = withTiming(3, { duration: 120 });
};
const handlePressIn = () => scale.value = withTiming(0.98, { duration: 140 });
const handlePressOut = () => scale.value = withTiming(1, { duration: 160 });
const handlePressOut = () => {
scale.value = withTiming(1, { duration: 160 });
elevation.value = withTiming(8, { duration: 160 });
};
const isAmended = item?.auctionStatus === 'Amended';
const biddingEnd = isAmended
? 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 (
<View style={{ flex: 1 }}>
{!showDetail ? <Animated.View
entering={FadeInUp.duration(400)}
style={[styles.shadowContainer, animatedStyle]}
<Animated.View
entering={FadeInUp.duration(450).springify()}
style={[styles.container, shadows.medium, animatedStyle]}
>
<TouchableOpacity
activeOpacity={1}
onPress={() => setShowDetail(true)}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={styles.card}
>
<TouchableOpacity
activeOpacity={1}
onPress={() => setShowDetail(true)}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={styles.cardInternal}
>
<View style={[styles.statusAccent, { backgroundColor: statusColor }]} />
<View style={styles.mainInfo}>
{/* Header */}
<View style={styles.headerRow}>
<View>
<View style={[styles.statusBadge, { backgroundColor: lightStatusBg }]}>
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
<Text style={[styles.statusText, { color: statusColor }]}>
{isAuctionEnd ? 'ENDED' : buttonEnable ? 'LIVE' : 'PENDING'}
</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>
{/* ── 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>
{/* Date */}
<View style={styles.dateRow}>
<Text style={styles.dateText}>
<Text style={styles.dateLabel}>Bidding Start: </Text>
{moment(
item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', ''),
'DD-MM-YYYY HH:mm:ss'
).format('DD MMM YYYY, hh:mm A')}
</Text>
<Text style={styles.dateText}>
<Text style={styles.dateLabel}>Bidding Ends: </Text>
{moment(
item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', ''),
'DD-MM-YYYY HH:mm:ss'
).format('DD MMM YYYY, hh:mm A')}
</Text>
</View>
{/* Footer */}
<View style={styles.footer}>
<View style={styles.footerTop}>
<Text style={styles.footerLabel}>Remaining Time</Text>
{buttonEnable && !isAuctionEnd ? (
<CountdownTimer
endDate={item.keyDates.auctionBidding.endDateTime}
/>
) : (
<Text style={styles.endedText}>--:--:--</Text>
)}
</View>
<TouchableOpacity
disabled={!buttonEnable}
onPress={onStartBidding}
activeOpacity={0.85}
style={[
styles.actionButton,
{ backgroundColor: statusColor },
!buttonEnable && styles.disabledButton,
]}
>
<Text style={styles.addText}>
{isAuctionEnd
? 'Bidding Closed'
: !buttonEnable
? 'Pending Bid'
: 'Proceed to Start Bidding'}
</Text>
</TouchableOpacity>
{/* 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>
</TouchableOpacity>
</Animated.View> :
<AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
}
</View>
{/* Work title */}
<AppText variant="bodySmall" color={colors.text} numberOfLines={2} style={styles.workTitle as any}>
{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>
<AppText variant="caption" color={colors.textSecondary}>Auction Fee</AppText>
<AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
₹{Number(item?.auctionFees ?? 0).toLocaleString('en-IN')}
</AppText>
</View>
</View>
{/* Bidding End */}
<View style={styles.infoRow}>
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<Calendar size={17} color={colors.primary} strokeWidth={2} />
</View>
<View>
<AppText variant="caption" color={colors.textSecondary}>Bidding Ends</AppText>
<AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
{biddingEnd
? moment(biddingEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')
: '—'}
</AppText>
</View>
</View>
</View>
{/* ── Footer ── */}
<View style={styles.footer}>
<AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' } as any}>
Time Remaining
</AppText>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
{/* Timer */}
<View style={styles.timerSection}>
{buttonEnable && !isAuctionEnd ? (
<CountdownTimer endDate={biddingEnd} />
) : (
<AppText variant="bodySmall" color={colors.textSecondary} style={{ fontWeight: '700' } as any}>
-- : -- : --
</AppText>
)}
</View>
{/* CTA Button */}
<TouchableOpacity
disabled={!buttonEnable || isAuctionEnd}
onPress={onStartBidding}
activeOpacity={0.85}
style={[
styles.ctaBtn,
{
backgroundColor:
isAuctionEnd
? colors.error
: buttonEnable
? colors.primary
: colors.border,
opacity: (!buttonEnable || isAuctionEnd) ? 0.8 : 1,
},
]}
>
<AppText
variant="button"
color={colors.buttonText}
style={styles.ctaText as any}
>
{isAuctionEnd ? 'Ended' : !buttonEnable ? 'Pending' : 'Bid Now'}
</AppText>
{buttonEnable && !isAuctionEnd && (
<ArrowRight size={15} color={colors.buttonText} strokeWidth={2.5} />
)}
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
</Animated.View>
);
};
export default PurchaseAuctionTile;
const styles = StyleSheet.create({
shadowContainer: {
marginHorizontal: 5,
marginTop: 14,
marginBottom: 6,
borderRadius: 16,
backgroundColor: '#fff',
elevation: 8,
},
cardInternal: {
borderRadius: 16,
overflow: 'hidden',
flexDirection: 'row',
backgroundColor: '#fff',
},
statusAccent: {
width: 5,
height: '100%',
},
mainInfo: {
flex: 1,
padding: 16,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
},
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 6,
marginBottom: 6,
alignSelf: 'flex-start',
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 5,
},
statusText: {
fontSize: 10,
fontWeight: '800',
letterSpacing: 0.5,
},
title: {
fontWeight: '800',
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',
},
dateLabel: {
fontWeight: '400',
color: '#64748B',
},
footer: {
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
paddingTop: 12,
},
footerTop: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
footerLabel: {
fontSize: 11,
fontWeight: '600',
color: '#94A3B8',
textTransform: 'uppercase',
},
endedText: {
fontSize: 13,
fontWeight: '700',
color: '#CBD5E1',
},
actionButton: {
width: '100%',
height: 48,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
}),
},
disabledButton: {
backgroundColor: '#E2E8F0',
},
addText: {
color: '#fff',
fontSize: 14,
fontWeight: '800',
letterSpacing: 0.2,
},
});
const makeStyles = (colors: any, spacing: any) =>
StyleSheet.create({
container: {
marginHorizontal: spacing.s,
marginBottom: spacing.m,
borderRadius: 16,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.surface,
},
card: {
borderRadius: 16,
backgroundColor: colors.surface,
padding: spacing.m,
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.03)',
},
/* Header */
header: {
marginBottom: spacing.m,
},
headerTop: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
},
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
gap: 5,
},
statusText: {
fontWeight: '800',
fontSize: 10,
textTransform: 'uppercase',
letterSpacing: 0.5,
} as any,
numberRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
numberText: {
letterSpacing: 0.4,
fontWeight: '600',
} as any,
workTitle: {
fontWeight: '600',
lineHeight: 20,
} as any,
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginBottom: spacing.m,
opacity: 0.6,
},
/* Info grid */
infoGrid: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: spacing.l,
gap: spacing.s,
},
infoRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flex: 1,
},
iconBox: {
width: 36,
height: 36,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
infoValue: {
fontWeight: '700',
} as any,
/* Footer */
footer: {
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: colors.background,
marginHorizontal: -spacing.m,
marginBottom: -spacing.m,
padding: spacing.m,
borderBottomLeftRadius: 16,
borderBottomRightRadius: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
},
timerSection: {
flex: 1,
},
ctaBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
paddingHorizontal: 18,
borderRadius: 12,
gap: 5,
},
ctaText: {
letterSpacing: 0.8,
} as any,
});
+344 -177
View File
@@ -1,220 +1,387 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import Animated, { FadeIn, FadeOut, Layout, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { ChevronDown, Trash2, Info } from 'lucide-react-native';
import React, { useEffect, useState } from 'react';
import {
View,
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 colors from '../../Constant/Color';
import { useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const PurchaseList = () => {
const Navigation: any = useNavigation()
const Dispatch = useDispatch()
const Navigation: any = useNavigation();
const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const toggleExpand = (index: number) => {
setExpandedIndex(prev => (prev === index ? null : index));
};
useEffect(() => {
if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) {
Navigation.goBack();
}
}, [SelectedPurchaseList]);
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={styles.screen}>
<InsideHeader title="Purchase" showArrow />
<InsideHeader title="Purchase Auctions" showArrow />
<ScrollView contentContainerStyle={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.heading}>Purchase List</Text>
{/* <Info size={20} color="#0F172A" /> */}
{/* Summary Strip */}
<View style={styles.summaryStrip}>
<View style={styles.summaryIcon}>
<ShoppingBag size={16} color={colors.primary} />
</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>
{/* Cards */}
{SelectedPurchaseList.map((item: any, index: any) => {
const expanded = expandedIndex === index;
return (
<Animated.View
key={item.auctionId}
layout={Layout.springify()}
style={styles.card}
>
<TouchableOpacity
activeOpacity={0.9}
onPress={() => toggleExpand(index)}
>
<CardHeader
title={item.auctionOfWork}
expanded={expanded}
/>
<ScrollView
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
>
{SelectedPurchaseList.map((item: any, index: number) => (
<PurchaseListItem
key={item.auctionId}
item={item}
index={index}
expandedIndex={expandedIndex}
toggleExpand={toggleExpand}
colors={colors}
styles={styles}
Dispatch={Dispatch}
/>
))}
<Text style={styles.subTitle}>
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>
);
})}
<View style={{ height: 90 }} />
</ScrollView>
{/* Footer */}
<TouchableOpacity disabled={Boolean(!SelectedPurchaseList.length)} style={[styles.proceedButton, { backgroundColor: Boolean(!SelectedPurchaseList.length) ? colors.lightText : colors.primary }]} onPress={() => { Navigation.navigate(NavigationString.PurchaseAuctionList) }}>
<Text style={styles.proceedText}>Proceed to Purchase</Text>
</TouchableOpacity>
{/* Floating Proceed Button */}
<View style={styles.footerContainer}>
<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>
</View>
</View>
);
};
export default PurchaseList;
const CardHeader = ({ title, expanded }: {
title: string;
expanded: boolean;
}) => {
const rotation = useSharedValue(expanded ? 1 : 0);
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
const arrowStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
/* ─── Sub-components ─── */
const PurchaseListItem = ({ item, index, expandedIndex, toggleExpand, colors, styles, Dispatch }: any) => {
const expanded = expandedIndex === index;
const isActive = item.status;
const chevronStyle = useChevronStyle(expanded);
return (
<View style={styles.cardHeader}>
<Text style={styles.title}>{title}</Text>
<Animated.View style={arrowStyle}>
<ChevronDown size={22} color={colors.black} />
</Animated.View>
</View>
<Animated.View
entering={FadeInDown.delay(index * 80).springify()}
layout={Layout.springify()}
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>
</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) => (
<View style={styles.row}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value}</Text>
const DetailRow = ({
label,
value,
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>
);
const styles = StyleSheet.create({
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: {
const detailRowStyles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 16,
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,
paddingVertical: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.06)',
},
label: {
fontSize: 13,
color: '#64748B',
},
flex: 1,
fontWeight: '500',
} as any,
value: {
fontSize: 15,
flex: 1,
textAlign: 'right',
fontWeight: '600',
color: '#0F172A',
},
removeButton: {
marginTop: 18,
backgroundColor: colors.primary,
borderRadius: 14,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'center',
gap: 8,
},
removeText: {
color: '#fff',
fontWeight: '700',
},
proceedButton: {
position: 'absolute',
bottom: 16,
left: 16,
right: 16,
backgroundColor: colors.primary,
paddingVertical: 16,
borderRadius: 18,
alignItems: 'center',
},
proceedText: {
color: '#fff',
fontSize: 16,
fontWeight: '800',
},
} 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: {
marginTop: spacing.m,
backgroundColor: colors.error,
borderRadius: 14,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: spacing.s,
},
removeText: {
letterSpacing: 1,
} as any,
footerContainer: {
position: 'absolute',
bottom: 20,
left: spacing.m,
right: spacing.m,
},
proceedButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 16,
borderRadius: 20,
gap: spacing.s,
...shadows.strong,
},
proceedText: {
letterSpacing: 1.5,
} as any,
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;
auctionId?: any;
auctionNumber?: any;
isPurchasedByContractor?: boolean
}
export interface allBooleanProps {
+50 -66
View File
@@ -1,9 +1,11 @@
import React, { memo, useCallback, useRef, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native';
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 MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import { Loader } from '../../../Component/Index';
import { useSelector } from 'react-redux';
import axios from 'axios';
@@ -11,6 +13,8 @@ import Variables from '../../../Constant/Variable';
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
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 [showDetail, setShowDetail] = useState(false);
const [value, setValue] = useState(`${item.bidAmount}`);
@@ -93,7 +97,6 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
setAllState((prev: any) => ({ ...prev, isLoading: false }))
}
}
// console.log(item, 'jkjjj');
return (
<View>
@@ -101,7 +104,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
!showDetail ? <Animated.View
layout={Layout.springify()}
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 */}
<TouchableOpacity
@@ -110,28 +113,28 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
style={styles.cardHeader}
>
<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 */}
<Text style={styles.itemIdText}>
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
Initial Amt : {item?.auctionStartValueFigure}
</Text>
</AppText>
</View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
<View style={[MainStyle.flexBetween, { gap: 10 }]}>
<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'}
</Text>
</AppText>
</View>
<Animated.Text style={[styles.arrow, arrowStyle]}>
<Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
▼
</Animated.Text>
</View>
<Text style={styles.itemIdText}>
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
{SelectedAuction?.auctionNumber}
</Text>
</AppText>
</TouchableOpacity>
</TouchableOpacity>
@@ -139,15 +142,15 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
{/* PRICE INFO */}
<View style={styles.priceRow}>
<View>
<Text style={styles.label}>Current Bid</Text>
<Text style={styles.mainPrice}>₹{item?.bidAmount || item.auctionStartValueFigure}</Text>
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item?.bidAmount || item.auctionStartValueFigure}</AppText>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Increment</Text>
<Text style={styles.incrementText}>
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Increment</AppText>
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
+₹{item.bidVariationValue}
</Text>
</AppText>
</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]}>
<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}>
<TouchableOpacity
@@ -163,29 +166,29 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
disabled={isMin}
style={[styles.stepBtn, isMin && styles.disabledBtn]}
>
<Text style={styles.stepText}>−</Text>
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
</TouchableOpacity>
<View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text>
<Text style={styles.bidValueText}>{bidValue}</Text>
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
</View>
<TouchableOpacity onPress={onIncrease} style={styles.stepBtn}>
<Text style={styles.stepText}>+</Text>
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
</TouchableOpacity>
</View>}
{
(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
value={value}
onChangeText={handleChange}
keyboardType="decimal-pad"
placeholder="0.00"
style={styles.input}
placeholderTextColor="#94A3B8"
style={[styles.input, { color: colors.text }]}
placeholderTextColor={colors.textSecondary}
/>
</View>
}
@@ -195,7 +198,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
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}
</Animated.View>
@@ -210,11 +213,11 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
export default memo(BiddingCard);
const styles = StyleSheet.create({
const makeStyles = (colors: any) => StyleSheet.create({
inputBox2: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F1F5F9',
backgroundColor: colors.background,
borderRadius: 12,
height: 45,
paddingHorizontal: 12,
@@ -222,39 +225,23 @@ const styles = StyleSheet.create({
marginBottom: 20
},
currency2: {
fontSize: 16,
fontWeight: 'bold',
color: '#64748B',
marginRight: 4,
},
} as any,
input: {
flex: 1,
fontSize: 18,
fontWeight: '700',
color: '#0F172A',
width: '100%'
},
card: {
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',
@@ -262,27 +249,24 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
},
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
productName: {} as any,
itemIdText: { marginTop: 2 } as any,
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' },
liveBadge: { backgroundColor: `${colors.error}20` },
meBadge: { backgroundColor: `${colors.success}20` },
badgeText: { fontSize: 10 } as any,
liveText: { color: colors.error },
meText: { color: colors.success },
arrow: { fontSize: 10 },
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' },
} as any,
mainPrice: {} as any,
incrementText: {} as any,
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
bidLabel: { marginBottom: 10 } as any,
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: {
width: 45,
@@ -292,19 +276,19 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
disabledBtn: { backgroundColor: colors.border },
stepText: {} as any,
inputBox: {
flex: 1,
height: 45,
backgroundColor: '#F1F5F9',
backgroundColor: colors.background,
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
currency: { marginRight: 4 } as any,
bidValueText: {} as any,
submitBtn: {
height: 50,
backgroundColor: colors.primary,
@@ -312,5 +296,5 @@ const styles = StyleSheet.create({
justifyContent: '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: {},
});
+189 -85
View File
@@ -1,105 +1,209 @@
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 Animated, { FadeInUp } from 'react-native-reanimated';
import colors from '../../../Constant/Color';
import Animated, { FadeInDown, FadeInUp, ZoomIn } from 'react-native-reanimated';
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 }) => {
return (
<View style={styles.container}>
<LottieView
source={require('../../../lottie/confetti.json')}
autoPlay
loop={true}
style={styles.confetti}
/>
const { colors, spacing, shadows } = useTheme();
<LottieView
source={require('../../../lottie/Success.json')}
autoPlay
loop={true}
style={styles.success}
/>
return (
<View style={[styles.root, { backgroundColor: colors.background }]}>
<Animated.Text entering={FadeInUp.delay(400).duration(700)} style={styles.title}>
EMD Complete
</Animated.Text>
<Animated.Text entering={FadeInUp.delay(700).duration(700)} style={styles.subText} >
Your submission was successful
</Animated.Text>
{/* ── Center content ── */}
<View style={styles.centerContent}>
<Animated.View entering={FadeInUp.delay(900).duration(700)} style={styles.nextWrapper} >
<TouchableOpacity activeOpacity={0.85} style={styles.nextBtn} onPress={onNext} >
<Text style={styles.nextText}>Next</Text>
</TouchableOpacity>
</Animated.View>
</View>
);
{/* Success animation bubble */}
<Animated.View entering={ZoomIn.delay(100).springify()} style={[styles.lottieBubble, { backgroundColor: `${colors.primary}12` }]}>
<LottieView
source={require('../../../lottie/Success.json')}
autoPlay
loop={false}
style={styles.lottie}
/>
</Animated.View>
{/* Title */}
<Animated.View entering={FadeInUp.delay(400).springify()} style={styles.titleBlock}>
<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>
{/* ── Step Completion Chips ── */}
<Animated.View entering={FadeInDown.delay(600).springify()} style={[styles.stepsCard,{backgroundColor: colors.surface, shadowColor: colors.shadow}]}>
{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>
{/* ── Info badge ── */}
<Animated.View
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>
</Animated.View>
</View>
);
};
export default EmdComplete;
/* ---------------- Styles ---------------- */
/* ─── Styles ─── */
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F8FAFC',
alignItems: 'center',
justifyContent: 'center',
},
root: {
flex: 1,
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 40,
},
confetti: {
position: 'absolute',
top: 0,
width: '100%',
height: '100%',
},
centerContent: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 24,
gap: 24,
width: '100%',
},
success: {
width: 220,
height: 220,
marginTop: -100
},
/* Lottie bubble */
lottieBubble: {
width: 160,
height: 160,
borderRadius: 80,
alignItems: 'center',
justifyContent: 'center',
},
lottie: {
width: 130,
height: 130,
},
title: {
marginTop: 16,
fontSize: 24,
fontWeight: '900',
color: '#0F172A',
letterSpacing: 0.5,
},
/* Title */
titleBlock: {
alignItems: 'center',
gap: 8,
},
title: {
letterSpacing: 0.3,
} as any,
subtitle: {
lineHeight: 22,
opacity: 0.75,
} as any,
subText: {
marginTop: 8,
fontSize: 14,
color: '#475569',
},
/* 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,
},
/* ---------- Floating Next Button ---------- */
nextWrapper: {
position: 'absolute',
right: 20,
bottom: 30,
},
/* Info badge */
infoBadge: {
width: '100%',
borderRadius: 16,
borderWidth: 1.2,
paddingVertical: 12,
paddingHorizontal: 16,
},
infoText: {
lineHeight: 20,
} as any,
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,
},
/* CTA */
bottomArea: {
width: '100%',
paddingHorizontal: 24,
},
ctaBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 16,
borderRadius: 18,
},
ctaText: {
letterSpacing: 0.5,
} as any,
});
@@ -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 { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput } from 'react-native';
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 MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import axios, { all } from 'axios';
import Variables from '../../../Constant/Variable';
import { useSelector } from 'react-redux';
@@ -11,6 +13,8 @@ import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
import { Loader } from '../../../Component/Index';
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 [allState, setAllState] = useState({ isLoading: false })
const [showDetail, setShowDetail] = useState(false);
@@ -108,9 +112,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
entering={FadeInUp.delay(100)}
style={[
styles.card,
item.me ? styles.leadingCard : undefined,
{
borderColor: item.me ? '#22C55E' : '#EF4444',
backgroundColor: item.me ? '#F0FDF4' : '#FDF0F0',
borderColor: item.me ? colors.success : colors.error,
backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10`,
},
]}
>
@@ -121,10 +126,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
style={styles.cardHeader}
>
<View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text>
<Text style={styles.itemIdText}>
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
Initial Amt : ₹{item.auctionStartValue}
</Text>
</AppText>
</View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
@@ -135,38 +140,40 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
item.me ? styles.meBadge : styles.liveBadge,
]}
>
<Text
<AppText
variant="caption"
bold
style={[
styles.badgeText,
styles.badgeText as any,
item.me ? styles.meText : styles.liveText,
]}
>
{item.me ? 'LEADING' : 'LIVE'}
</Text>
</AppText>
</View>
<Animated.Text style={[styles.arrow, arrowStyle]}>
<Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
▼
</Animated.Text>
</View>
<Text style={styles.itemIdText}>
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
{SelectedAuction?.auctionNumber}
</Text>
</AppText>
</TouchableOpacity>
</TouchableOpacity>
{/* PRICE INFO */}
<View style={styles.priceRow}>
<View>
<Text style={styles.label}>Current Bid</Text>
<Text style={styles.mainPrice}>₹{item.bidAmount || item.auctionStartValue}</Text>
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item.bidAmount || item.auctionStartValue}</AppText>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Decrement</Text>
<Text style={styles.incrementText}>
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Decrement</AppText>
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
-₹{item.emdAmount}
</Text>
</AppText>
</View>
</View>
@@ -175,7 +182,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
<Animated.View style={[styles.collapsibleContainer, bodyStyle]}>
<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}>
{/* DECREASE */}
@@ -187,25 +194,25 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
!canDecrease && styles.disabledBtn,
]}
>
<Text style={styles.stepText}>−</Text>
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
</TouchableOpacity>}
{/* VALUE */}
{!allBoolean.oneTimeBid && <View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text>
<Text style={styles.bidValueText}>{bidValue}</Text>
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
</View>}
{
(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
value={value}
onChangeText={handleChange}
keyboardType="decimal-pad"
placeholder="0.00"
style={styles.input}
placeholderTextColor="#94A3B8"
style={[styles.input, { color: colors.text }]}
placeholderTextColor={colors.textSecondary}
/>
</View>
}
@@ -219,7 +226,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
!canIncrease && styles.disabledBtn,
]}
>
<Text style={styles.stepText}>+</Text>
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
</TouchableOpacity>}
</View>
@@ -228,7 +235,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, 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>
@@ -265,50 +272,34 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
export default ReverseBiddingCard;
const styles = StyleSheet.create({
const makeStyles = (colors: any) => StyleSheet.create({
inputBox2: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F1F5F9',
backgroundColor: colors.background,
borderRadius: 12,
height: 45,
paddingHorizontal: 12,
width: '100%'
},
currency2: {
fontSize: 16,
fontWeight: 'bold',
color: '#64748B',
marginRight: 4,
},
} as any,
input: {
flex: 1,
fontSize: 18,
fontWeight: '700',
color: '#0F172A',
width: '100%'
},
card: {
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',
@@ -316,27 +307,24 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
},
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
productName: {} as any,
itemIdText: { marginTop: 2 } as any,
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' },
liveBadge: { backgroundColor: `${colors.error}20` },
meBadge: { backgroundColor: `${colors.success}20` },
badgeText: { fontSize: 10 } as any,
liveText: { color: colors.error },
meText: { color: colors.success },
arrow: { fontSize: 10 },
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' },
} as any,
mainPrice: {} as any,
incrementText: {} as any,
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
bidLabel: { marginBottom: 10 } as any,
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: {
width: 45,
@@ -346,19 +334,19 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
disabledBtn: { backgroundColor: colors.border },
stepText: {} as any,
inputBox: {
flex: 1,
height: 45,
backgroundColor: '#F1F5F9',
backgroundColor: colors.background,
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
currency: { marginRight: 4 } as any,
bidValueText: {} as any,
submitBtn: {
height: 50,
backgroundColor: colors.primary,
@@ -366,5 +354,5 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
submitBtnText: {} as any,
});
@@ -1,22 +1,26 @@
import React, { useState } from 'react'
import { Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView } from 'react-native'
import React, { useState, useMemo } from 'react'
import { Modal, View, StyleSheet, TextInput, TouchableOpacity, ScrollView, Platform } from 'react-native'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { pick } from '@react-native-documents/picker'
import { useSelector } from 'react-redux'
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 colors from '../../../Constant/Color'
import { Loader } from '../../../Component/Index'
import { useNavigation } from '@react-navigation/native'
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 }) => {
if (!template) return null
const SelectedAuction = useSelector((state: any) => state.auction.auction)
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 initialValues = template?.labels?.reduce((acc: any, label: any) => {
acc[label.labelName] = ''
@@ -65,10 +69,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
if (label.labelType === '3') {
return (
<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.mandatory && <Text style={styles.required}> *</Text>}
</Text>
{label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
</AppText>
<TouchableOpacity
style={[
@@ -79,18 +83,25 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
pickFile(label.labelName, setFieldValue)
}
>
<Text
style={[
styles.fileText,
value?.name && styles.fileSelected,
]}
>
{value?.name || 'Upload PDF'}
</Text>
{value?.name ? (
<View style={styles.fileSelectedRow}>
<FileText size={14} color={colors.success} strokeWidth={2} />
<AppText variant="caption" color={colors.text} bold style={{ flex: 1 } as any} numberOfLines={1}>
{value.name}
</AppText>
</View>
) : (
<View style={styles.fileSelectedRow}>
<Upload size={14} color={colors.textSecondary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary}>
Upload PDF
</AppText>
</View>
)}
</TouchableOpacity>
{showError && (
<Text style={styles.errorText}>{error}</Text>
<AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
)}
</View>
)
@@ -98,10 +109,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
return (
<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.mandatory && <Text style={styles.required}> *</Text>}
</Text>
{label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
</AppText>
<TextInput
style={[
@@ -109,7 +120,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
showError && styles.errorBorder,
]}
placeholder={`Enter ${label.labelName}`}
placeholderTextColor="#9CA3AF"
placeholderTextColor={colors.textSecondary}
value={value}
onChangeText={(text) =>
setFieldValue(label.labelName, text)
@@ -122,7 +133,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
/>
{showError && (
<Text style={styles.errorText}>{error}</Text>
<AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
)}
</View>
)
@@ -210,26 +221,26 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
<View style={styles.header}>
<View style={styles.headerLeft}>
<Text style={styles.headerTitle}>Technical Documents</Text>
<Text style={styles.headerSubtitle}>
<AppText variant="h4" color={colors.text}>Technical Documents</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Fill the form, Click save when you're done.
</Text>
</AppText>
</View>
<TouchableOpacity onPress={onClose} style={styles.closeBtn}>
<Text style={styles.closeText}>✕</Text>
<X size={20} color={colors.textSecondary} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.content}>
<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}>
<Text style={styles.templateName}>
<AppText variant="bodySmall" color={colors.text} bold style={{ marginBottom: 12 } as any}>
{template.templateName}
</Text>
</AppText>
<Formik
initialValues={initialValues}
@@ -264,12 +275,12 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
{loading ? (
<Loader
size="small"
color={colors.white}
color="#fff"
/>
) : (
<Text style={styles.submitText}>
<AppText variant="button" color="#fff" bold style={styles.submitText as any}>
Submit
</Text>
</AppText>
)}
</TouchableOpacity>
</View>
@@ -287,129 +298,98 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
export default TemplateFormModal
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: '#F9FAFB',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderColor: '#E5E7EB',
},
headerLeft: {
flex: 1,
},
headerTitle: {
fontSize: 16,
fontWeight: '700',
color: '#111827',
},
headerSubtitle: {
fontSize: 12,
color: '#6B7280',
marginTop: 2,
},
closeBtn: {
padding: 6,
},
closeText: {
fontSize: 18,
color: '#6B7280',
},
content: {
paddingHorizontal: 16,
},
card: {
backgroundColor: '#FFFFFF',
borderColor: '#E5E7EB',
paddingVertical: 20
},
templateHeading: {
fontSize: 18,
fontWeight: '700',
textAlign: 'center',
marginBottom: 16,
color: '#111827',
},
templateBox: {
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 10,
padding: 16,
},
templateName: {
fontSize: 14,
fontWeight: '700',
color: '#111827',
marginBottom: 12,
},
field: {
marginBottom: 14,
},
label: {
fontSize: 13,
fontWeight: '600',
color: '#374151',
marginBottom: 6,
},
required: {
color: '#EF4444',
},
input: {
backgroundColor: '#FFFFFF',
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 8,
padding: 12,
fontSize: 14,
color: '#111827',
},
filePicker: {
borderWidth: 1,
borderColor: '#E5E7EB',
borderRadius: 8,
padding: 12,
},
fileText: {
color: '#6B7280',
fontSize: 14,
},
fileSelected: {
color: '#111827',
fontWeight: '600',
},
errorText: {
color: '#EF4444',
fontSize: 12,
marginTop: 4,
},
errorBorder: {
borderColor: '#EF4444',
},
submitRow: {
marginTop: 20,
alignItems: 'flex-end',
height: 50
},
submitBtn: {
backgroundColor: colors.primary,
paddingVertical: 10,
paddingHorizontal: 22,
borderRadius: 8,
minWidth: 120,
alignItems: 'center',
},
submitText: {
color: '#FFFFFF',
fontWeight: '700',
fontSize: 14,
},
})
const makeStyles = (colors: any, spacing: any, typography: any) =>
StyleSheet.create({
screen: {
flex: 1,
backgroundColor: colors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderColor: colors.border,
},
headerLeft: {
flex: 1,
},
closeBtn: {
padding: 8,
backgroundColor: colors.overlay,
borderRadius: 10,
},
content: {
paddingHorizontal: 16,
},
card: {
backgroundColor: colors.surface,
borderRadius: 18,
marginTop: spacing.m,
padding: spacing.m,
...Platform.select({
ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 12 },
android: { elevation: 3 },
}),
},
templateBox: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 14,
padding: 16,
},
field: {
marginBottom: 14,
},
label: {
letterSpacing: 0.5,
textTransform: 'uppercase',
marginBottom: 6,
} as any,
input: {
backgroundColor: colors.background,
borderWidth: 1.2,
borderColor: colors.border,
borderRadius: 12,
padding: 12,
fontSize: 14,
color: colors.text,
},
filePicker: {
borderWidth: 1.2,
borderColor: colors.border,
backgroundColor: colors.background,
borderRadius: 12,
padding: 12,
},
fileSelectedRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
errorText: {
marginTop: 4,
} as any,
errorBorder: {
borderColor: colors.error,
},
submitRow: {
marginTop: 20,
alignItems: 'flex-end',
height: 50,
},
submitBtn: {
backgroundColor: colors.primary,
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 14,
minWidth: 120,
alignItems: 'center',
},
submitText: {
letterSpacing: 0.5,
} as any,
})
File diff suppressed because it is too large Load Diff
+241 -251
View File
@@ -1,52 +1,74 @@
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 Animated, { FadeInDown } from 'react-native-reanimated';
import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useWebSocketService } from '../../socket/Socket';
import Variables from '../../Constant/Variable';
import { useSelector } from 'react-redux';
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 ConfirmBidModal from './Component/ConfirmBidModal';
import GoBackModal from './Component/GoBackModal';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
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 { colors, spacing, shadows } = useTheme();
const SelectedAuction = useSelector((state: any) => state.auction.auction);
const loginUser = useSelector((state: any) => state.token.token)
const Navigation: any = useNavigation()
const loginUser = useSelector((state: any) => state.token.token);
const Navigation: any = useNavigation();
const [items, setItems] = useState<any>([]);
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(
Variables.webSocketUrl,
() => console.log('Connected!'),
(error) => console.log('WebSocket Error:', error),
(error: any) => console.log('WebSocket Error:', error),
loginUser.token,
);
const getAuction = async () => {
const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId });
if (selectedRes.statusCode == 200 && selectedRes.data.contractorId.length) {
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
if (selectedRes.statusCode === 200 && selectedRes.data.contractorId.length) {
setAllBoolean(prev => ({ ...prev, oneTimeBid: true }));
}
}
};
const getOneTimeBidValue = async () => {
try {
const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId });
if (selectedRes.statusCode == 200) {
return selectedRes.data
} else {
return []
}
} catch (error) {
return selectedRes.statusCode === 200 ? selectedRes.data : [];
} catch { return []; }
};
}
}
const getAllAcutionItems = async () => {
try {
const payload = { auctionNumber: SelectedAuction.auctionNumber };
@@ -58,87 +80,40 @@ const StartBidding = () => {
}
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(
new Set(selectedRes?.data || [])
);
const filteredItems = auctionItem.filter(item =>
uniqueArray.includes(item.id)
);
if (SelectedAuction.auctionCategories == 'Live') {
if (SelectedAuction.auctionCategories === 'Live') {
const result = await createRoom(SelectedAuction.auctionNumber);
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, data => {
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data: any) => {
console.log('🔥 BID UPDATE:', data);
});
const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>();
(result || []).forEach((r: any) => {
bidAmountMap2.set(r.auctionItemId, {
bidAmount: r.bidAmount,
contractorId: r.contractorId,
});
bidAmountMap2.set(r.auctionItemId, { 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 bidData = bidAmountMap2.get(item.id);
return {
...item,
bidAmount: bidData?.bidAmount ?? item.auctionStartValueFigure,
bidAvilable: bidData?.bidAmount ?? 0,
me: bidData?.contractorId == loginUser.contractorId ? true : false
me: bidData?.contractorId === loginUser.contractorId,
};
});
const initialBids: Record<number, number> = {};
updatedFilteredItems.forEach(item => {
initialBids[item.id] = item.bidAmount;
});
updatedFilteredItems.forEach(item => { initialBids[item.id] = item.bidAmount; });
setBidValues(initialBids);
setItems(updatedFilteredItems);
} else {
let result = []
if (SelectedAuction.rebidAllowed) {
setAllBoolean((prev: any) => ({ ...prev, rebid: true }))
}
if (SelectedAuction.auctionCategories != 'Live') {
result = await getOneTimeBidValue()
}
if (SelectedAuction.rebidAllowed) setAllBoolean(prev => ({ ...prev, rebid: true }));
let result: any[] = [];
if (SelectedAuction.auctionCategories !== 'Live') result = await getOneTimeBidValue();
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 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 {
...item,
bidAmount: bid.bidAmount,
@@ -150,56 +125,36 @@ const StartBidding = () => {
};
});
const initialBids: Record<number, number> = {};
mergedData.forEach(item => {
initialBids[item.id] = item.bidAmount;
});
setBidValues(initialBids);
setItems(mergedData)
mergedData.forEach(item => { initialBids[item.id] = item.bidAmount; });
setBidValues(initialBids);
setItems(mergedData);
}
} catch (error) {
console.error('Error fetching auction items:', error);
}
};
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message) => {
// console.log('RAW MESSAGE:', message[message.length - 1]);
const result = message[message.length - 1]
const updatedBidItem = (prev: any[]) =>
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message: any) => {
const result = message[message.length - 1];
setItems((prev: any[]) =>
prev.map(item =>
item.id === result.auctionItemId
? { ...item, bidAmount: result.bidAmount, me: result.contractorId == loginUser.contractorId ? true : false }
: item
);
setItems(updatedBidItem);
setBidValues(prev => ({
...prev,
[result.auctionItemId]: result.bidAmount,
}));
try {
} catch (err) {
console.log('JSON parse error:', err);
}
? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
: item,
),
);
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
});
useEffect(() => {
if (SelectedAuction.auctionCategories == 'One-time') {
getAuction()
}
if (SelectedAuction.auctionCategories === 'One-time') getAuction();
getAllAcutionItems();
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data) => { });
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, () => {});
}, [allBoolean.reload]);
useEffect(() => {
const initial: Record<number, number> = {};
items.forEach((item: any) => {
initial[item.id] = item.bidAmount + item.bidVariationValue;
});
items.forEach((item: any) => { initial[item.id] = item.bidAmount + item.bidVariationValue; });
setBidValues(initial);
}, []);
@@ -216,51 +171,14 @@ const StartBidding = () => {
fullName: loginUser.fullName,
roomId: SelectedAuction.auctionNumber,
};
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, }));
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 }
);
setAllBoolean(prev => ({ ...prev, bidPayload: payload, showDialog: true }));
};
const placeBidSubmit = async (data: any) => {
setAllBoolean(prev => ({ ...prev, showDialog: false }));
try {
const result: any = await placeBid(data);
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()
}
if (result?.status && result.data) getAllAcutionItems();
} catch (error) {
console.error('Error placing bid:', error);
}
@@ -269,56 +187,75 @@ const StartBidding = () => {
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
Alert.alert(
'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
setAllBoolean(prev => ({ ...prev, showBackDialog: true }));
return true;
};
const subscription = BackHandler.addEventListener(
'hardwareBackPress',
onBackPress
);
return () => subscription.remove();
}, [Navigation])
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => sub.remove();
}, [Navigation]),
);
// console.log(bidValues, 'jjfdlsjfldsjfl');
const styles = makeStyles(colors, spacing, shadows);
const biddingEndDate =
SelectedAuction?.auctionStatus === 'Amended'
? SelectedAuction?.amendedkeyDates?.auctionBidding?.endDateTime
: SelectedAuction?.keyDates?.auctionBidding?.endDateTime;
return (
<View style={styles.container}>
<View style={styles.root}>
<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
endDate={SelectedAuction.keyDates.auctionBidding.endDateTime}
myStyle={{ fontSize: 30 }}
size={30}
endDate={biddingEndDate}
myStyle={{ fontSize: 28, fontWeight: '900', color: colors.primary, letterSpacing: 1 }}
size={28}
/>
</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
data={items}
keyExtractor={item => item.id.toString()}
@@ -328,15 +265,17 @@ const StartBidding = () => {
bidValue={bidValues[item.id] || 0}
onIncrease={() => handleUpdate(item.id, item.bidVariationValue)}
onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)}
onPlaceBid={() => { submitBid(item) }}
onPlaceBid={() => submitBid(item)}
SelectedAuction={SelectedAuction}
allBoolean={allBoolean}
setAllBoolean={setAllBoolean}
/>
)}
contentContainerStyle={{ padding: 16, paddingBottom: 300 }}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/>
{/* ── Success overlay ── */}
{allBoolean.showSuccess && (
<View style={styles.overlay}>
<LottieView
@@ -347,66 +286,117 @@ const StartBidding = () => {
/>
</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>
);
};
export default StartBidding;
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.white },
container2: {
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 8,
},
title: {
fontSize: 18,
fontFamily: FontsFamily.poppinsSemiBold,
color: colors.black,
marginBottom: 6,
// textAlign:'center'
},
timerWrapper: {
alignItems: 'center',
},
card: {
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 },
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
root: {
flex: 1,
backgroundColor: colors.background,
},
/* Info band */
infoBand: {
backgroundColor: colors.surface,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
...shadows.light,
},
deptRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
deptIcon: {
width: 30,
height: 30,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
deptName: {
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,
},
});
+38 -90
View File
@@ -7,20 +7,25 @@ import { useWebSocketService } from '../../socket/Socket';
import Variables from '../../Constant/Variable';
import { useSelector } from 'react-redux';
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 NavigationString from '../../Constant/NavigationString';
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 ConfirmBidModal from './Component/ConfirmBidModal';
import GoBackModal from './Component/GoBackModal';
const StartReverseBidding = () => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const SelectedAuction = useSelector((state: any) => state.auction.auction);
const loginUser = useSelector((state: any) => state.token.token)
const Navigation: any = useNavigation()
const [items, setItems] = useState<any>([]);
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(
Variables.webSocketUrl,
() => console.log('Connected!'),
@@ -216,30 +221,13 @@ const StartReverseBidding = () => {
fullName: loginUser.fullName,
roomId: SelectedAuction.auctionNumber,
};
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, }));
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 }
);
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, showDialog: true }));
};
const placeBidSubmit = async (data: any) => {
setAllBoolean((prev: any) => ({ ...prev, showDialog: false }));
try {
const result: any = await placeBid(data);
console.log(result, 'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk');
if (result?.status && result.data) {
const updatedBidItem = (prev: any[]) =>
@@ -259,26 +247,7 @@ const StartReverseBidding = () => {
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
Alert.alert(
'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 }
);
setAllBoolean((prev: any) => ({ ...prev, showBackDialog: true }));
return true; // block default back action
};
@@ -295,9 +264,9 @@ const StartReverseBidding = () => {
<View style={styles.container}>
<InsideHeader title="Bidding Panel" showArrow />
<View style={styles.container2}>
<Text style={styles.title}>
<AppText variant="h3" color={colors.text} bold style={styles.title as any}>
{SelectedAuction.deptName}
</Text>
</AppText>
<View style={styles.timerWrapper}>
<CountdownTimer
@@ -336,66 +305,45 @@ const StartReverseBidding = () => {
/>
</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>
);
};
export default StartReverseBidding;
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.white },
const makeStyles = (colors: any) => StyleSheet.create({
container: { flex: 1, backgroundColor: colors.background },
container2: {
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 8,
},
title: {
fontSize: 18,
fontFamily: FontsFamily.poppinsSemiBold,
color: colors.black,
marginBottom: 6,
// textAlign:'center'
},
} as any,
timerWrapper: {
alignItems: 'center',
},
card: {
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 },
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.overlay, justifyContent: 'center', alignItems: 'center', zIndex: 99 },
});
+221 -229
View File
@@ -1,260 +1,252 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, Dimensions, FlatList, ActivityIndicator } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withDelay, withTiming } from 'react-native-reanimated';
import { Layers, Clock, ShoppingCart, X, FileText } from 'lucide-react-native';
import { Header } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useDashboard, useESetuAdharVarification } from '../../Services/BBPS/Hooks';
import { log } from 'console';
import React, { useCallback, useEffect, useState } from 'react';
import { View, StyleSheet, Dimensions, ActivityIndicator, ScrollView, TouchableOpacity } from 'react-native';
import Animated, { FadeInDown, FadeInUp } from 'react-native-reanimated';
import { Layers, ShoppingCart, X, FileText, Menu, Radio, Clock, ChevronRight } from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
import { useDashboard, useTenderDashboard } from '../../Services/ApiCall/Hooks';
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 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 { data, isLoading } = useDashboard();
const user = useSelector((state: any) => state.token?.token);
console.log("-=--",user)
const { mutateAsync:eSetuAdharVarification } = useESetuAdharVarification();
const dashboard = data?.data || {};
const [esetu,setEsetu] = useState<boolean>();
useEffect(()=>{
if(user?.esetuVerified === 'Not Verified'){
const payLoad= {
contId: user,
transactionId: "string",
udf1: "string",
udf2: "string",
udf3: "string",
udf4: "string",
responseUrl: "string"
}
setEsetu(true)
eSetuAdharVarification(payLoad, {
onSuccess: (res) => {
console.log(res)
}, onError: (err) => {
console.log(err, '-----auction item error');
}
});
}else{
setEsetu(false)
}
},[user])
const { colors } = useTheme();
const DASHBOARD_CARDS = [
{
title: 'Total Auctions',
value: dashboard.myAuctionCount ?? 0,
desc: 'Auctions purchased by you',
icon: Layers,
color: '#2563EB',
},
{
title: 'Live Auctions',
value: dashboard.liveAuctionCount ?? 0,
desc: 'Currently live',
icon: Clock,
color: '#0284C7',
},
{
title: 'Participated Auctions',
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',
},
const [auctionStats, setAuctionStats] = useState<any>({});
const [tenderStats, setTenderStats] = useState<any>({});
const [isMenuVisible, setIsMenuVisible] = useState(false);
{
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',
},
];
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(() => {
if (dashboardData) {
setAuctionStats(dashboardData.data || {});
}
if (tenderData) {
setTenderStats(tenderData.data || {});
}
}, [dashboardData, tenderData]);
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,
{
backgroundColor: colors.glass,
borderColor: colors.overlay,
}
]}
>
<View style={styles.cardHeader}>
<View style={[styles.iconContainer, { backgroundColor: colors.overlay }]}>
{icon}
</View>
<AppText variant="h3" color={colors.primary}>{value || '0'}</AppText>
</View>
<View style={styles.cardContent}>
<AppText variant="caption" color={colors.textSecondary} numberOfLines={1}>{title}</AppText>
</View>
</Animated.View>
);
return (
<View style={styles.screen}>
<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);
<View style={[styles.container, { backgroundColor: colors.background }]}>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
}}
onClose={() => setEsetu(false)}
/>
<FlatList
data={DASHBOARD_CARDS}
keyExtractor={(_, index) => index.toString()}
numColumns={2}
columnWrapperStyle={styles.row}
contentContainerStyle={styles.list}
renderItem={({ item, index }) => (
<StatCard
item={item}
index={index}
isLoading={isLoading}
/>
{/* Top Bar */}
<Animated.View entering={FadeInDown.duration(600)} style={styles.headerTop}>
<View style={styles.userInfo}>
<View style={[styles.avatarContainer, { backgroundColor: colors.primary }]}>
<AppText variant="h3" color="#FFF">{userData?.firstName?.charAt(0) || 'U'}</AppText>
</View>
<View>
<AppText variant="caption" color={colors.textSecondary}>Welcome,</AppText>
<AppText variant="body" color={colors.text} style={{ fontWeight: '600' }}>{userData?.firstName || 'User'}</AppText>
</View>
</View>
<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}
/>
</View>
{/* 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 >
);
};
export default Home;
const styles = StyleSheet.create({
screen: {
container: {
flex: 1,
backgroundColor: colors.white,
},
list: {
padding: 16,
paddingBottom: 24,
scrollContent: {
paddingTop: 40,
paddingHorizontal: 16,
paddingBottom: 90,
},
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: {
headerTop: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
alignItems: 'center',
marginBottom: 20,
},
title: {
fontSize: 13,
fontWeight: '600',
color: '#374151',
flex: 1,
paddingRight: 8,
},
badge: {
userInfo: {
flexDirection: 'row',
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,
borderRadius: 20,
paddingHorizontal: 8,
paddingVertical: 4,
marginBottom: 12,
flexDirection: 'column',
justifyContent: 'center',
},
badgeText: {
fontSize: 12,
fontWeight: '700',
halfWidthCard: {
width: (width - 42) / 2,
},
value: {
fontSize: 28,
fontWeight: '800',
color: '#111827',
marginTop: 5,
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
desc: {
fontSize: 12,
color: '#6B7280',
marginTop: 6,
iconContainer: {
width: 32,
height: 32,
borderRadius: 8,
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',
},
});
+12 -19
View File
@@ -15,6 +15,9 @@ import {
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import colors from '../../Constant/Color';
import { showToast } from '../../Constant/ShowDailog';
import NavigationString from '../../Constant/NavigationString';
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
interface ForgotModalProps {
visible: boolean;
@@ -28,7 +31,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
visible,
onClose
}) => {
const navigation = useNavigation() as Navigation;
const Navigation = useNavigation<any>();
const [email, setEmail] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>('');
@@ -56,7 +59,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
try {
// Make API call to send OTP for forgot password
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',
headers: {
@@ -67,20 +70,10 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
);
if (response.ok) {
Alert.alert(
'Success',
'OTP has been sent to your email address. Please check your inbox.',
[
{
text: 'OK',
onPress: () => {
onClose();
navigation.navigate('ResetPassword', { email });
}
}
]
);
setEmail('');
showToast('OTP has been sent to your email address. Please check your inbox.')
onClose();
Navigation.navigate(NavigationString.ResetPassword, { email });
} else {
const errorData = await response.text();
console.error('API Error:', errorData);
@@ -109,13 +102,13 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.centeredView}>
<KeyboardAvoidingView
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoidingView}
>
<View style={styles.modalView}>
<Text style={styles.title}>Reset Password</Text>
<Text style={styles.subtitle}>
Enter your email address and we'll send you an OTP to reset your password.
</Text>
@@ -142,7 +135,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.resetButton, loading && styles.disabledButton]}
onPress={handleResetPassword}
File diff suppressed because it is too large Load Diff
+21 -37
View File
@@ -16,8 +16,10 @@ import {
TextInputKeyPressEventData,
ToastAndroid,
} 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';
interface ResetPasswordProps {
route?: any;
navigation?: any;
@@ -45,12 +47,10 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
const confirmPasswordRef = useRef<TextInput>(null);
const resendTimerRef = useRef<NodeJS.Timeout | null>(null);
// Initialize resend cooldown from storage or set to 0
useEffect(() => {
checkResendCooldown();
}, []);
// Clean up timer on unmount
useEffect(() => {
return () => {
if (resendTimerRef.current) {
@@ -59,7 +59,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
};
}, []);
// Update timer every second
useEffect(() => {
if (resendCooldown > 0) {
resendTimerRef.current = setInterval(() => {
@@ -98,13 +97,12 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
};
const startResendCooldown = async () => {
const cooldownTime = 300; // 5 minutes in seconds
const cooldownTime = 300;
setResendCooldown(cooldownTime);
await AsyncStorage.setItem('otpResendTime', Date.now().toString());
};
useEffect(() => {
// Auto-submit OTP when all fields are filled
const fullOtp = otp.join('');
if (fullOtp.length === 6 && !isOtpVerified) {
handleSubmitOtp();
@@ -112,14 +110,13 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
}, [otp]);
const handleOtpChange = (value: string, index: number) => {
if (!/^\d*$/.test(value)) return; // Only allow numbers
if (!/^\d*$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
setOtpError('');
// Auto-focus to next input
if (value && index < 5) {
otpInputs.current[index + 1]?.focus();
}
@@ -127,7 +124,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
const handleOtpKeyPress = (e: NativeSyntheticEvent<TextInputKeyPressEventData>, index: number) => {
if (e.nativeEvent.key === 'Backspace' && !otp[index] && index > 0) {
// Move focus to previous input on backspace
otpInputs.current[index - 1]?.focus();
}
};
@@ -149,7 +145,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
setOtpError('');
try {
// Make the API request to verify OTP
const res = await axios.post(
`${BASE_URL}eproc/verify-otp?email=${encodeURIComponent(email)}&otp=${fullOtp}`,
null,
@@ -167,7 +162,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
ToastAndroid.show('OTP verified successfully!', ToastAndroid.SHORT);
setIsOtpVerified(true);
// Focus on password field after successful OTP verification
setTimeout(() => {
newPasswordRef.current?.focus();
}, 100);
@@ -193,19 +187,16 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
const validateForm = (): boolean => {
let isValid = true;
// Validate OTP
if (!isOtpVerified) {
setOtpError('Please verify OTP first');
isValid = false;
}
// Validate password
if (newPassword.length < 8) {
setPasswordError('Password must be at least 8 characters long');
isValid = false;
}
// Validate confirm password
if (newPassword !== confirmPassword) {
setConfirmPasswordError('Passwords do not match');
isValid = false;
@@ -225,7 +216,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
setLoading(true);
try {
// Make API call to reset password according to curl
const res = await axios.post(
`${BASE_URL}eproc/reset-password?email=${encodeURIComponent(email)}&otp=${otp.join('')}&newPassword=${encodeURIComponent(newPassword)}&confirmPassword=${encodeURIComponent(confirmPassword)}`,
null,
@@ -245,7 +235,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
'Your password has been reset successfully.',
[{
text: 'OK', onPress: () => {
// Navigate to login screen
navigation.navigate('Login');
}
}]
@@ -273,7 +262,6 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
setResendLoading(true);
try {
// Make API call to resend OTP
const res = await axios.post(
`${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
null,
@@ -286,11 +274,9 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
if (res.data?.statusCode === 200) {
ToastAndroid.show('OTP sent successfully', ToastAndroid.SHORT);
// Clear OTP fields
setOtp(['', '', '', '', '', '']);
setIsOtpVerified(false);
setOtpError('');
// Start the cooldown timer
startResendCooldown();
} else {
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
</Text>
{/* OTP Input Section */}
<View style={styles.otpSection}>
<Text style={styles.sectionLabel}>OTP Code</Text>
<View style={styles.otpContainer}>
@@ -363,10 +348,8 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
)}
</TouchableOpacity>
)}
</View>
{/* Password Input Section - Only enabled after OTP verification */}
<View style={styles.passwordSection}>
<Text style={styles.sectionLabel}>New Password</Text>
<View style={styles.passwordInputContainer}>
@@ -374,7 +357,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
ref={newPasswordRef}
style={[styles.input, passwordError ? styles.inputError : null, !isOtpVerified && styles.disabledInput]}
placeholder="Enter new password"
placeholderTextColor={!isOtpVerified ? '#4f058bff' : '#000'}
placeholderTextColor={!isOtpVerified ? colors.lightText : colors.black}
value={newPassword}
onChangeText={(text) => {
setNewPassword(text);
@@ -390,9 +373,11 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
style={styles.eyeIcon}
onPress={() => setNewPasswordVisible(!newPasswordVisible)}
>
<Text style={styles.eyeIconText}>
{newPasswordVisible ? '🙈' : '👁️'}
</Text>
{newPasswordVisible ? (
<EyeOff size={20} color={colors.primary} />
) : (
<Eye size={20} color={colors.primary} />
)}
</TouchableOpacity>
)}
</View>
@@ -404,7 +389,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
ref={confirmPasswordRef}
style={[styles.input, confirmPasswordError ? styles.inputError : null, !isOtpVerified && styles.disabledInput]}
placeholder="Confirm new password"
placeholderTextColor={!isOtpVerified ? '#4f058bff' : '#000'}
placeholderTextColor={!isOtpVerified ? colors.lightText : colors.black}
value={confirmPassword}
onChangeText={(text) => {
setConfirmPassword(text);
@@ -420,9 +405,11 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
style={styles.eyeIcon}
onPress={() => setConfirmPasswordVisible(!confirmPasswordVisible)}
>
<Text style={styles.eyeIconText}>
{confirmPasswordVisible ? '🙈' : '👁️'}
</Text>
{confirmPasswordVisible ? (
<EyeOff size={20} color={colors.primary} />
) : (
<Eye size={20} color={colors.primary} />
)}
</TouchableOpacity>
)}
</View>
@@ -445,7 +432,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
)}
</TouchableOpacity>
<TouchableOpacity
{!isOtpVerified && <TouchableOpacity
style={[styles.resendButton, resendCooldown > 0 && styles.disabledResendButton]}
onPress={handleResendOtp}
disabled={resendLoading || resendCooldown > 0}
@@ -459,7 +446,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
) : (
<Text style={styles.resendButtonText}>Resend OTP</Text>
)}
</TouchableOpacity>
</TouchableOpacity>}
</ScrollView>
</KeyboardAvoidingView>
);
@@ -550,12 +537,9 @@ const styles = StyleSheet.create({
eyeIcon: {
position: 'absolute',
right: 15,
top: 13,
top: 15,
zIndex: 1,
},
eyeIconText: {
fontSize: 20,
},
errorText: {
color: '#ff3b30',
fontSize: 14,
@@ -616,4 +600,4 @@ const styles = StyleSheet.create({
},
});
export default ResetPassword;
export default ResetPassword;
File diff suppressed because it is too large Load Diff
+111 -88
View File
@@ -3,23 +3,22 @@ import {
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
Platform,
} from 'react-native';
import React, { use, useState } from 'react';
import { useNavigation, useRoute, NavigationProp, RouteProp } from '@react-navigation/native';
import colors from '../../Constant/Color';
import React, { useMemo, useState } from 'react';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import MyInput from '../../Component/MyInput';
import MyInput from '../../components/MyInput';
import { showToast } from '../../Constant/ShowDailog';
import { PARTNER_API_ENDPOINT } from '../../Services/BBPS/ApiUrls';
import { usePartnerShipSubmit } from '../../Services/BBPS/Hooks';
import { usePartnerShipSubmit } from '../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Navigation } from 'lucide-react-native';
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 = {
partnerName: string;
@@ -32,11 +31,12 @@ type Navigation = {
replace: (route: string) => void;
};
type PartnershipParams = {
otherId: string | number
otherId: string | number;
};
const Partnership = () => {
const route = useRoute<RouteProp<{ params: PartnershipParams }, 'params'>>();
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation() as Navigation;
const [partners, setPartners] = useState<Partner[]>([
{
@@ -49,9 +49,11 @@ const Partnership = () => {
]);
const [focusedField, setFocusedField] = useState<string>('');
const [loading, setLoading] = useState(false)
const { mutateAsync:partnerShipSubmit } = usePartnerShipSubmit();
const insets = useSafeAreaInsets();
const [loading, setLoading] = useState(false);
const { mutateAsync: partnerShipSubmit } = usePartnerShipSubmit();
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 updated = [...partners];
updated[index][name] = value;
@@ -90,9 +92,8 @@ const Partnership = () => {
};
const handleSubmit = async () => {
setLoading(true)
setLoading(true);
try {
// Convert percentage strings to numbers and validate
const payload = partners.map((p, i) => {
const percentage = Number(p.percentage);
@@ -105,174 +106,196 @@ const Partnership = () => {
ptnrAadharNo: p.aadharNumber,
ptnrMobNo: p.mobileNumber,
ptnrName: p.partnerName,
ptnrPop: percentage, // Ensure this is a number
ptnrPop: percentage,
ptnrPANNo: p.partnerPan,
ptnrNo: i,
};
});
// Validate total percentage equals 100%
const totalPercentage = payload.reduce((sum, p) => sum + p.ptnrPop, 0);
if (totalPercentage !== 100) {
showToast(`Total percentage must equal 100% (currently ${totalPercentage}%)`);
return;
}
console.log("Partner ship payload:", payload);
const response = await partnerShipSubmit(payload);
console.log("first ====== ", response)
if (response?.statusCode === 200) {
console.log('✅ Partners submitted successfully:', response);
showToast(response.message || 'Submitted successfully!');
navigation.replace(NavigationString.Home);
}
} catch (error: any) {
console.error('❌ Submission failed:', error.message);
showToast(error.message || 'Submission failed. Please check partner details.');
}finally{
setLoading(false)
} finally {
setLoading(false);
}
};
return (
<View style={{ flex: 1 }}>
<Text style={[styles.heading, { paddingTop: insets.top }]}>Partnership Details</Text>
<View style={{ flex: 1, backgroundColor: colors.background }}>
<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 }}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40}
>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{partners.map((partner, index) => (
<View style={{ marginBottom: 20 }} key={index}>
<Text style={styles.partnerTitle}>Partner {index + 1}</Text>
<Animated.View
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
icon="user"
placeholder="Name of Partner *"
label="Partner Name"
placeholder="Enter Partner Name"
value={partner.partnerName}
onChangeText={text => handleChange(index, 'partnerName', text)}
onFocus={() => setFocusedField(`partnerName-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `partnerName-${index}`}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="percent"
placeholder="Partnership Percentage *"
label="Share Percentage"
placeholder="Enter Percentage"
value={partner.percentage}
onChangeText={text => handleChange(index, 'percentage', text)}
onFocus={() => setFocusedField(`percentage-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `percentage-${index}`}
keyboardType="numeric"
maxlength={3}
maxLength={3}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="id-card"
placeholder="PAN of Partner *"
label="PAN of Partner"
placeholder="Enter PAN Number"
value={partner.partnerPan}
onChangeText={text => handleChange(index, 'partnerPan', text)}
onFocus={() => setFocusedField(`partnerPan-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `partnerPan-${index}`}
autoCapitalize="characters"
maxlength={10}
maxLength={10}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="credit-card"
placeholder="Aadhar Number *"
label="Aadhaar Number"
placeholder="Enter 12-digit Aadhaar"
value={partner.aadharNumber}
onChangeText={text => handleChange(index, 'aadharNumber', text)}
onFocus={() => setFocusedField(`aadharNumber-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `aadharNumber-${index}`}
keyboardType="numeric"
maxlength={12}
maxLength={12}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="phone"
placeholder="Mobile Number *"
label="Mobile Number"
placeholder="Enter Mobile Number"
value={partner.mobileNumber}
onChangeText={text => handleChange(index, 'mobileNumber', text)}
onFocus={() => setFocusedField(`mobileNumber-${index}`)}
onBlur={() => setFocusedField('')}
focused={focusedField === `mobileNumber-${index}`}
keyboardType="phone-pad"
maxlength={10}
maxLength={10}
/>
{partners.length > 1 && (
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
<Text style={styles.deleteText}>Delete Partner</Text>
</TouchableOpacity>
)}
</View>
</Animated.View>
))}
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}>
<Text style={styles.addText}>+ Add Partner</Text>
<TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
<UserPlus size={16} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
Add Partner
</AppText>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
<PrimaryButton title={loading ? "Loading..." : "Submit" }onPress={handleSubmit} />
<View style={styles.footer}>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
</View>
</View>
);
};
export default Partnership;
const styles = StyleSheet.create({
heading: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
marginTop: 10,
textAlign: 'center',
},
partnerTitle: {
fontWeight: '600',
fontSize: 16,
marginBottom: 10,
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
header: {
paddingHorizontal: spacing.m,
paddingBottom: spacing.m,
borderBottomWidth: 1,
borderBottomColor: colors.border,
backgroundColor: colors.surface,
},
scrollContainer: {
// backgroundColor: color.background,
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: {
backgroundColor: colors.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
flexDirection: 'row',
alignItems: 'center',
width: 140,
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 12,
alignSelf: 'flex-end',
},
addText: {
color: 'white',
fontWeight: '600',
},
deleteButton: {
backgroundColor: '#FF5C5C',
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 140,
alignSelf: 'flex-end',
},
deleteText: {
color: 'white',
fontWeight: '600',
footer: {
padding: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
backgroundColor: colors.surface,
},
});
+189 -272
View File
@@ -1,31 +1,27 @@
import React, { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, SafeAreaView, ScrollView, BackHandler, ToastAndroid, Button, Alert } from 'react-native';
// import AntDesign from 'react-native-vector-icons/AntDesign';
import React, { useEffect, useMemo, useState } from 'react';
import { View, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, ScrollView, BackHandler, ToastAndroid, Alert } from '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 { 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 { pick } from '@react-native-documents/picker';
// import { REJ_STAGE_TWO } from '../../utils/services/apiUrl';
// import LoaderModel from '../../component/LoaderModal';
import axios from 'axios';
import StageOne from './ragistrationStages/StageOne';
import StageTwo from './ragistrationStages/StageTwo';
import StageZero from './ragistrationStages/StageZero';
import StageThree from './ragistrationStages/StageThree';
import StageFour from './ragistrationStages/StageFour';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import StepProgressBar from '../../Component/StepProgressBar';
import { formatDateInput } from '../../Helper/formatDateInput';
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 { 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 = {
userData?: {
@@ -38,11 +34,12 @@ type RouteParams = {
};
const Registration: React.FC = () => {
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation<any>();
const { params } = useRoute<RouteProp<Record<string, RouteParams>, string>>();
const userData = params as RouteParams;
const [stage, setStage] = useState(0);
const insets = useSafeAreaInsets();
const insets = useSafeAreaInsets();
const [focusedField, setFocusedField] = useState<null | string>(null);
const [selectedOrg, setSelectedOrg] = useState<OrgType>(0);
const [documents, setDocuments] = useState<DocumentsType>({
@@ -102,65 +99,54 @@ const Registration: React.FC = () => {
const { mutateAsync: verifyGstNumber } = useVerifyGstNumber();
const { mutateAsync: contractorStageTwoSubmit } = useContractorStageTwoSubmit();
const { data: masterData } = usefetchMasterData();
console.log("master",masterData)
useEffect(() => {
if (!masterData?.data) return;
const contractorClasses = masterData.data.contractorClasses || [];
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
useEffect(() => {
if (!masterData?.data) return;
const contractorClasses = masterData.data.contractorClasses || [];
const msmeOrgs = masterData.data.msmeOrgs || [];
const mappedmsmeOrgs = msmeOrgs.map((item:any)=> ({
label: item.comName,
value: item.msmeId,
}));
const orgs = masterData.data.organizationTypes || [];
const mappedcontractorClasses = contractorClasses.map((item:any)=> ({
label: item.contClassName || '',
value: item.contClassId,
}));
const mapped = orgs.map((item:any)=> ({
label: item.organizationType,
value: item.orgId,
}));
const socialCategories = masterData.data.socialCategories || [];
const mappedsocialCategories = socialCategories.map((item:any)=> ({
label: item.categoryName,
value: item.socialCategoryId,
}));
const stateList = masterData.data.states || [];
const mappedStates = stateList.map((item:any)=> ({
label: item.stateName,
value: item.stateId,
}));
setAllMsmeOrgs(mappedmsmeOrgs);
setAllSocialCategories(mappedsocialCategories);
setContractorClassItems(mappedcontractorClasses);
setAllOrganizations(mapped);
setstates(mappedStates);
}, [masterData?.data]);
const mappedmsmeOrgs = msmeOrgs.map((item: any) => ({
label: item.comName,
value: item.msmeId,
}));
const orgs = masterData.data.organizationTypes || [];
const mappedcontractorClasses = contractorClasses.map((item: any) => ({
label: item.contClassName || '',
value: item.contClassId,
}));
const mapped = orgs.map((item: any) => ({
label: item.organizationType,
value: item.orgId,
}));
const socialCategories = masterData.data.socialCategories || [];
const mappedsocialCategories = socialCategories.map((item: any) => ({
label: item.categoryName,
value: item.socialCategoryId,
}));
const stateList = masterData.data.states || [];
const mappedStates = stateList.map((item: any) => ({
label: item.stateName,
value: item.stateId,
}));
setAllMsmeOrgs(mappedmsmeOrgs);
setAllSocialCategories(mappedsocialCategories);
setContractorClassItems(mappedcontractorClasses);
setAllOrganizations(mapped);
setstates(mappedStates);
}, [masterData?.data]);
const { mutate: cityDropdownMutate } = useCityDropdown();
const { data: MasterData}=usefetchMasterData()
const user = useSelector((state: any) => state.token.token);
const { data: MasterData } = usefetchMasterData();
const user = useSelector((state: any) => state.token.token);
const steps = ['', '', '', '', ''];
const dropdownStyle = {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: '#fff',
marginBottom: 10,
}
useEffect(() => {
const fetchUserData = async (user:any) => {
console.log("hitting it ")
const fetchUserData = async (user: any) => {
try {
console.log("hitting it 1 ")
console.log("object====",user)
if (user) {
const parsedUser = user;
console.log(parsedUser);
setFormData(prev => ({
...prev,
firstName: parsedUser?.firstName || userData?.userData?.firstName,
@@ -169,7 +155,6 @@ useEffect(() => {
middleName: parsedUser?.middleName || userData?.userData?.middleName,
email: parsedUser?.email || userData?.userData?.email,
}));
console.log('User Data from Storage:', parsedUser);
} else {
console.log('No user data found');
}
@@ -186,25 +171,11 @@ useEffect(() => {
gstin = gstin.toUpperCase();
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)) {
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.' };
};
@@ -213,7 +184,7 @@ useEffect(() => {
};
const verifyPanHandler = async () => {
setPanVerifying(true); // Start loading
setPanVerifying(true);
const payload = {
dob: formData?.dateOfIncorp.trim(),
name: formData?.orgPanName.trim(),
@@ -226,11 +197,10 @@ useEffect(() => {
} else {
setPanVerified(false);
}
setPanVerifying(false); // Stop loading
setPanVerifying(false);
};
const sendAdharOTPHandler = async (text: string) => {
console.log('Sending Aadhaar OTP...');
setAadhaarVerifying(true);
setadharreference('');
@@ -307,7 +277,7 @@ useEffect(() => {
const handleSubmit = async () => {
setLoading(true);
const dtoData = {
agentAddress: formData.address,
agentCity: formData.city,
@@ -327,9 +297,8 @@ useEffect(() => {
orgPAN: formData.orgPanNumber,
comAadharNo: formData.aadhaarNumber,
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 {
const fileData = new FormData();
@@ -341,38 +310,33 @@ useEffect(() => {
fileData.append('pancardfile', documents.panCard as any);
fileData.append('regfile', documents.registrationCertificate as any);
console.log('File Data for registration',fileData);
const res = await contractorStageTwoSubmit(fileData);
console.log('Register contractor Response : ', res);
if (res.status === 200) {
console.log('Stage Two Res : ', res)
if (res.statusCode === 200) {
if (res) {
showToast('Form submitted successfully!');
console.log('response?.data?.contOtherId : ', res?.data?.contOtherId);
const orgType = selectedOrg;
const params = { otherId: res?.data?.contOtherId, orgType: orgType };
console.log("params : ", params)
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) {
console.log('org type else if : ', orgType);
navigation.replace('PartnerShip' as never, { otherId: res?.data?.contOtherId } as never);
navigation.replace(NavigationString.Partnership as never, { otherId: res?.data?.contOtherId } as never);
} else {
console.log('org type else : ', orgType);
navigation.replace('Dashboard' as never);
navigation.replace(NavigationString.Home as never);
}
} else {
showToast('Form submission failed');
}
} else {
showToast(res.message || 'Form submission failed. Please try again.');
}
} catch (error: any) {
console.log("Error in form submission")
if (axios.isAxiosError(error)) {
console.log('11111', error.response?.status);
console.log('22222', error.response?.data);
console.log('3333333', error.response?.headers);
} else {
console.error('Unexpected error', error);
}
@@ -383,117 +347,21 @@ 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) => {
cityDropdownMutate(stateId, {
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);
},
onError: (error) => {
setCities([]);
console.error('Error in city fetch:', error);
}
onSuccess: (data) => {
setCities(data);
},
onError: (error) => {
setCities([]);
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);
// 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(() => {
if (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 (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40}
<View style={{ flex: 1, backgroundColor: colors.background }}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
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
onPress={() => {
if (stage > 0) {
@@ -625,12 +509,39 @@ useEffect(() => {
}
}}
style={styles.backBtn}
activeOpacity={0.7}
>
{/* Replaced AntDesign with Lucide ArrowLeft */}
<ArrowLeft size={25} color={colors.primary} />
<ArrowLeft size={22} color={colors.text} />
</TouchableOpacity>
<StepProgressBar currentStep={stage} steps={steps} />
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
Registration
</AppText>
<View style={{ width: 36 }} />
</View>
{/* ═══ Progress Bar ═══ */}
<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 && (
<StageZero
formData={formData}
@@ -656,7 +567,7 @@ useEffect(() => {
allMsmeOrgs={allMsmeOrgs}
allSocialCategories={allSocialCategories}
allMSME={allMSME}
dropdownStyle={dropdownStyle}
dropdownStyle={{}}
setSelectedOrg={setSelectedOrg}
selectedOrg={selectedOrg}
/>
@@ -701,8 +612,9 @@ useEffect(() => {
states={states}
cities={cities}
fetchCity={() => {
fetchCity(Number(formData.state))}}
dropdownStyle={dropdownStyle}
fetchCity(Number(formData.state));
}}
dropdownStyle={{}}
styles={styles}
/>
)}
@@ -716,10 +628,14 @@ useEffect(() => {
selectedOrg={selectedOrg}
/>
)}
</View>
</Animated.View>
</View>
{/* ═══ Next / Submit Button ═══ */}
<Animated.View entering={FadeInDown.duration(300).delay(200)}>
<PrimaryButton
title={stage === 4 ? (loading ? 'Loading...' : 'Submit') : 'Next'}
style={{ backgroundColor: colors.primary }}
onPress={() => {
if (validateStage(stage)) {
switch (stage) {
@@ -764,104 +680,105 @@ useEffect(() => {
}
}}
/>
</ScrollView>
</KeyboardAvoidingView>
</Animated.View>
</ScrollView>
</KeyboardAvoidingView>
</View>
);
};
export default Registration;
const styles = StyleSheet.create({
// ── Styles ────────────────────────────────────────────────────────────
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
scrollContainer: {
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: {
fontSize: 15,
fontWeight: '600',
marginVertical: 10,
color: colors.black,
fontSize: typography.size.s,
fontWeight: '700',
color: colors.primary,
marginBottom: spacing.s,
letterSpacing: 0.3,
textTransform: 'uppercase',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 10,
},
backBtn: {
padding: 5,
width: 50,
marginBottom: 5,
},
sectionTitle: {
fontSize: 24,
fontSize: typography.size.l,
fontWeight: '700',
color: colors.black,
marginBottom: 20,
marginTop: 10,
color: colors.text,
marginBottom: spacing.m,
textAlign: 'center',
},
uploadGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
gap: spacing.s,
},
uploadCard: {
width: '48%',
backgroundColor: '#fdfdfd',
borderRadius: 20,
padding: 20,
backgroundColor: colors.surface,
borderRadius: 16,
padding: spacing.m,
alignItems: 'center',
marginBottom: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 6,
elevation: 4,
borderWidth: 1,
borderColor: '#ddd',
borderColor: colors.border,
},
iconCircle: {
width: 50,
height: 50,
borderRadius: 25,
width: 46,
height: 46,
borderRadius: 23,
backgroundColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 10,
marginBottom: spacing.s,
},
cardTitle: {
fontSize: 15,
fontSize: typography.size.s,
fontWeight: '600',
color: colors.black,
color: colors.text,
marginBottom: 4,
textAlign: 'center',
},
cardHint: {
fontSize: 12,
color: '#888',
fontSize: typography.size.xs,
color: colors.textSecondary,
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,
},
});
+119 -80
View File
@@ -3,23 +3,21 @@ import {
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
Platform,
} from 'react-native';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import MyInput from '../../Component/MyInput';
import MyInput from '../../components/MyInput';
import { showToast } from '../../Constant/ShowDailog';
import { STAGETHREE } from '../../Services/BBPS/ApiUrls';
import { useContractorStageThreeSubmit } from '../../Services/BBPS/Hooks';
import { useContractorStageThreeSubmit } from '../../Services/ApiCall/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 = {
director: string;
@@ -31,12 +29,15 @@ type Company = {
type StageThreeRouteParams = {
orgType: string;
otherId: string | number
otherId: string | number;
};
const StageThreeScreen = () => {
const route = useRoute<RouteProp<{ params: StageThreeRouteParams }, 'params'>>();
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[]>([
{
@@ -51,7 +52,7 @@ const StageThreeScreen = () => {
const [focusedField, setFocusedField] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const { mutateAsync: stageThreeSubmit } = useContractorStageThreeSubmit();
const handleChange = (index: number, field: keyof Company, value: string) => {
const updated = [...company];
updated[index][field] = value;
@@ -71,7 +72,6 @@ const StageThreeScreen = () => {
]);
};
console.log("first company route : ", route);
const handleSubmit = async () => {
setLoading(true);
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 (
<View style={{ flex: 1 }}>
<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={{ flex: 1, backgroundColor: colors.background }}>
<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 }}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40}
>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{company.map((partner, index) => (
<View style={{ marginBottom: 20 }} key={index}>
<Text style={styles.partnerTitle}>Company {index + 1}</Text>
<Animated.View
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
icon="user"
placeholder="Name of Director *"
label="Director Name"
placeholder="Enter Name of Director"
value={partner.director}
onChangeText={(text:any) => handleChange(index, 'director', text)}
onChangeText={(text: any) => handleChange(index, 'director', text)}
onFocus={() => setFocusedField(`director-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `director-${index}`}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="id-card"
placeholder="DIN Number in Firm *"
label="DIN Number"
placeholder="Enter DIN Number"
value={partner.DINNumber}
onChangeText={(text:any) => handleChange(index, 'DINNumber', text)}
onChangeText={(text: any) => handleChange(index, 'DINNumber', text)}
onFocus={() => setFocusedField(`DINNumber-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `DINNumber-${index}`}
keyboardType="numeric"
maxlength={8}
maxLength={8}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="id-card"
placeholder="PAN of Director *"
label="PAN of Director"
placeholder="Enter PAN Number"
value={partner.panNumber}
onChangeText={(text:any) => handleChange(index, 'panNumber', text)}
onChangeText={(text: any) => handleChange(index, 'panNumber', text)}
onFocus={() => setFocusedField(`panNumber-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `panNumber-${index}`}
maxlength={12}
maxLength={12}
autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="credit-card"
placeholder="Aadhar Number *"
label="Aadhaar Number"
placeholder="Enter 12-digit Aadhaar"
value={partner.aadharNumber}
onChangeText={(text:any) => handleChange(index, 'aadharNumber', text)}
onChangeText={(text: any) => handleChange(index, 'aadharNumber', text)}
onFocus={() => setFocusedField(`aadharNumber-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `aadharNumber-${index}`}
keyboardType="numeric"
maxlength={12}
maxLength={12}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="phone"
placeholder="Mobile Number *"
label="Mobile Number"
placeholder="Enter Mobile Number"
value={partner.mobileNumber}
onChangeText={(text:any) => handleChange(index, 'mobileNumber', text)}
onChangeText={(text: any) => handleChange(index, 'mobileNumber', text)}
onFocus={() => setFocusedField(`mobileNumber-${index}`)}
onBlur={() => setFocusedField(null)}
focused={focusedField === `mobileNumber-${index}`}
keyboardType="phone-pad"
maxlength={10}
maxLength={10}
/>
{company.length > 1 && (
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
<Text style={styles.deleteText}>Delete Company</Text>
</TouchableOpacity>
)}
</View>
</Animated.View>
))}
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}>
<Text style={styles.addText}>+ Add Company</Text>
<TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
<Building2 size={16} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
Add Director
</AppText>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
<View style={styles.footer}>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
</View>
</View>
);
};
export default StageThreeScreen;
const styles = StyleSheet.create({
heading: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
marginTop: 10,
textAlign: 'center',
},
partnerTitle: {
fontWeight: '600',
fontSize: 16,
marginBottom: 10,
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
header: {
paddingHorizontal: spacing.m,
paddingBottom: spacing.m,
borderBottomWidth: 1,
borderBottomColor: colors.border,
backgroundColor: colors.surface,
},
scrollContainer: {
// backgroundColor: color.background,
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: {
backgroundColor: colors.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
flexDirection: 'row',
alignItems: 'center',
width: 140,
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 12,
alignSelf: 'flex-end',
},
addText: {
color: 'white',
fontWeight: '600',
},
deleteButton: {
backgroundColor: '#FF5C5C',
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 140,
alignSelf: 'flex-end',
},
deleteText: {
color: 'white',
fontWeight: '600',
footer: {
padding: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
backgroundColor: colors.surface,
},
});
@@ -1,5 +1,5 @@
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { View, TouchableOpacity } from 'react-native';
import {
IdCard,
FileText,
@@ -7,7 +7,12 @@ import {
ShieldCheck,
Building2,
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 {
key: string;
@@ -30,13 +35,15 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
formData,
selectedOrg,
}) => {
const { colors, spacing, shadows } = useTheme();
const documentItems: DocumentItem[] = [
{ key: 'panCard', label: 'PAN Card', icon: 'pan' },
{ key: 'aadharCard', label: 'Aadhar Card', icon: 'aadhar' },
{ 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: 'registrationCertificate', label: 'Registration Certificate', icon: 'registration' },
{ key: 'registrationCertificate', label: 'Registration Cert.', icon: 'registration' },
];
const shouldRenderItem = (itemKey: string): boolean => {
@@ -52,54 +59,84 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
return false;
}
return true; // pan + aadhar + rest (unless filtered above)
return true;
};
const renderIcon = (iconType: DocumentItem['icon']) => {
const commonProps = { size: 26, color: '#fff', strokeWidth: 2 };
const commonProps = { size: 24, color: colors.buttonText, strokeWidth: 2 };
switch (iconType) {
case 'pan':
return <IdCard {...commonProps} />; // PAN → ID card feel [web:19]
return <IdCard {...commonProps} />;
case 'aadhar':
return <IdCard {...commonProps} />; // Same icon, different label
return <IdCard {...commonProps} />;
case 'gst':
return <FileBadge {...commonProps} />; // File with badge [web:20]
return <FileBadge {...commonProps} />;
case 'contractor':
return <ShieldCheck {...commonProps} />; // Safety / verification feel
return <ShieldCheck {...commonProps} />;
case 'msme':
return <Building2 {...commonProps} />; // Organization / company
return <Building2 {...commonProps} />;
case 'registration':
return <ScrollText {...commonProps} />; // Certificate / document scroll
return <ScrollText {...commonProps} />;
default:
return <FileText {...commonProps} />;
}
};
let animIndex = 0;
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 => {
if (!shouldRenderItem(item.key)) return null;
const isUploaded = !!documents[item.key];
const currentIndex = animIndex++;
return (
<TouchableOpacity
<Animated.View
key={item.key}
style={styles.uploadCard}
onPress={() => handleDocumentPick(item.key)}
entering={FadeInUp.delay(80 * currentIndex).springify()}
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}>
{documents[item.key]
? `${documents[item.key]?.name} ✅`
: 'Tap to upload PDF'}
</Text>
</TouchableOpacity>
<AppText variant="caption" color={isUploaded ? colors.primary : colors.textSecondary} style={{ textAlign: 'center', fontSize: 10 }}>
{isUploaded
? `${documents[item.key]?.name?.slice(0, 18)}...`
: 'Tap to upload PDF'}
</AppText>
</TouchableOpacity>
</Animated.View>
);
})}
</View>
@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { Text, View, StyleSheet } from 'react-native';
import DatePicker from 'react-native-date-picker'
import MyInput from '../../../Component/MyInput';
import { View, TouchableOpacity } from 'react-native';
import DatePicker from 'react-native-date-picker';
import MyInput from '../../../components/MyInput';
import MyDropdown from '../../../Component/Dropdown/MyDropdown';
import colors from '../../../Constant/Color';
import { DropdownOption } from '../types/types';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
formData: {
@@ -14,7 +14,7 @@ interface Props {
orgType: string;
registrationDetail: string | number;
socialCategory: string;
expiryDate: string
expiryDate: string;
};
handleChange: (key: string, value: string) => void;
focusedField: string | null;
@@ -26,7 +26,7 @@ interface Props {
allMSME: DropdownOption[];
dropdownStyle: object;
setSelectedOrg: (val: number) => void;
selectedOrg: string | number
selectedOrg: string | number;
}
const StageOne: React.FC<Props> = ({
@@ -40,22 +40,27 @@ const StageOne: React.FC<Props> = ({
allSocialCategories,
dropdownStyle,
setSelectedOrg,
selectedOrg
selectedOrg,
}) => {
const [selectedAllMSME, setSelectedAllMSME] = useState()
const [open, setOpen] = useState(false)
const [date, setDate] = useState(new Date())
const { colors, spacing } = useTheme();
const [selectedAllMSME, setSelectedAllMSME] = useState();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(new Date());
const handleDateConfirm = (selectedDate: Date) => {
setOpen(false);
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 (
<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
label="Type of Organization"
@@ -68,24 +73,26 @@ const StageOne: React.FC<Props> = ({
setSelectedOrg(item.value);
handleChange('orgType', item.value);
}}
style={dropdownStyle}
onFocus={() => setFocusedField('orgType')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'orgType'}
/>
{
selectedOrg !== 8 &&
{selectedOrg !== 8 && (
<MyInput
icon="file"
placeholder="Registration Number *"
label="Registration Number"
placeholder="Enter Registration Number"
value={formData.registrationNumber}
onChangeText={text => handleChange('registrationNumber', text)}
onFocus={() => setFocusedField('registrationNumber')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'registrationNumber'}
autoCapitalize='characters' />
}
{selectedOrg &&
autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/>
)}
{selectedOrg && (
<MyDropdown
label="Contractor Class"
data={contractorClassItems}
@@ -93,7 +100,6 @@ const StageOne: React.FC<Props> = ({
valueField="value"
placeholder="Select Class"
value={selectedOrg == 8 ? contractorClassItems[0]?.value : formData.contractorClass}
style={dropdownStyle}
disable={selectedOrg == 8 ? true : false}
onChange={item => {
handleChange('contractorClass', item.value);
@@ -101,9 +107,10 @@ const StageOne: React.FC<Props> = ({
onFocus={() => setFocusedField('contractorClass')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'contractorClass'}
/>}
/>
)}
{selectedOrg !== 8 &&
{selectedOrg !== 8 && (
<MyDropdown
label="Registration Detail (MSME/NSIC etc.)"
data={allMsmeOrgs}
@@ -112,32 +119,15 @@ const StageOne: React.FC<Props> = ({
placeholder="Select Registration Type"
value={formData.registrationDetail}
onChange={item => {
setSelectedAllMSME(item.value)
console.log('first', item)
handleChange('registrationDetail', item.value)
setSelectedAllMSME(item.value);
handleChange('registrationDetail', item.value);
}}
style={dropdownStyle}
onFocus={() => setFocusedField('registrationDetail')}
onBlur={() => setFocusedField(null)}
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
label="Social Category"
data={allSocialCategories}
@@ -146,50 +136,39 @@ const StageOne: React.FC<Props> = ({
placeholder="Select Social Category"
value={formData.socialCategory}
onChange={item => handleChange('socialCategory', item.value)}
style={dropdownStyle}
onFocus={() => setFocusedField('socialCategory')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'socialCategory'}
/>
{
selectedAllMSME !== 3 && selectedOrg !== 8 &&
<>
<MyInput
icon="calendar-alt"
placeholder="Expiry Date *"
value={formData.expiryDate}
onChangeText={text => handleChange('expiryDate', text)}
onFocus={() => setFocusedField('expiryDate')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'expiryDate'}
rightIcon='wallet'
editable={true}
rightIconPress={() => setOpen(true)}
/>
{selectedAllMSME !== 3 && selectedOrg !== 8 && (
<>
<TouchableOpacity onPress={() => setOpen(true)} activeOpacity={0.8}>
<View pointerEvents="none">
<MyInput
label="Expiry Date"
placeholder="Select Expiry Date"
value={formData.expiryDate}
onChangeText={text => handleChange('expiryDate', text)}
onFocus={() => setFocusedField('expiryDate')}
onBlur={() => setFocusedField(null)}
focused={open || focusedField === 'expiryDate'}
editable={false}
/>
</View>
</TouchableOpacity>
<DatePicker
modal
open={open}
date={date}
onConfirm={handleDateConfirm}
onCancel={() => setOpen(false)}
mode="date" // Ensure you're using date mode
mode="date"
/>
</>
}
)}
</View>
);
};
const styles = StyleSheet.create({
label: {
fontSize: 16,
fontWeight: '600',
color: colors.black,
marginBottom: 10,
},
});
export default StageOne;
@@ -1,10 +1,10 @@
import React from 'react';
import { Text } from 'react-native';
import { View } from 'react-native';
import { DropdownOption } from '../types/types';
import MyInput from '../../../Component/MyInput';
import MyInput from '../../../components/MyInput';
import MyDropdown from '../../../Component/Dropdown/MyDropdown';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface AddressDetailsSectionProps {
formData: {
@@ -21,7 +21,7 @@ interface AddressDetailsSectionProps {
cities: DropdownOption[];
fetchCity: (stateValue: number) => void;
dropdownStyle?: object;
styles?: any; // Optional styles prop for custom styling
styles?: any;
}
const StageThree: React.FC<AddressDetailsSectionProps> = ({
@@ -32,54 +32,51 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
states,
cities,
fetchCity,
styles
styles,
}) => {
const { colors, spacing } = useTheme();
const dropdownStyle = {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: '#fff',
marginBottom: 10,
};
return (
<>
<Text style={styles.label}>Address Details</Text>
<AppText variant="caption" color={colors.primary} style={styles.label}>
Address Details
</AppText>
<MyInput
icon="home"
placeholder="Enter Address *"
label="Address"
placeholder="Enter Full Address"
value={formData.address}
onChangeText={text => handleChange('address', text)}
onFocus={() => setFocusedField('address')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'address'}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
maxlength={10}
icon="phone"
placeholder="Contact Number *"
label="Contact Number"
maxLength={10}
placeholder="Enter Contact Number"
value={formData.agentContact2}
onChangeText={text => handleChange('agentContact2', text)}
onFocus={() => setFocusedField('agentContact2')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'agentContact2'}
keyboardType="numeric"
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="lock"
placeholder="Pin Code *"
label="Pin Code"
placeholder="Enter 6-digit Pin Code"
keyboardType="numeric"
maxlength={6}
maxLength={6}
value={formData.pinCode}
onChangeText={text => handleChange('pinCode', text)}
onFocus={() => setFocusedField('pinCode')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'pinCode'}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyDropdown
@@ -94,7 +91,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
handleChange('city', '');
fetchCity(item.value);
}}
style={dropdownStyle}
onFocus={() => setFocusedField('state')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'state'}
@@ -108,7 +104,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
placeholder="Select City"
value={formData.city}
onChange={item => handleChange('city', item.value)}
style={dropdownStyle}
onFocus={() => setFocusedField('city')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'city'}
@@ -1,13 +1,14 @@
import React from 'react';
import React, { useState } from 'react';
import DatePicker from 'react-native-date-picker';
import {
Text,
TouchableOpacity,
ActivityIndicator,
View,
Alert,
} from 'react-native';
import MyInput from '../../../Component/MyInput';
// Adjust the path to your custom input
import MyInput from '../../../components/MyInput';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
formData: {
@@ -21,12 +22,10 @@ interface Props {
handleChange: (key: string, value: string) => void;
focusedField: string | null;
setFocusedField: (field: string | null | any) => void;
// PAN
panVerified: boolean;
setPanVerified: (value: boolean) => void;
verifyPanHandler: () => void;
panVerifying: boolean;
// Aadhaar
aadhaarVerified: boolean;
setAadhaarVerified: (value: boolean) => void;
sendAdharOTPHandler: (aadhaarNumber: string) => void;
@@ -34,13 +33,12 @@ interface Props {
setadharotpsend: (value: boolean) => void;
verifyAdharOtpHandler: () => void;
aadhaarVerifying: boolean;
// GST
verifyGSTHandler: () => void;
gstverifying: boolean;
color: any;
styles: any;
formatDateInput: (text: string) => string;
selectedOrg:string|number
selectedOrg: string | number;
}
const StageTwo: React.FC<Props> = ({
@@ -64,61 +62,107 @@ const StageTwo: React.FC<Props> = ({
color,
styles,
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 (
<>
{/* ---------- PAN Details ---------- */}
<Text style={styles.label}>PAN Details</Text>
{/* ── PAN Details ── */}
<AppText variant="caption" color={colors.primary} style={styles.label}>
PAN Details
</AppText>
<MyInput
icon="file"
placeholder={selectedOrg == 8 ? "Pan Number *" : "Organization PAN Number *"}
label={selectedOrg == 8 ? "PAN Number" : "Organization PAN Number"}
placeholder={selectedOrg == 8 ? "Enter PAN Number" : "Enter Organization PAN"}
value={formData.orgPanNumber}
onChangeText={(text) => {
setPanVerified(false);
handleChange('orgPanNumber', text);
}}
isVarify={panVerified}
isVerify={panVerified}
onFocus={() => setFocusedField('pan')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'pan'}
editable={!panVerified}
autoCapitalize="characters"
maxlength={10}
maxLength={10}
containerStyle={{ marginBottom: spacing.s }}
/>
<TouchableOpacity
onPress={() => !panVerified && setOpen(true)}
activeOpacity={0.8}
disabled={panVerified}
>
<View pointerEvents="none">
<MyInput
label={selectedOrg == 8 ? "Date of Birth" : "Date of Incorporation"}
placeholder="DD/MM/YYYY"
value={formData.dateOfIncorp}
onChangeText={(text) => {
const formatted = formatDateInput(text);
setPanVerified(false);
handleChange('dateOfIncorp', formatted);
}}
isVerify={panVerified}
onFocus={() => setFocusedField('dob')}
onBlur={() => setFocusedField(null)}
focused={open || focusedField === 'dob'}
editable={false}
keyboardType="numeric"
containerStyle={{ marginBottom: spacing.s }}
/>
</View>
</TouchableOpacity>
<DatePicker
modal
open={open}
date={date}
onConfirm={handleDateConfirm}
onCancel={() => setOpen(false)}
mode="date"
/>
<MyInput
icon="calendar"
placeholder={selectedOrg == 8 ? "Date of Birth *" :"Date of Incorporation *"}
value={formData.dateOfIncorp}
onChangeText={(text) => {
const formatted = formatDateInput(text);
setPanVerified(false);
handleChange('dateOfIncorp', formatted);
}}
isVarify={panVerified}
onFocus={() => setFocusedField('dob')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'dob'}
editable={!panVerified}
keyboardType='numeric'
/>
<MyInput
icon="file-signature"
placeholder="PAN Name *"
label="PAN Name"
placeholder="Enter Name as on PAN"
value={formData.orgPanName}
onChangeText={(text) => {
setPanVerified(false);
handleChange('orgPanName', text);
}}
isVarify={panVerified}
isVerify={panVerified}
onFocus={() => setFocusedField('panName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'panName'}
editable={!panVerified}
autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/>
{formData.orgPanNumber &&
@@ -126,139 +170,187 @@ const StageTwo: React.FC<Props> = ({
formData.orgPanName &&
!panVerified && (
<TouchableOpacity
style={{
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
style={verifyButtonStyle}
disabled={panVerifying}
onPress={verifyPanHandler}
activeOpacity={0.8}
>
<Text style={{ color: 'white', fontWeight: '600' }}>
{panVerifying ? 'Verifying' : 'Verify PAN'}
</Text>
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
{panVerifying ? 'Verifying...' : 'Verify PAN'}
</AppText>
</TouchableOpacity>
)}
{/* ---------- Aadhaar Details ---------- */}
<Text style={styles.label}>Aadhaar Details</Text>
<MyInput
icon="address-card"
placeholder="Aadhaar Number *"
keyboardType="numeric"
maxlength={12}
value={formData.aadhaarNumber}
onChangeText={(text) => {
handleChange('aadhaarNumber', text);
setAadhaarVerified(false);
setadharotpsend(false);
handleChange('aadhaarOtp', '');
}}
isVarify={aadhaarVerified}
onFocus={() => setFocusedField('adhar')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'adhar'}
editable={!aadhaarVerified}
/>
{formData.aadhaarNumber.length === 12 && !adharotpsend && (
{panVerified && (
<TouchableOpacity
style={{
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderWidth: 1.5,
borderColor: '#E67E22',
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
backgroundColor: 'transparent',
}}
onPress={() => sendAdharOTPHandler(formData.aadhaarNumber)}
disabled={aadhaarVerifying}
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}
>
{aadhaarVerifying ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={{ color: 'white', fontWeight: '600' }}>Send OTP</Text>
)}
<AppText variant="bodySmall" color={'#E67E22'} style={{ fontWeight: '700' }}>
Change PAN
</AppText>
</TouchableOpacity>
)}
{adharotpsend && !aadhaarVerified && (
<>
<MyInput
icon="lock"
placeholder="Enter OTP *"
keyboardType="numeric"
maxlength={6}
value={formData.aadhaarOtp}
onChangeText={(text) => handleChange('aadhaarOtp', text)}
onFocus={() => setFocusedField('adharOtp')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'adharOtp'}
/>
{/* ── 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
label="Aadhaar Number"
placeholder="Enter 12-digit Aadhaar Number"
keyboardType="numeric"
maxLength={12}
value={formData.aadhaarNumber}
onChangeText={(text) => {
handleChange('aadhaarNumber', text);
setAadhaarVerified(false);
setadharotpsend(false);
handleChange('aadhaarOtp', '');
}}
isVerify={aadhaarVerified}
onFocus={() => setFocusedField('adhar')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'adhar'}
editable={!aadhaarVerified}
containerStyle={{ marginBottom: spacing.s }}
/>
{formData.aadhaarNumber.length === 12 && !adharotpsend && (
<TouchableOpacity
style={verifyButtonStyle}
onPress={() => sendAdharOTPHandler(formData.aadhaarNumber)}
disabled={aadhaarVerifying}
activeOpacity={0.8}
>
{aadhaarVerifying ? (
<ActivityIndicator color={colors.buttonText} size="small" />
) : (
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
Send OTP
</AppText>
)}
</TouchableOpacity>
)}
{adharotpsend && !aadhaarVerified && (
<>
<MyInput
label="OTP"
placeholder="Enter 6-digit OTP"
keyboardType="numeric"
maxLength={6}
value={formData.aadhaarOtp}
onChangeText={(text) => handleChange('aadhaarOtp', text)}
onFocus={() => setFocusedField('adharOtp')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'adharOtp'}
containerStyle={{ marginBottom: spacing.s }}
/>
<TouchableOpacity
style={verifyButtonStyle}
onPress={verifyAdharOtpHandler}
disabled={aadhaarVerifying}
activeOpacity={0.8}
>
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
{aadhaarVerifying ? 'Verifying...' : 'Verify OTP'}
</AppText>
</TouchableOpacity>
</>
)}
{aadhaarVerified && (
<TouchableOpacity
style={{
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderWidth: 1.5,
borderColor: '#E67E22',
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
backgroundColor: 'transparent',
}}
onPress={verifyAdharOtpHandler}
disabled={aadhaarVerifying}
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}
>
<Text style={{ color: 'white', fontWeight: '600' }}>
{aadhaarVerifying ? 'Verifying...' : 'Verify'}
</Text>
<AppText variant="bodySmall" color={'#E67E22'} style={{ fontWeight: '700' }}>
Change Aadhaar
</AppText>
</TouchableOpacity>
</>
)}
</View>
{/* ── GST Section ── */}
{selectedOrg !== 11 && (
<View style={{ marginTop: spacing.m, borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.m }}>
<MyInput
label="GSTN Number"
placeholder="Enter GSTN Number"
value={formData.gstnNo}
onChangeText={(text) => handleChange('gstnNo', text)}
onFocus={() => setFocusedField('gstNumber')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'gstNumber'}
autoCapitalize="characters"
/>
</View>
)}
{/* ---------- GST Section ---------- */}
{selectedOrg !== 11 &&
<>
<MyInput
icon="file-invoice"
placeholder="GSTN No"
value={formData.gstnNo}
onChangeText={(text) => handleChange('gstnNo', text)}
onFocus={() => setFocusedField('gstNumber')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'gstNumber'}
autoCapitalize="characters"
/>
{/* {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;
@@ -1,12 +1,12 @@
import React, { useEffect } from 'react';
import { Text, View, StyleSheet } from 'react-native';
import MyInput from '../../../Component/MyInput';
import React from 'react';
import { View } from 'react-native';
import MyInput from '../../../components/MyInput';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
formData: {
agentContact2: string;
organizationName: string;
officialWebsite: string;
@@ -14,7 +14,7 @@ interface Props {
email: string;
firstName: string;
middleName: string;
lastName: string
lastName: string;
};
handleChange: (key: string, value: string) => void;
focusedField: string | null;
@@ -29,52 +29,55 @@ const StageZero: React.FC<Props> = ({
setFocusedField,
styles,
}) => {
const { colors, spacing } = useTheme();
return (
<>
<Text style={styles.label}>Personal & Contact Details</Text>
{/* <View style={styles.row}> */}
<AppText variant="caption" color={colors.primary} style={styles.label}>
Personal & Contact Details
</AppText>
<MyInput
icon="user"
placeholder="First Name *"
label="First Name"
placeholder="First Name"
value={formData.firstName}
onChangeText={(text: string) => handleChange('firstName', text)}
onFocus={() => setFocusedField('firstname')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'firstname'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
<View style={{ width: 10 }} />
{
formData.middleName ?
<MyInput
icon="user"
placeholder="Middle Name"
value={formData.middleName}
onChangeText={(text: string) => handleChange('middleName', text)}
onFocus={() => setFocusedField('middleName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'middleName'}
editable={false}
/>
: null}
{formData.middleName ? (
<MyInput
label="Middle Name"
placeholder="Middle Name"
value={formData.middleName}
onChangeText={(text: string) => handleChange('middleName', text)}
onFocus={() => setFocusedField('middleName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'middleName'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
) : null}
<MyInput
icon="user"
placeholder="Last Name *"
label="Last Name"
placeholder="Last Name"
value={formData.lastName}
onChangeText={(text: string) => handleChange('lastName', text)}
onFocus={() => setFocusedField('lastName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'lastName'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="envelope"
placeholder="Email *"
label="Email"
placeholder="Email Address"
keyboardType="email-address"
value={formData.email}
onChangeText={(text: string) => handleChange('email', text)}
@@ -82,11 +85,12 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)}
focused={focusedField === 'email'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="phone"
placeholder="Mobile Number *"
label="Mobile Number"
placeholder="Mobile Number"
keyboardType="phone-pad"
value={formData.mobileNumber}
onChangeText={(text: string) => handleChange('mobileNumber', text)}
@@ -94,22 +98,24 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)}
focused={focusedField === 'mobileNumber'}
editable={false}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="building"
placeholder="Organization Name *"
label="Organization Name"
placeholder="Enter Organization Name"
value={formData.organizationName}
onChangeText={(text: string) => handleChange('organizationName', text)}
onFocus={() => setFocusedField('organizationName')}
onBlur={() => setFocusedField(null)}
focused={focusedField === 'organizationName'}
autoCapitalize="none"
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
icon="globe"
placeholder="Official Website (optional)"
label="Official Website"
placeholder="Website (optional)"
value={formData.officialWebsite}
onChangeText={(text: string) => handleChange('officialWebsite', text)}
onFocus={() => setFocusedField('officialWebsite')}
+276 -295
View File
@@ -1,9 +1,7 @@
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
@@ -13,16 +11,18 @@ import {
ScrollView,
ToastAndroid,
} from 'react-native';
import colors from '../../Constant/Color';
import { User, Phone, Mail, Lock, Eye, EyeOff, ArrowLeft, CheckSquare, Square } from 'lucide-react-native';
import { useGetDesignation, useRegisterContractor } from '../../Services/BBPS/Hooks';
import { User, Phone, Mail, Lock, CheckSquare, Square, Shield, UserPlus, Contact, Eye, EyeOff } from 'lucide-react-native';
import { useGetDesignation, useRegisterContractor } from '../../Services/ApiCall/Hooks';
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 AppText from '../../components/AppText';
import { Formik } from 'formik';
import * as Yup from 'yup';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { InsideHeader } from '../../Component/Index';
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
export type ContractorFormData = {
firstName: string;
@@ -40,7 +40,7 @@ export type ContractorFormData = {
type DesignationOption = {
label: string;
value: string; // string id from API
value: string;
};
const SignupSchema = Yup.object().shape({
@@ -48,7 +48,7 @@ const SignupSchema = Yup.object().shape({
firstName: Yup.string().required('First name is required.'),
middleName: Yup.string().optional(),
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()
.matches(/^[6-9]\d{9}$/, 'Invalid mobile number.')
.required('Mobile number is required.'),
@@ -65,6 +65,7 @@ const SignupSchema = Yup.object().shape({
});
const Signup = () => {
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation<any>();
const [designation, setDesignation] = useState<DesignationOption[]>([]);
const { data, isLoading: isDesignationLoading } = useGetDesignation();
@@ -74,6 +75,7 @@ const Signup = () => {
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
useEffect(() => {
const list = (data as any)?.data;
@@ -81,89 +83,15 @@ const Signup = () => {
const formattedDesignation: DesignationOption[] = list.map((item: any) => ({
label: item.designationName,
value: String(item.designationid), // e.g. "3"
value: String(item.designationid),
}));
setDesignation(formattedDesignation);
}, [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) => {
console.log('values', values);
if (!isCheck) {
Alert.alert('Check terms and condition');
Alert.alert('Terms Required', 'Please accept the terms and conditions to proceed.');
return;
}
@@ -175,7 +103,7 @@ const Signup = () => {
mobNo: values.mobileNumber.trim(),
emailAdd: values.email.trim(),
contractorType: values.bidderType,
designation: values.designation ? Number(values.designation) : 0, // convert string -> number
designation: values.designation ? Number(values.designation) : 0,
password: values.password.trim(),
otherDesignation: null,
conPassword: values.confirmPassword.trim(),
@@ -183,35 +111,37 @@ const Signup = () => {
registerContractor(formData, {
onSuccess: (res: any) => {
console.log('Register Res : ', res)
if (res?.status === '201') {
ToastAndroid.show(`${res.message}`, ToastAndroid.SHORT);
navigation.pop();
} else {
ToastAndroid.show(`Registration failed : ${res.message}`, ToastAndroid.SHORT);
ToastAndroid.show(`Registration failed: ${res.message}`, ToastAndroid.SHORT);
}
},
onError: (error: any) => {
console.log('Register Res : ', error)
ToastAndroid.show(error.message, ToastAndroid.SHORT);
},
});
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.container}>
<InsideHeader title="Sign Up" showArrow />
<View style={{ flex: 1, backgroundColor: colors.background }}>
<SafeAreaView style={{ flex: 1 }}>
<InsideHeader title="Create Account" showArrow />
<KeyboardAvoidingView
style={styles.container}
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
<Formik
initialValues={{
bidderType: '',
bidderType: 'Indian',
firstName: '',
middleName: '',
lastName: '',
designation: '', // string, not 0
designation: '',
mobileNumber: '',
email: '',
password: '',
@@ -232,174 +162,239 @@ const Signup = () => {
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
bounces={false}
showsVerticalScrollIndicator={false}
>
<Animated.View entering={FadeInUp.duration(450).delay(80)}>
<Text style={styles.label}>Bidder Type</Text>
<View style={styles.toggleContainer}>
{/* ═══ 1. Bidder Type Toggle ═══ */}
<Animated.View entering={FadeInUp.duration(400).delay(100)} style={{ marginBottom: spacing.m }}>
<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 => (
<TouchableOpacity
key={type}
style={[
styles.toggleButton,
values.bidderType === type && styles.selectedToggle,
values.bidderType === type && [styles.toggleButtonActive, { backgroundColor: colors.primary }],
]}
onPress={() => setFieldValue('bidderType', type)}
activeOpacity={0.8}
>
<Text
style={[
styles.toggleText,
values.bidderType === type && styles.selectedToggleText,
]}
<AppText
variant="bodySmall"
color={values.bidderType === type ? colors.buttonText : colors.textSecondary}
style={{ fontWeight: values.bidderType === type ? '700' : '500' }}
>
{type}
</Text>
{type} Contractor
</AppText>
</TouchableOpacity>
))}
</View>
{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 entering={FadeInUp.duration(450).delay(120)}>
<View style={styles.row}>
<View style={styles.halfInput}>
{renderInputWithIcon(
'user',
'First Name *',
values.firstName,
handleChange('firstName'),
() => 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>
{/* ═══ 2. Personal Information ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(200)} style={[styles.card, shadows.medium]}>
<View style={styles.cardHeader}>
<User size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
Personal Information
</AppText>
</View>
{renderInputWithIcon(
'user',
'Last Name *',
values.lastName,
handleChange('lastName'),
() => handleBlur('lastName'),
errors.lastName,
touched.lastName,
)}
</Animated.View>
<MyInput
label="First Name"
placeholder="Enter First Name"
value={values.firstName}
onChangeText={handleChange('firstName')}
onBlur={handleBlur('firstName')}
error={touched.firstName && errors.firstName ? errors.firstName : undefined}
rightIcon={<User size={18} color={colors.textSecondary} />}
containerStyle={{ marginBottom: spacing.s }}
/>
<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
label="Select Designation"
label="Designation"
labelField="label"
valueField="value"
data={designation}
value={values.designation}
onChange={(item: any) => {
setFieldValue('designation', item.value);
}}
placeholder={isDesignationLoading ? 'Loading...' : 'Choose Designation'}
style={dropdownStyle}
onChange={(item: any) => { setFieldValue('designation', item.value); }}
placeholder={isDesignationLoading ? 'Loading...' : 'Select 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 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(
'mail',
'Email *',
values.email,
handleChange('email'),
() => handleBlur('email'),
errors.email,
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>
{/* ═══ 3. Contact Details ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(300)} style={[styles.card, shadows.medium]}>
<View style={styles.cardHeader}>
<Contact size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
Contact Details
</AppText>
</View>
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(320)} style={styles.buttonWrapper}>
<PrimaryButton
title={isRegistering ? 'Submitting...' : 'Submit'}
disabled={isRegistering}
onPress={() => handleSubmit()}
<MyInput
label="Mobile Number"
placeholder="Enter Mobile Number"
value={values.mobileNumber}
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>
{/* ═══ 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 && (
<TermsAndConditionsModal
visible={checkModal}
onClose={() => setCheckModal(false)}
onChecked={() => setIsCheck(true)}
onChecked={() => { setIsCheck(true); setCheckModal(false); }}
/>
)}
</ScrollView>
@@ -413,99 +408,85 @@ const Signup = () => {
export default Signup;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.white,
},
// ── Styles ────────────────────────────────────────────────────────────
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
scrollContainer: {
flexGrow: 1,
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 24,
justifyContent: 'flex-start',
padding: spacing.m,
paddingBottom: 30,
},
label: {
fontSize: 15,
fontWeight: '600',
marginTop: 6,
marginBottom: 4,
color: colors.darkText,
sectionLabelRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginBottom: 6,
},
sectionLabel: {
fontWeight: '700',
fontSize: typography.size.xs,
letterSpacing: 0.5,
textTransform: 'uppercase',
},
toggleContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 4,
borderRadius: 12,
padding: 3,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
height: 46,
},
toggleButton: {
flex: 1,
paddingVertical: 10,
borderRadius: 10,
borderWidth: 1,
borderColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 4,
backgroundColor: colors.white,
borderRadius: 9,
},
selectedToggle: {
backgroundColor: colors.primary,
borderColor: colors.primary,
toggleButtonActive: {
elevation: 2,
},
toggleText: {
fontSize: 14,
color: colors.grayText,
card: {
backgroundColor: colors.surface,
borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
},
selectedToggleText: {
color: colors.white,
fontWeight: '600',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 6,
},
halfInput: {
width: '48%',
},
inputWrapper: {
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.border,
paddingHorizontal: 10,
marginTop: 8,
height: 52,
gap: 8,
marginBottom: spacing.s,
paddingBottom: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
icon: {
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
color: colors.darkText,
paddingVertical: 0,
cardTitle: {
fontWeight: '700',
fontSize: typography.size.s,
letterSpacing: 0.3,
},
errorText: {
color: colors.error,
fontSize: 12,
marginTop: 3,
marginLeft: 4,
marginTop: 4,
fontSize: typography.size.xs,
fontWeight: '500',
},
termsRow: {
termsCard: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 14,
marginHorizontal: 2,
},
termsText: {
flex: 1,
fontSize: 13,
color: colors.darkText,
marginLeft: 8,
gap: 10,
backgroundColor: colors.surface,
borderRadius: 12,
padding: 14,
marginBottom: spacing.m,
borderWidth: 1,
borderColor: colors.border,
},
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 { useNavigation, useIsFocused } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useSelector } from 'react-redux';
import { useStore } from 'react-redux'; // For hydration check
import { showToast } from '../../Constant/ShowDailog';
import { useRagistrationState } from '../../Services/BBPS/Hooks';
import { showErrorAlert, showToast } from '../../Constant/ShowDailog';
import { useRagistrationState } from '../../Services/ApiCall/Hooks';
import NavigationString from '../../Constant/NavigationString';
// Navigation type
@@ -24,34 +23,14 @@ const Splash: React.FC = () => {
const isFocused = useIsFocused();
const fadeAnim = useRef(new Animated.Value(0)).current;
const reduxToken = useSelector((state: any) => state.token?.token);
console.log("user Token",reduxToken)
const store = useStore();
const isPersistRehydrated = useSelector((state: any) => state?._persist?.rehydrated);
const { mutateAsync: registrationStateMutate } = useRagistrationState();
const [isHydrated, setIsHydrated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const hasSessionToken = Boolean(reduxToken && typeof reduxToken === 'object' && reduxToken.token);
useEffect(() => {
let unsubscribe: any;
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);
const routeByOrgType = useCallback((params: { otherId: any; orgType: any }) => {
const orgType = String(params.orgType ?? '');
if (orgType === '4' || orgType === '5' || orgType === '6') {
navigation.replace('StageThree', params);
navigation.replace(NavigationString.StageThreeScreen, params);
return;
}
@@ -61,12 +40,27 @@ const Splash: React.FC = () => {
}
navigation.replace('Dashboard');
};
}, [navigation]);
const handleGetData = async () => {
const handleGetData = useCallback(async () => {
try {
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) {
navigation.replace('Registration');
return;
@@ -75,8 +69,6 @@ const Splash: React.FC = () => {
if (res?.statusCode === 200 && res?.data) {
const { registrationStatus, contOtherId, contOrganization } = res.data;
const params = { otherId: contOtherId, orgType: contOrganization };
console.log('Registration status:', registrationStatus);
switch (registrationStatus) {
case null:
case undefined:
@@ -90,14 +82,17 @@ const Splash: React.FC = () => {
navigation.replace(NavigationString.Home);
break;
default:
navigation.replace(NavigationString.Registration);
navigation.replace(NavigationString.Login);
break;
}
return;
}
console.error('API error:', res);
showToast(res?.message || 'Something went wrong');
navigation.replace('Registration');
showErrorAlert(res?.error || 'Something went wrong. Please try re-logging in.');
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
});
} catch (error: any) {
console.error('Registration status error:', error);
showToast(error?.message || 'Something went wrong');
@@ -105,18 +100,19 @@ const Splash: React.FC = () => {
if (error?.response?.status === 404) {
navigation.replace('Registration');
} else {
await AsyncStorage.removeItem('token');
navigation.replace('Signin');
await AsyncStorage.removeItem('persist:root');
navigation.replace(NavigationString.Login);
}
}
};
}, [navigation, registrationStateMutate, routeByOrgType]);
useEffect(() => {
if (!isFocused || !isHydrated) return;
if (!isFocused || !isPersistRehydrated) {
return;
}
const runSplash = async () => {
try {
setIsLoading(true);
Animated.timing(fadeAnim, {
toValue: 1,
duration: 500,
@@ -124,7 +120,7 @@ const Splash: React.FC = () => {
}).start();
if (!reduxToken.token) {
if (!hasSessionToken) {
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
@@ -132,7 +128,7 @@ const Splash: React.FC = () => {
return;
}
if (reduxToken.userType == 'tender') {
if (reduxToken.userType === 'tender') {
await handleGetData();
} else {
navigation.reset({
@@ -144,18 +140,16 @@ const Splash: React.FC = () => {
} catch (error) {
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login || 'Signin' }],
routes: [{ name: NavigationString.Login }],
});
} finally {
setIsLoading(false);
}
};
const timeoutId = setTimeout(runSplash, 200);
return () => clearTimeout(timeoutId);
}, [isFocused, isHydrated]);
}, [fadeAnim, handleGetData, hasSessionToken, isFocused, isPersistRehydrated, navigation, reduxToken?.userType]);
if (!isHydrated) {
if (!isPersistRehydrated) {
return (
<View style={styles.loadingContainer}>
<Animated.Image
@@ -175,11 +169,6 @@ const Splash: React.FC = () => {
style={[styles.logo, { opacity: fadeAnim }]}
resizeMode="contain"
/>
{isLoading && (
<View style={{ marginTop: 20 }}>
{/* Optional spinner */}
</View>
)}
</View>
</View>
);
@@ -1,285 +1,532 @@
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 * as XLSX from 'xlsx';
import axios from 'axios';
import axios, { AxiosError } from 'axios';
import { useSelector } from 'react-redux';
import { InsideHeader } from '../../../Component/Index';
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 { encrypt, encryptWithRSA } from '../../../Services/Encryption/encrypt';
// Excel column keys
const QTY_KEY = '__EMPTY_1';
const RATE_KEY = '__EMPTY_2';
const VALUE_KEY = '__EMPTY_3';
const TOTAL_LABEL = 'TOTAL';
/* -------------------- UTILS -------------------- */
interface OfficerKey {
officersId: number;
officerPublicKey: string;
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(() => {
loadExcelFromApi();
}, []);
interface SaveEnvelopeOfficerDetail {
officerId: number;
officerEncryptedKey: string;
officerType: string;
}
// ArrayBuffer → Base64
const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
// Convert ArrayBuffer → Base64
const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return global.btoa(binary);
};
// 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 () => {
try {
setLoading(true);
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',
headers: {
Authentication: `Bearer ${token.token}`,
},
}
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
const loadExcelFromApi = async () => {
try {
setLoading(true);
const response = await axios.get(`${GET_ENVELOPE_C}${tenderData?.tenderId?.tenderId}`, {
responseType: 'arraybuffer',
headers: {
Authentication: `Bearer ${token.token}`,
},
});
console.log("excel response",response)
const base64 = arrayBufferToBase64(response.data);
const workbook = XLSX.read(base64, { type: 'base64' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: '' });
setColumns(Object.keys(rows[0] || {}));
setData(rows);
await RNFS.writeFile(
`${RNFS.DocumentDirectoryPath}/envelopeC.xlsx`,
base64,
'base64'
);
} catch (e) {
Alert.alert('Error', 'Failed to load Excel');
} finally {
setLoading(false);
}
};
// UPDATE CELL (QTY / RATE) + AUTO VALUE + AUTO TOTAL
const updateCell = (rowIndex: number, key: string, value: string) => {
const updated = [...data];
const row = updated[rowIndex];
// TOTAL row locked
if (row['DEMO DEPARTEMENT'] === TOTAL_LABEL) return;
// Allow only QTY & RATE edit
if (key === QTY_KEY || key === RATE_KEY) {
row[key] = value;
const qty = Number(row[QTY_KEY]);
const rate = Number(row[RATE_KEY]);
// 🔥 Auto VALUE = QTY × RATE
row[VALUE_KEY] =
!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);
}
setData(updated);
};
// SAVE UPDATED EXCEL
const saveUpdatedExcel = async () => {
try {
setLoading(true);
const worksheet = XLSX.utils.json_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// Generate binary string
const binary = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'binary',
});
// Convert binary string to buffer
const buffer = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
buffer[i] = binary.charCodeAt(i) & 0xff;
}
);
const filePath = `${RNFS.DocumentDirectoryPath}/envelopeC_updated.xlsx`;
// Write as binary
await RNFS.writeFile(filePath, buffer.toString(), 'ascii');
// const fileBinary = await RNFS.readFile(filePath, 'ascii');
// console.log('Excel binary content:', fileBinary);
// Alert.alert('Success', 'Excel saved successfully');
const formData = new FormData();
// requestDto payload
formData.append(
'requestDto',
JSON.stringify({
tenderId: tenderData?.tenderId?.tenderId,
deptId: tenderData?.tenderId?.deptId,
typesOfTender: tenderData?.tenderId?.typesOfTender,
fieldValue: '',
fieldValueInWord: 'zero',
})
);
// binary file
formData.append('file', {
uri: `file://${filePath}`,
name: 'envelopeC_updated.xlsx',
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
const res = await axios.post(SUBMIT_ENVELOPE_C_UPLOAD, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authentication: `Bearer ${token.token}`,
},
});
navigation.goBack();
console.log("res of updated excel file : ", res)
} catch (e) {
console.error(e);
Alert.alert('Error', 'Save failed');
} finally {
setLoading(false);
}
};
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 workbook = XLSX.read(base64, { type: 'base64' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: '' });
if (loading) {
return (
<View style={styles.loader}>
<ActivityIndicator size="large" />
</View>
);
console.log('📊 PARSED DATA - Total Rows:', rows.length);
setColumns(Object.keys(rows[0] || {}));
setData(rows);
console.log('✅ EXCEL LOADED SUCCESSFULLY\n');
} catch (e: any) {
console.log('\n❌ ERROR LOADING EXCEL');
console.error('Error:', e.message);
Alert.alert('Error', 'Failed to load Excel: ' + e.message);
} finally {
setLoading(false);
}
};
/* -------------------- UPDATE CELL -------------------- */
const updateCell = (rowIndex: number, key: string, value: string) => {
const updated = [...data];
const row = updated[rowIndex];
if (isTotalRow(row)) return;
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;
const qty = Number(row[qtyKey]);
const rate = Number(row[rateKey]);
if (valueKey) {
row[valueKey] =
!isNaN(qty) && !isNaN(rate) ? qty * rate : 0;
}
}
// 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);
};
/* -------------------- 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 () => {
try {
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 workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
const binary = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'binary',
});
const buffer = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
buffer[i] = binary.charCodeAt(i) & 0xff;
}
// 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);
// Step 3: Encrypt the entire Excel file
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 requestDto = {
tenderId: tenderData?.tenderId?.tenderId,
deptId: tenderData?.tenderId?.deptId,
typesOfTender: tenderData?.tenderId?.typesOfTender,
fieldValue: '',
fieldValueInWord: 'zero',
deviceType: 'MOBILE',
fileName: 'envelopeC_encrypted.xlsx',
saveEnvelopesOfficersDetailsResDTO: officerEncryptedKeys,
randomKeyGenerated: ENCRYPTION_KEY,
};
formData.append('requestDto', JSON.stringify(requestDto));
// Use the ENCRYPTED file for upload
formData.append('file', {
uri: `file://${encryptedFilePath}`,
name: 'envelopeC_encrypted.xlsx',
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
} as any);
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: {
'Content-Type': 'multipart/form-data',
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();
} catch (e: any) {
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 {
setLoading(false);
}
};
/* -------------------- UI -------------------- */
if (loading) {
return (
<View style={styles.container}>
<ScrollView horizontal contentContainerStyle={{ paddingHorizontal: 10, paddingTop: 20, backgroundColor: colors.white }}>
<View>
{/* HEADER */}
<View style={styles.row}>
{columns.map(col => (
<Text key={col} style={styles.headerCell}>
{col}
</Text>
))}
</View>
{/* DATA */}
<ScrollView style={{ maxHeight: 420 }}>
{data.map((row, rowIndex) => (
<View key={rowIndex} style={styles.row}>
{columns.map(key => {
const isTotal = row['DEMO DEPARTEMENT'] === TOTAL_LABEL;
const isEditable =
!isTotal && (key === QTY_KEY || key === RATE_KEY);
return (
<TextInput
key={key}
value={String(row[key])}
editable={isEditable}
keyboardType={
key === QTY_KEY || key === RATE_KEY
? 'numeric'
: 'default'
}
style={[
styles.cell,
isTotal && styles.totalCell,
key === VALUE_KEY && styles.valueCell,
]}
onChangeText={text =>
updateCell(rowIndex, key, text)
}
/>
);
})}
</View>
))}
</ScrollView>
</View>
</ScrollView>
<TouchableOpacity style={styles.button} onPress={saveUpdatedExcel}>
<Text style={styles.buttonText}>Save Updated Excel</Text>
</TouchableOpacity>
</View>
<View style={styles.loader}>
<ActivityIndicator size="large" />
</View>
);
}
const qtyKey = findColumnKey(columns, ['qty', 'quantity']);
const rateKey = findColumnKey(columns, ['rate', 'price']);
const valueKey = findColumnKey(columns, ['value', 'amount', 'total']);
return (
<View style={styles.container}>
<ScrollView horizontal contentContainerStyle={{ padding: 10 }}>
<View>
{/* HEADER */}
<View style={styles.row}>
{columns.map(col => (
<Text key={col} style={styles.headerCell}>{col}</Text>
))}
</View>
{/* DATA */}
<ScrollView style={{ maxHeight: 420 }}>
{data.map((row, rowIndex) => (
<View key={rowIndex} style={styles.row}>
{columns.map(key => {
const editable =
!isTotalRow(row) && (key === qtyKey || key === rateKey);
return (
<TextInput
key={key}
value={String(row[key])}
editable={editable}
keyboardType={editable ? 'numeric' : 'default'}
style={[
styles.cell,
isTotalRow(row) && styles.totalCell,
key === valueKey && styles.valueCell,
]}
onChangeText={text =>
updateCell(rowIndex, key, text)
}
/>
);
})}
</View>
))}
</ScrollView>
</View>
</ScrollView>
{tenderData?.buttonEnable && (
<TouchableOpacity style={styles.button} onPress={saveUpdatedExcel}>
<Text style={styles.buttonText}>Save Updated Excel</Text>
</TouchableOpacity>
)}
</View>
);
};
export default EnvelopeCExcelPreview;
/* -------------------- STYLES -------------------- */
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' },
loader: { flex: 1, justifyContent: 'center', alignItems: 'center' },
title: { fontSize: 18, fontWeight: '700', marginBottom: 10 },
row: { flexDirection: 'row' },
headerCell: {
minWidth: 140,
padding: 10,
backgroundColor: '#f1f5f9',
borderWidth: 1,
fontWeight: '700',
},
cell: {
minWidth: 140,
padding: 10,
borderWidth: 1,
borderColor: '#cbd5e1',
},
valueCell: {
backgroundColor: '#f8fafc',
},
totalCell: {
backgroundColor: '#e5e7eb',
fontWeight: '700',
},
button: {
marginTop: 15,
backgroundColor: colors.primary,
padding: 14,
borderRadius: 8,
},
buttonText: {
color: '#fff',
textAlign: 'center',
fontWeight: '600',
},
});
container: { flex: 1, backgroundColor: '#fff' },
loader: { flex: 1, justifyContent: 'center', alignItems: 'center' },
row: { flexDirection: 'row' },
headerCell: {
minWidth: 140,
padding: 10,
backgroundColor: '#f1f5f9',
borderWidth: 1,
fontWeight: '700',
},
cell: {
minWidth: 140,
padding: 10,
borderWidth: 1,
borderColor: '#cbd5e1',
},
valueCell: {
backgroundColor: '#f8fafc',
},
totalCell: {
backgroundColor: '#e5e7eb',
fontWeight: '700',
},
button: {
margin: 15,
backgroundColor: colors.primary,
padding: 14,
borderRadius: 8,
},
buttonText: {
color: '#fff',
textAlign: 'center',
fontWeight: '600',
},
});
+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;

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