updated code with dsc and device binding

This commit is contained in:
Abin Toppo
2026-05-08 13:39:41 +05:30
parent c1ecc97e29
commit 66461370a2
120 changed files with 11910 additions and 8347 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ yarn-error.log
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed. # screenshots whenever they are needed.
# For more information about the recommended setup visit: # For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/ # http://docs.fastlane.tools/best-practices/source-control/
**/fastlane/report.xml **/fastlane/report.xml
**/fastlane/Preview.html **/fastlane/Preview.html
+4
View File
@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
}
+4 -2
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { LogBox, StatusBar, View } from 'react-native'; import { LogBox, View } from 'react-native';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AlertNotificationRoot } from 'react-native-alert-notification'; import { AlertNotificationRoot } from 'react-native-alert-notification';
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
@@ -11,6 +11,7 @@ import { NavigationContainer } from '@react-navigation/native';
import colors from './src/Constant/Color'; import colors from './src/Constant/Color';
import { TextEncoder } from 'text-encoding'; import { TextEncoder } from 'text-encoding';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import AppStatusBar from './src/components/AppStatusBar';
LogBox.ignoreAllLogs(true); LogBox.ignoreAllLogs(true);
const App = () => { const App = () => {
@@ -25,7 +26,7 @@ const App = () => {
<Provider store={store}> <Provider store={store}>
<PersistGate persistor={persistor}> <PersistGate persistor={persistor}>
<View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}> <View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}>
<StatusBar barStyle="dark-content" /> <AppStatusBar />
<Routes /> <Routes />
</View> </View>
</PersistGate> </PersistGate>
@@ -38,3 +39,4 @@ const App = () => {
}; };
export default App; export default App;
+2 -2
View File
@@ -1,6 +1,6 @@
source 'https://rubygems.org' source 'http://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version # You may use http://rbenv.org/ or http://rvm.io/ to install and use this version
ruby ">= 2.6.10" ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures. # Exclude problematic versions of cocoapods and activesupport that causes build failures.
+12 -12
View File
@@ -1,8 +1,8 @@
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). This is a new [**React Native**](http://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](http://github.com/react-native-community/cli).
# Getting Started # Getting Started
> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. > **Note**: Make sure you have completed the [Set Up Your Environment](http://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
## Step 1: Start Metro ## Step 1: Start Metro
@@ -48,7 +48,7 @@ Then, and every time you update your native dependencies, run:
bundle exec pod install bundle exec pod install
``` ```
For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). For more information, please visit [CocoaPods Getting Started guide](http://guides.cocoapods.org/using/getting-started.html).
```sh ```sh
# Using npm # Using npm
@@ -66,7 +66,7 @@ This is one way to run your app — you can also build it directly from Android
Now that you have successfully run the app, let's make changes! Now that you have successfully run the app, let's make changes!
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](http://reactnative.dev/docs/fast-refresh).
When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
@@ -79,19 +79,19 @@ You've successfully run and modified your React Native App. :partying_face:
### Now what? ### Now what?
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). - If you want to add this new React Native code to an existing application, check out the [Integration guide](http://reactnative.dev/docs/integration-with-existing-apps).
- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). - If you're curious to learn more about React Native, check out the [docs](http://reactnative.dev/docs/getting-started).
# Troubleshooting # Troubleshooting
If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. If you're having issues getting the above steps to work, see the [Troubleshooting](http://reactnative.dev/docs/troubleshooting) page.
# Learn More # Learn More
To learn more about React Native, take a look at the following resources: To learn more about React Native, take a look at the following resources:
- [React Native Website](https://reactnative.dev) - learn more about React Native. - [React Native Website](http://reactnative.dev) - learn more about React Native.
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. - [Getting Started](http://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. - [Learn the Basics](http://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. - [Blog](http://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. - [`@facebook/react-native`](http://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
@@ -1,4 +1,4 @@
arguments=--init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\init\\init.gradle --init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\protobuf\\init.gradle arguments=--init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Antigravity\\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\\Antigravity\\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\\Antigravity\\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\\Antigravity\\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\\Antigravity\\User\\globalStorage\\redhat.java\\1.54.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\scala\\init.gradle
auto.sync=false auto.sync=false
build.scans.enabled=false build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
Binary file not shown.
+2 -2
View File
@@ -40,7 +40,7 @@ react {
// entryFile = file("../js/MyApplication.android.js") // entryFile = file("../js/MyApplication.android.js")
// //
// A list of extra flags to pass to the 'bundle' commands. // A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle // See http://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = [] // extraPackagerArgs = []
/* Hermes Commands */ /* Hermes Commands */
@@ -99,7 +99,7 @@ android {
} }
release { release {
// Caution! In production, you need to generate your own keystore file. // Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android. // see http://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.procure",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 24
}
+2
View File
@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@@ -22,13 +22,13 @@
<selector> <selector>
<!-- <!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I). This file is a copy of abc_edit_text_material (http://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException. The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)' NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/> <item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR. For more info, see http://bit.ly/3CdLStv (react-native/pull/29452) and http://bit.ly/3nxOMoR.
--> -->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/> <item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/> <item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
+3 -3
View File
@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
@@ -19,13 +19,13 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# AndroidX package structure to make it clearer which packages are bundled with the # AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # http://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Use this property to specify which architecture you want to build. # Use this property to specify which architecture you want to build.
# You can also override it from the CLI using # You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64 # ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 reactNativeArchitectures=armeabi-v7a,arm64-v8a
# Use this property to enable support to the new architecture. # Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in # This will allow you to use TurboModules and the Fabric render in
+4 -4
View File
@@ -7,7 +7,7 @@
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# https://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
@@ -57,10 +57,10 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # http://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at http://github.com/gradle/gradle/.
# #
############################################################################## ##############################################################################
@@ -85,7 +85,7 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (http://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
+1 -1
View File
@@ -5,7 +5,7 @@
@rem you may not use this file except in compliance with the License. @rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at @rem You may obtain a copy of the License at
@rem @rem
@rem https://www.apache.org/licenses/LICENSE-2.0 @rem http://www.apache.org/licenses/LICENSE-2.0
@rem @rem
@rem Unless required by applicable law or agreed to in writing, software @rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS, @rem distributed under the License is distributed on an "AS IS" BASIS,
+1 -1
View File
@@ -24,7 +24,7 @@ target 'procure' do
) )
post_install do |installer| post_install do |installer|
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 # http://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
react_native_post_install( react_native_post_install(
installer, installer,
config[:reactNativePath], config[:reactNativePath],
+1 -1
View File
@@ -32,7 +32,7 @@
<true/> <true/>
</dict> </dict>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string/> <string>We need your location to securely bind this device for tender bidding verification.</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
+18 -2
View File
@@ -1,3 +1,4 @@
const path = require("path");
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config"); const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
const defaultConfig = getDefaultConfig(__dirname); const defaultConfig = getDefaultConfig(__dirname);
@@ -5,7 +6,7 @@ const { assetExts, sourceExts } = defaultConfig.resolver;
/** /**
* Metro configuration * Metro configuration
* https://reactnative.dev/docs/metro * http://reactnative.dev/docs/metro
* *
* @type {import('metro-config').MetroConfig} * @type {import('metro-config').MetroConfig}
*/ */
@@ -17,7 +18,22 @@ const config = {
}, },
resolver: { resolver: {
assetExts: assetExts.filter((ext) => ext !== "svg"), assetExts: assetExts.filter((ext) => ext !== "svg"),
sourceExts: [...sourceExts, "svg"] sourceExts: [...sourceExts, "svg"],
blockList: [
// Exclude native Android build artifacts that are created/deleted
// during Gradle builds, which crash Metro's file watcher (ENOENT)
/node_modules\/.*\/android\/\.cxx\/.*/,
/node_modules\/.*\/android\/build\/.*/,
/node_modules\/.*\/android\/\.gradle\/.*/,
]
},
watcher: {
additionalExclusions: [
// Also exclude from the watcher layer for safety
"**/android/.cxx/**",
"**/android/build/**",
"**/android/.gradle/**",
]
} }
}; };
+700 -772
View File
File diff suppressed because it is too large Load Diff
+14 -10
View File
@@ -7,16 +7,18 @@
"ios": "react-native run-ios", "ios": "react-native run-ios",
"lint": "eslint .", "lint": "eslint .",
"start": "react-native start", "start": "react-native start",
"test": "jest" "test": "jest",
"create:users": "node scripts/create-users.js"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/blur": "4.4.1", "@react-native-community/blur": "4.4.1",
"@react-native-documents/picker": "11.0.3", "@react-native-documents/picker": "11.0.3",
"@react-native-masked-view/masked-view": "0.3.2", "@react-native-masked-view/masked-view": "0.2.9",
"@react-native-vector-icons/material-icons": "12.4.0", "@react-native-vector-icons/material-icons": "12.4.0",
"@react-navigation/bottom-tabs": "7.9.0", "@react-navigation/bottom-tabs": "7.9.0",
"@react-navigation/native": "7.1.26", "@react-navigation/drawer": "^7.8.1",
"@react-navigation/native": "^7.1.28",
"@react-navigation/stack": "7.6.13", "@react-navigation/stack": "7.6.13",
"@reduxjs/toolkit": "2.11.2", "@reduxjs/toolkit": "2.11.2",
"@stomp/stompjs": "7.2.1", "@stomp/stompjs": "7.2.1",
@@ -39,16 +41,18 @@
"react-native-blob-util": "0.24.6", "react-native-blob-util": "0.24.6",
"react-native-bouncy-checkbox": "4.1.2", "react-native-bouncy-checkbox": "4.1.2",
"react-native-date-picker": "5.0.13", "react-native-date-picker": "5.0.13",
"react-native-device-info": "^15.0.2",
"react-native-element-dropdown": "2.12.4", "react-native-element-dropdown": "2.12.4",
"react-native-fs": "2.20.0", "react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.30.0", "react-native-geolocation-service": "^5.3.1",
"react-native-get-random-values": "2.0.0", "react-native-gesture-handler": "^2.30.0",
"react-native-get-random-values": "1.11.0",
"react-native-gifted-charts": "1.4.70", "react-native-gifted-charts": "1.4.70",
"react-native-haptic-feedback": "2.3.3", "react-native-haptic-feedback": "2.3.3",
"react-native-linear-gradient": "2.8.3", "react-native-linear-gradient": "^2.8.3",
"react-native-modal": "14.0.0-rc.1", "react-native-modal": "^14.0.0-rc.1",
"react-native-permissions": "5.4.4", "react-native-permissions": "5.4.4",
"react-native-reanimated": "3.18.0", "react-native-reanimated": "^3.18.0",
"react-native-responsive-screen": "1.4.2", "react-native-responsive-screen": "1.4.2",
"react-native-safe-area-context": "5.6.2", "react-native-safe-area-context": "5.6.2",
"react-native-screens": "4.12.0", "react-native-screens": "4.12.0",
@@ -69,7 +73,7 @@
"@babel/preset-env": "7.25.3", "@babel/preset-env": "7.25.3",
"@babel/runtime": "7.25.0", "@babel/runtime": "7.25.0",
"@react-native-community/cli": "18.0.1", "@react-native-community/cli": "18.0.1",
"@react-native-community/cli-platform-android": "18.0.0", "@react-native-community/cli-platform-android": "^18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0", "@react-native-community/cli-platform-ios": "18.0.0",
"@react-native/babel-preset": "0.79.0", "@react-native/babel-preset": "0.79.0",
"@react-native/eslint-config": "0.79.0", "@react-native/eslint-config": "0.79.0",
@@ -81,7 +85,7 @@
"@types/react-test-renderer": "19.0.0", "@types/react-test-renderer": "19.0.0",
"@types/sockjs-client": "1.5.4", "@types/sockjs-client": "1.5.4",
"@types/text-encoding": "0.0.40", "@types/text-encoding": "0.0.40",
"eslint": "8.19.0", "eslint": "^8.57.0",
"jest": "29.6.3", "jest": "29.6.3",
"jsencrypt": "^3.5.4", "jsencrypt": "^3.5.4",
"prettier": "2.8.8", "prettier": "2.8.8",
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);
});
+1
View File
@@ -102,6 +102,7 @@ export const apiCall = async <R, D = {}>(
success: false, success: false,
message: message:
error?.response?.data?.message || 'No Response From Server', error?.response?.data?.message || 'No Response From Server',
...(error?.response?.data || {})
} as R; } as R;
} }
}; };
+76 -51
View File
@@ -1,37 +1,8 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View, StyleProp, ViewStyle, TextStyle } from 'react-native'; import { StyleSheet, Text, View, StyleProp, ViewStyle, TextStyle } from 'react-native';
import { Dropdown } from 'react-native-element-dropdown'; import { Dropdown } from 'react-native-element-dropdown';
import { Shield } from 'lucide-react-native'; import { Shield, ChevronDown } from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
const colors = {
primary: '#F97316',
primaryLight: '#FDBA74',
primaryHighlight: '#f4f1efff',
secondary: '#1B4B66',
secondaryLight: '#639599',
success: '#22C55E',
error: '#EF4444',
warning: '#F59E0B',
info: '#3B82F6',
golden: '#FFD700',
white: '#FFFFFF',
black: '#000000',
darkText: '#2B2B2B',
grayText: '#6B7280',
lightText: '#9CA3AF',
border: '#E5E7EB',
lightGray: '#F9FAFB',
disabled: '#D1D5DB',
placeholder: '#9CA3AF',
backdrop: 'rgba(0,0,0,0.5)',
shadow: 'rgba(0,0,0,0.12)',
Orange: '#F97316',
OrangeLight: '#FDBA74',
OrangeSoft: '#FFF3E8',
darkGray: '#5f6368',
lightestGray: '#f1f3f4',
successLight: '#e6f4ea',
};
type DropdownItem = { type DropdownItem = {
[key: string]: any; [key: string]: any;
@@ -74,9 +45,22 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
dropdownStyle, dropdownStyle,
...rest ...rest
}) => { }) => {
const { colors, spacing, typography, shadows } = useTheme();
return ( return (
<View style={[{ zIndex: 4000 }, containerStyle]}> <View style={[{ zIndex: 4000, marginBottom: spacing.m }, containerStyle]}>
{label && <Text style={styles.label}>{label}</Text>} {label && (
<Text style={[styles.label, {
color: colors.text,
fontSize: typography.size.s,
fontWeight: '600',
letterSpacing: 0.5,
textTransform: 'uppercase',
marginBottom: spacing.xs
}]}>
{label}
</Text>
)}
<Dropdown <Dropdown
data={data} data={data}
labelField={labelField} labelField={labelField}
@@ -86,23 +70,70 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
onChange={onChange} onChange={onChange}
style={[ style={[
styles.dropdown, styles.dropdown,
{
backgroundColor: colors.surface,
borderColor: focusedField ? colors.primary : colors.border,
borderWidth: 1.5,
borderRadius: 12,
paddingHorizontal: spacing.m,
paddingVertical: 12,
height: 56,
},
focusedField && shadows.light,
style, style,
{ borderColor: focusedField ? colors.primary : '#e0e0e0' },
]} ]}
containerStyle={[styles.dropdownContainer, dropdownStyle]} containerStyle={[
selectedTextStyle={[styles.selectedText, selectedTextStyle]} styles.dropdownContainer,
placeholderStyle={[styles.placeholderText, placeholderStyle]} {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 12,
...shadows.medium,
},
dropdownStyle
]}
selectedTextStyle={[
styles.selectedText,
{
color: colors.text,
fontSize: typography.size.m,
fontWeight: '500'
},
selectedTextStyle
]}
placeholderStyle={[
styles.placeholderText,
{
color: colors.warmGray,
fontSize: typography.size.m
},
placeholderStyle
]}
itemTextStyle={{ color: colors.textSecondary }}
activeColor={colors.background} // Beige for active item bg
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
search search
searchPlaceholder="Search Class" searchPlaceholder="Search..."
inputSearchStyle={{
borderRadius: 8,
backgroundColor: colors.background,
borderColor: colors.border,
color: colors.text
}}
renderLeftIcon={() => ( renderLeftIcon={() => (
<Shield <Shield
style={{ marginRight: 10 }} style={{ marginRight: 10 }}
color={focusedField ? colors.primary : '#aaa'} color={focusedField ? colors.primary : colors.warmGray}
size={20} size={20}
/> />
)} )}
renderRightIcon={() => (
<ChevronDown
size={20}
color={focusedField ? colors.primary : colors.warmGray}
/>
)}
{...rest} {...rest}
/> />
</View> </View>
@@ -113,25 +144,19 @@ export default MyDropdown;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
label: { label: {
marginVertical: 10, // marginVertical: 10, handled inline
color: colors.darkText,
}, },
dropdown: { dropdown: {
borderWidth: 1, // handled inline with theme
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 12,
borderColor: colors.border,
backgroundColor: colors.white,
}, },
dropdownContainer: { dropdownContainer: {
borderRadius: 12, // handled inline
borderColor: colors.border, marginTop: 4,
}, },
selectedText: { selectedText: {
color: colors.darkText, // handled inline
}, },
placeholderText: { placeholderText: {
color: colors.placeholder, // handled inline
}, },
}); });
+22 -16
View File
@@ -2,10 +2,9 @@ import React from 'react';
import { View, Text, TouchableOpacity, Image, StyleSheet, Dimensions } from 'react-native'; import { View, Text, TouchableOpacity, Image, StyleSheet, Dimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ArrowLeft } from 'lucide-react-native'; import { ArrowLeft } from 'lucide-react-native';
import colors from '../../Constant/Color'; import { useTheme } from '../../Hooks/useTheme';
import ImagePath from '../../Constant/ImagePath'; import ImagePath from '../../Constant/ImagePath';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import FontsFamily from '../../Constant/FontsFamily';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@@ -18,6 +17,7 @@ interface HeaderProps {
const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => { const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
const navigation: any = useNavigation(); const navigation: any = useNavigation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { colors, spacing, typography, shadows } = useTheme();
return ( return (
<View <View
@@ -25,6 +25,8 @@ const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
styles.container, styles.container,
{ {
paddingTop: insets.top - 10, paddingTop: insets.top - 10,
backgroundColor: colors.background, // Off-white
...shadows.light, // Soft shadow
}, },
]} ]}
> >
@@ -36,13 +38,21 @@ const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
style={styles.backButton} style={styles.backButton}
activeOpacity={0.7} activeOpacity={0.7}
> >
<ArrowLeft size={26} color={colors.black} strokeWidth={2.5} /> <ArrowLeft size={24} color={colors.primary} strokeWidth={2.5} />
</TouchableOpacity> </TouchableOpacity>
)} )}
<Text style={styles.title}>{title}</Text> <Text style={[styles.title, {
color: colors.primary,
fontSize: typography.size.xl,
fontWeight: '600',
letterSpacing: 0.5
}]}>
{title}
</Text>
</View> </View>
{/* Optional: Add Logo or Right Action */}
<TouchableOpacity onPress={() => { navigation.navigate(NavigationString.Home) }}> <TouchableOpacity onPress={() => { navigation.navigate(NavigationString.Home) }}>
<Image <Image
source={ImagePath.Icon.Logo2} source={ImagePath.Icon.Logo2}
@@ -60,25 +70,21 @@ export default InsideHeader;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
width, width,
backgroundColor: colors.white, // IMPORTANT for shadow
shadowColor: '#000', // iOS shadow
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.12,
shadowRadius: 10,
elevation: 6,
zIndex: 10, zIndex: 10,
paddingBottom: 10,
}, },
row: { row: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 10, paddingHorizontal: 16,
}, },
leftRow: { leftRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
flex: 1,
}, },
backButton: { backButton: {
@@ -88,16 +94,16 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 8, marginRight: 8,
// backgroundColor: 'rgba(0,0,0,0.03)' // Optional touch target bg
}, },
title: { title: {
fontSize: 20, flex: 1,
color: colors.black,
fontFamily: FontsFamily.poppinsSemiBold,
}, },
logo: { logo: {
height: 60, height: 48, // Slightly smaller for cleaner look
width: 60, width: 48,
opacity: 0.9,
}, },
}); });
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView, StatusBar } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView } from 'react-native';
import LottieView from 'lottie-react-native'; import LottieView from 'lottie-react-native';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import { AlertCircle, Clock, RefreshCw, Activity } from 'lucide-react-native'; import { AlertCircle, Clock, RefreshCw, Activity } from 'lucide-react-native';
@@ -10,7 +10,6 @@ import FontsFamily from '../../Constant/FontsFamily';
const ServerDown = () => { const ServerDown = () => {
return ( return (
<> <>
<StatusBar barStyle="dark-content" />
<ScrollView <ScrollView
contentContainerStyle={styles.scrollContainer} contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}> showsVerticalScrollIndicator={false}>
+4 -12
View File
@@ -1,4 +1,4 @@
import { StyleSheet, Text, TouchableOpacity, ViewStyle } from 'react-native'; import { StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
import React from 'react'; import React from 'react';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import colors from '../../Constant/Color'; import colors from '../../Constant/Color';
@@ -13,16 +13,8 @@ type PrimaryButtonProps = {
const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, disabled }) => { const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, disabled }) => {
return ( return (
<LinearGradient <View
colors={[
colors.primary, // #F97316 (Orange)
colors.primaryLight, // #FDBA74 (OrangeLight)
colors.OrangeLight, // #FDBA74 (backup)
colors.OrangeSoft // #FFF3E8 (super soft orange end)
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
locations={[0.0, 0.4, 0.7, 1.0]}
style={[styles.primaryButton, style]} style={[styles.primaryButton, style]}
> >
<TouchableOpacity <TouchableOpacity
@@ -33,7 +25,7 @@ const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, di
> >
<Text style={styles.buttonText}>{title}</Text> <Text style={styles.buttonText}>{title}</Text>
</TouchableOpacity> </TouchableOpacity>
</LinearGradient> </View>
); );
}; };
+39 -34
View File
@@ -7,7 +7,7 @@ import Animated, {
withTiming, withTiming,
interpolateColor, interpolateColor,
} from "react-native-reanimated"; } from "react-native-reanimated";
import colors from "../../Constant/Color"; import { useTheme } from "../../Hooks/useTheme";
export interface SearchType { export interface SearchType {
placeholder?: string; placeholder?: string;
@@ -15,7 +15,7 @@ export interface SearchType {
label?: string; label?: string;
filter?: boolean; filter?: boolean;
filterPress?: () => void; filterPress?: () => void;
debounceDelay?: number; // 👈 optional debounceDelay?: number;
} }
const AnimatedView = Animated.createAnimatedComponent(View); const AnimatedView = Animated.createAnimatedComponent(View);
@@ -26,8 +26,9 @@ export const SearchInput: React.FC<SearchType> = ({
label, label,
filter = true, filter = true,
filterPress, filterPress,
debounceDelay = 500, // 👈 default debounce debounceDelay = 500,
}) => { }) => {
const { colors, shadows } = useTheme();
const isFocused = useSharedValue(0); const isFocused = useSharedValue(0);
const [text, setText] = useState(''); const [text, setText] = useState('');
const debounceRef = useRef<NodeJS.Timeout | null>(null); const debounceRef = useRef<NodeJS.Timeout | null>(null);
@@ -36,18 +37,17 @@ export const SearchInput: React.FC<SearchType> = ({
const borderColor = interpolateColor( const borderColor = interpolateColor(
isFocused.value, isFocused.value,
[0, 1], [0, 1],
[colors.grayText + '50', colors.grayText] [colors.overlay, colors.primary] // Use theme overlay/primary
); );
return { return {
borderColor, borderColor,
borderWidth: withTiming(isFocused.value ? 1.5 : 1), borderWidth: withTiming(isFocused.value ? 1 : 1), // Constant thin border
transform: [{ scale: withTiming(isFocused.value ? 1.01 : 1) }], transform: [{ scale: withTiming(isFocused.value ? 1.01 : 1) }],
shadowOpacity: withTiming(isFocused.value ? 0.1 : 0),
}; };
}); });
// 🔥 Debounce logic // Debounce logic
useEffect(() => { useEffect(() => {
if (debounceRef.current) { if (debounceRef.current) {
clearTimeout(debounceRef.current); clearTimeout(debounceRef.current);
@@ -66,34 +66,47 @@ export const SearchInput: React.FC<SearchType> = ({
return ( return (
<View style={styles.container}> <View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>} {label && <Text style={[styles.label, { color: colors.text }]}>{label}</Text>}
<View style={styles.row}> <View style={styles.row}>
<AnimatedView style={[styles.inputWrapper, inputContainerStyle]}> <AnimatedView style={[
styles.inputWrapper,
{
backgroundColor: colors.glass,
...shadows.light // Use theme shadow
},
inputContainerStyle
]}>
<Search <Search
size={18} size={18}
color={colors.grayText} color={colors.primary} // Theme primary
style={styles.searchIcon} style={styles.searchIcon}
/> />
<TextInput <TextInput
style={styles.input} style={[styles.input, { color: colors.text }]}
placeholder={placeholder} placeholder={placeholder}
value={text} value={text}
onChangeText={setText} // 👈 update local state only onChangeText={setText}
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
onFocus={() => (isFocused.value = withTiming(1))} onFocus={() => (isFocused.value = withTiming(1))}
onBlur={() => (isFocused.value = withTiming(0))} onBlur={() => (isFocused.value = withTiming(0))}
selectionColor={colors.grayText} selectionColor={colors.primary}
/> />
</AnimatedView> </AnimatedView>
{filter && ( {filter && (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
style={styles.filterButton} style={[
styles.filterButton,
{
backgroundColor: colors.surface,
borderColor: colors.overlay
}
]}
onPress={filterPress} onPress={filterPress}
> >
<Filter size={22} color={colors.black} /> <Filter size={20} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
@@ -110,7 +123,6 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
marginBottom: 8, marginBottom: 8,
color: colors.black,
letterSpacing: 0.3, letterSpacing: 0.3,
}, },
row: { row: {
@@ -122,39 +134,32 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
height: 48, height: 50, // Slightly taller pill
backgroundColor: '#F9FAFB', borderRadius: 25, // Full Pill
borderRadius: 12, paddingHorizontal: 16,
paddingHorizontal: 12,
borderWidth: 1, borderWidth: 1,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowRadius: 4,
elevation: 2,
}, },
searchIcon: { searchIcon: {
marginRight: 8, marginRight: 10,
}, },
input: { input: {
flex: 1, flex: 1,
height: '100%', height: '100%',
fontSize: 15, fontSize: 15,
color: colors.black, fontWeight: '500',
paddingVertical: Platform.OS === 'ios' ? 0 : 8, paddingVertical: Platform.OS === 'ios' ? 0 : 8,
}, },
filterButton: { filterButton: {
height: 48, height: 50,
width: 48, width: 50,
backgroundColor: '#FFFFFF',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderRadius: 12, borderRadius: 25, // Circle
borderWidth: 1, borderWidth: 1,
borderColor: colors.grayText + '30',
elevation: 2, elevation: 2,
shadowColor: "#000", shadowColor: "#000",
shadowOffset: { width: 0, height: 1 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1, shadowOpacity: 0.1,
shadowRadius: 2, shadowRadius: 4,
}, },
}); });
+97 -21
View File
@@ -1,34 +1,110 @@
import { View, Dimensions } from 'react-native'; import React, { useEffect, useRef } from 'react';
import React from 'react'; import { View, Animated, StyleSheet, Dimensions } from 'react-native';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; import { useTheme } from '../../Hooks/useTheme';
const Skeleton = () => { // 1. Create a Reusable "Bone" Component
const { width } = Dimensions.get('screen'); const SkeletonBone = ({ width, height, style, borderRadius = 8 }) => {
const cardWidth = width - 30; // full width with some padding const { isDarkMode } = useTheme();
const opacity = useRef(new Animated.Value(0.3)).current;
// Animation Loop
useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 1000,
useNativeDriver: true,
}),
])
);
animation.start();
return () => animation.stop();
}, [opacity]);
// Dynamic colors based on theme
const boneColor = isDarkMode ? '#3E4145' : '#E1E9EE';
return ( return (
<View style={{ padding: 10 }}> <Animated.View
style={[
{
width: width,
height: height,
backgroundColor: boneColor,
borderRadius: borderRadius,
opacity: opacity,
},
style,
]}
/>
);
};
// 2. Main Skeleton Layout
const Skeleton = () => {
const { colors } = useTheme();
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
{[...Array(6)].map((_, index) => ( {[...Array(6)].map((_, index) => (
<SkeletonPlaceholder key={index}> <View
<SkeletonPlaceholder.Item key={index}
width={cardWidth} style={{
marginBottom={20} marginBottom: 16,
borderRadius={12} marginHorizontal: 16,
overflow="hidden" borderRadius: 20,
backgroundColor: colors.surface,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
}}
> >
{/* Image */} {/* Header Row: Tag + Price */}
<SkeletonPlaceholder.Item width={cardWidth} height={150} borderRadius={12} /> <View style={styles.row}>
<SkeletonBone width={80} height={24} borderRadius={12} />
<SkeletonBone width={60} height={24} borderRadius={4} />
</View>
{/* Title */} {/* Title (2 lines) */}
<SkeletonPlaceholder.Item marginTop={10} width={cardWidth * 0.7} height={18} borderRadius={6} /> <View style={styles.section}>
<SkeletonBone width="90%" height={18} borderRadius={4} style={{ marginBottom: 6 }} />
<SkeletonBone width="60%" height={18} borderRadius={4} />
</View>
{/* Subtitle / Price */} {/* Info Grid (2 cols) */}
<SkeletonPlaceholder.Item marginTop={6} width={cardWidth * 0.5} height={14} borderRadius={6} /> <View style={styles.row}>
</SkeletonPlaceholder.Item> <SkeletonBone width="45%" height={40} borderRadius={8} />
</SkeletonPlaceholder> <SkeletonBone width="45%" height={40} borderRadius={8} />
</View>
{/* Action Row */}
<View style={styles.row}>
<SkeletonBone width="35%" height={40} borderRadius={14} />
<SkeletonBone width="60%" height={40} borderRadius={14} />
</View>
</View>
))} ))}
</View> </View>
); );
}; };
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
section: {
marginBottom: 16,
},
});
export default Skeleton; export default Skeleton;
+105 -67
View File
@@ -1,12 +1,14 @@
import React, { useRef, useEffect } from 'react'; import React, { useMemo } from 'react';
import { import { View, StyleSheet } from 'react-native';
View, import Animated, {
Text, useSharedValue,
TouchableOpacity, useAnimatedStyle,
StyleSheet, withSpring,
Animated, withTiming,
} from 'react-native'; } from 'react-native-reanimated';
import colors from '../Constant/Color'; import { useTheme } from '../Hooks/useTheme';
import AppText from '../components/AppText';
import { Check } from 'lucide-react-native';
type StepProgressBarProps = { type StepProgressBarProps = {
currentStep: number; currentStep: number;
@@ -14,96 +16,132 @@ type StepProgressBarProps = {
onStepPress?: (stepIndex: number) => void; onStepPress?: (stepIndex: number) => void;
}; };
const STEP_LABELS = ['Info', 'Register', 'KYC', 'Address', 'Docs'];
const StepProgressBar: React.FC<StepProgressBarProps> = ({ const StepProgressBar: React.FC<StepProgressBarProps> = ({
currentStep, currentStep,
steps, steps,
onStepPress = () => { }, onStepPress = () => { },
}) => { }) => {
const animations = useRef<Animated.Value[]>( const { colors, spacing, typography, shadows } = useTheme();
steps.map(() => new Animated.Value(1)) const styles = useMemo(() => makeStyles(colors, spacing), [colors, spacing]);
).current; const totalSteps = steps.length;
useEffect(() => {
animations.forEach((anim, index) => {
Animated.timing(anim, {
toValue: index === currentStep ? 1.2 : 1,
duration: 300,
useNativeDriver: true,
}).start();
});
}, [currentStep]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
{steps.map((label, index) => { {/* Progress line background */}
<View style={styles.lineContainer}>
<View style={[styles.lineBackground, { backgroundColor: colors.border }]} />
{/* Animated fill line */}
<Animated.View
style={[
styles.lineFill,
{
backgroundColor: colors.primary,
width: `${(currentStep / (totalSteps - 1)) * 100}%`,
},
]}
/>
</View>
{/* Step circles */}
<View style={styles.stepsRow}>
{steps.map((_, index) => {
const isActive = index === currentStep; const isActive = index === currentStep;
const isCompleted = index < currentStep; const isCompleted = index < currentStep;
return ( return (
<TouchableOpacity <View key={index} style={styles.stepItem}>
key={index} <View
onPress={() => onStepPress(index)}
style={styles.stepContainer}
activeOpacity={0.8}
>
<Animated.View
style={[ style={[
styles.circle, styles.circle,
isCompleted && styles.completedCircle,
isActive && styles.activeCircle,
{ {
transform: [{ scale: animations[index] }], backgroundColor: isCompleted
? colors.primary
: isActive
? colors.primary
: colors.surface,
borderColor: isCompleted || isActive
? colors.primary
: colors.border,
transform: [{ scale: isActive ? 1.15 : 1 }],
}, },
]} isActive && { ...shadows.light },
/>
<Text
style={[
styles.label,
isCompleted && { color: '#4caf50' },
isActive && { color: '#2196f3' },
]} ]}
> >
{label} {isCompleted ? (
</Text> <Check size={14} color={colors.buttonText} strokeWidth={3} />
</TouchableOpacity> ) : (
<AppText
variant="caption"
color={isActive ? colors.buttonText : colors.textSecondary}
style={{ fontWeight: '800', fontSize: 11 }}
>
{index + 1}
</AppText>
)}
</View>
<AppText
variant="caption"
color={isActive ? colors.primary : isCompleted ? colors.primary : colors.textSecondary}
style={{
marginTop: 4,
fontWeight: isActive || isCompleted ? '700' : '500',
fontSize: typography.size.xs,
}}
>
{STEP_LABELS[index] || ''}
</AppText>
</View>
); );
})} })}
</View> </View>
</View>
); );
}; };
export default StepProgressBar; export default StepProgressBar;
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any) => StyleSheet.create({
container: { container: {
flexDirection: 'row', paddingVertical: spacing.m,
alignItems: 'center', paddingHorizontal: spacing.s,
paddingVertical: 20,
flexWrap: 'wrap',
justifyContent: 'center',
}, },
stepContainer: { lineContainer: {
position: 'absolute',
top: spacing.m + 14, // Center with circle
left: 40,
right: 40,
height: 3,
zIndex: 0,
},
lineBackground: {
position: 'absolute',
height: 3,
width: '100%',
borderRadius: 2,
},
lineFill: {
position: 'absolute',
height: 3,
borderRadius: 2,
},
stepsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
zIndex: 1,
},
stepItem: {
alignItems: 'center', alignItems: 'center',
width: 70, width: 52,
}, },
circle: { circle: {
width: 50, width: 28,
height: 5, height: 28,
borderRadius: 25, borderRadius: 14,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.primary, borderWidth: 2,
},
completedCircle: {
backgroundColor: '#4caf50',
},
activeCircle: {
backgroundColor: '#2196f3',
},
label: {
// marginTop: 6,
// fontSize: 12,
textAlign: 'center',
color: '#555',
}, },
}); });
@@ -1,14 +1,20 @@
import React, { ReactNode } from 'react'; import React, { ReactNode, useMemo } from 'react';
import { View, Text, Modal, ScrollView, TouchableOpacity, StyleSheet, Dimensions } from 'react-native'; import { View, Modal, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
import { CheckCircle, X } from 'lucide-react-native'; // Updated icons import { CheckCircle, X, FileText, Shield, Users, Brain, Lock, RefreshCw, Scale } from 'lucide-react-native';
import colors from '../../Constant/Color'; import Animated, { FadeInUp, FadeInDown, FadeIn } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
type ModalProps = { type ModalProps = {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;
onChecked: () => void onChecked: () => void;
} };
const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChecked }) => { const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChecked }) => {
const { colors, spacing, shadows, typography } = useTheme();
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
return ( return (
<Modal <Modal
animationType="slide" animationType="slide"
@@ -17,185 +23,282 @@ const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChe
onRequestClose={onClose} onRequestClose={onClose}
> >
<View style={styles.container}> <View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer}> {/* ═══ Header ═══ */}
<View style={styles.header}> <Animated.View
<Text style={styles.title}>Terms & Conditions</Text> entering={FadeInDown.duration(400).springify()}
<Text style={styles.subtitle}>Last updated: {new Date().toLocaleDateString()}</Text> style={styles.header}
>
<View style={styles.headerRow}>
<View style={{ flex: 1 }}>
<AppText variant="h3" color={colors.buttonText} style={{ fontWeight: '800' }}>
Terms & Conditions
</AppText>
<AppText variant="caption" color={`${colors.buttonText}CC`} style={{ marginTop: 4 }}>
Last updated: {new Date().toLocaleDateString()}
</AppText>
</View> </View>
<TouchableOpacity
onPress={onClose}
style={styles.closeButton}
activeOpacity={0.7}
>
<X size={20} color={colors.buttonText} />
</TouchableOpacity>
</View>
</Animated.View>
<View style={styles.content}> {/* ═══ Content ═══ */}
<Section title="1. Acceptance of Terms"> <ScrollView
<Text style={styles.paragraph}> contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<Section
title="1. Acceptance of Terms"
icon={<FileText size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={0}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
By accessing or using our services, you agree to be bound by these Terms and Conditions. By accessing or using our services, you agree to be bound by these Terms and Conditions.
If you do not agree with any part of these terms, you must not use our services. If you do not agree with any part of these terms, you must not use our services.
</Text> </AppText>
</Section> </Section>
<Section title="2. User Responsibilities"> <Section
<Text style={styles.paragraph}> title="2. User Responsibilities"
icon={<Users size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={1}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
You are responsible for maintaining the confidentiality of your account information and You are responsible for maintaining the confidentiality of your account information and
for all activities that occur under your account. You agree to notify us immediately for all activities that occur under your account. You agree to notify us immediately
of any unauthorized use of your account. of any unauthorized use of your account.
</Text> </AppText>
</Section> </Section>
<Section title="3. Intellectual Property"> <Section
<Text style={styles.paragraph}> title="3. Intellectual Property"
icon={<Brain size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={2}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
All content included on our platform, such as text, graphics, logos, and software, is All content included on our platform, such as text, graphics, logos, and software, is
the property of our company and protected by intellectual property laws. You may not the property of our company and protected by intellectual property laws. You may not
reproduce, distribute, or create derivative works without our express permission. reproduce, distribute, or create derivative works without our express permission.
</Text> </AppText>
</Section> </Section>
<Section title="4. Limitation of Liability"> <Section
<Text style={styles.paragraph}> title="4. Limitation of Liability"
icon={<Shield size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={3}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
Our company shall not be liable for any indirect, incidental, special, or consequential Our company shall not be liable for any indirect, incidental, special, or consequential
damages resulting from the use or inability to use our services, even if we have been damages resulting from the use or inability to use our services, even if we have been
advised of the possibility of such damages. advised of the possibility of such damages.
</Text> </AppText>
</Section> </Section>
<Section title="5. Privacy Policy"> <Section
<Text style={styles.paragraph}> title="5. Privacy Policy"
icon={<Lock size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={4}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
Your use of our services is also governed by our Privacy Policy, which explains how we Your use of our services is also governed by our Privacy Policy, which explains how we
collect, use, and protect your personal information. Please review our Privacy Policy collect, use, and protect your personal information. Please review our Privacy Policy
to understand our practices. to understand our practices.
</Text> </AppText>
</Section> </Section>
<Section title="6. Changes to Terms"> <Section
<Text style={styles.paragraph}> title="6. Changes to Terms"
icon={<RefreshCw size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={5}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
We reserve the right to modify these terms at any time. We will notify you of any changes We reserve the right to modify these terms at any time. We will notify you of any changes
by posting the new Terms and Conditions on this page. Your continued use of the service by posting the new Terms and Conditions on this page. Your continued use of the service
after such modifications constitutes your acceptance of the new terms. after such modifications constitutes your acceptance of the new terms.
</Text> </AppText>
</Section> </Section>
<Section title="7. Governing Law"> <Section
<Text style={styles.paragraph}> title="7. Governing Law"
icon={<Scale size={18} color={colors.primary} />}
styles={styles}
colors={colors}
spacing={spacing}
index={6}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
These Terms shall be governed by and construed in accordance with the laws of the These Terms shall be governed by and construed in accordance with the laws of the
jurisdiction in which our company is registered, without regard to its conflict of law provisions. jurisdiction in which our company is registered, without regard to its conflict of law provisions.
</Text> </AppText>
</Section> </Section>
<Text style={styles.footerText}> <Animated.View entering={FadeInUp.delay(500).springify()}>
<AppText variant="caption" color={colors.textSecondary} style={styles.footerText}>
If you have any questions about these Terms, please contact us at legal@company.com. If you have any questions about these Terms, please contact us at legal@company.com.
</Text> </AppText>
</View> </Animated.View>
{/* Accept Button */} {/* Spacer for button */}
<View style={{ height: 100 }} />
</ScrollView>
{/* ═══ Accept Button ═══ */}
<Animated.View
entering={FadeInDown.delay(300).springify()}
style={styles.acceptButtonWrapper}
>
<TouchableOpacity <TouchableOpacity
style={styles.acceptButton} style={styles.acceptButton}
onPress={() => { onPress={() => {
onClose(); onClose();
onChecked(); onChecked();
}} }}
activeOpacity={0.85}
> >
<CheckCircle color={colors.white} size={28} /> <CheckCircle color={colors.buttonText} size={22} strokeWidth={2.5} />
<Text style={styles.acceptButtonText}>I Accept</Text> <AppText variant="button" color={colors.buttonText} style={{ marginLeft: 10 }}>
I Accept
</AppText>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </Animated.View>
</View> </View>
</Modal> </Modal>
); );
}; };
// Reusable Section component
// ── Section sub-component ─────────────────────────────────────────────
type SectionProps = { type SectionProps = {
title: string; title: string;
icon: ReactNode;
children: ReactNode; children: ReactNode;
styles: any;
colors: any;
spacing: any;
index: number;
}; };
const Section: React.FC<SectionProps> = ({ title, children }) => ( const Section: React.FC<SectionProps> = ({ title, icon, children, styles, colors, spacing, index }) => (
<View style={styles.section}> <Animated.View
<Text style={styles.sectionTitle}>{title}</Text> entering={FadeInUp.delay(100 + index * 60).springify()}
{children} style={styles.section}
>
<View style={styles.sectionHeader}>
{icon}
<AppText variant="bodySmall" color={colors.text} style={styles.sectionTitle}>
{title}
</AppText>
</View> </View>
{children}
</Animated.View>
); );
const styles = StyleSheet.create({
export default TermsAndConditionsModal;
// ── Styles ────────────────────────────────────────────────────────────
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.lightGray, // Updated with your colors backgroundColor: colors.background,
},
scrollContainer: {
paddingBottom: 120, // Space for accept button
flexGrow: 1,
}, },
header: { header: {
backgroundColor: colors.primary, // Orange header backgroundColor: colors.primary,
paddingVertical: 32, paddingTop: spacing.xl,
paddingHorizontal: 24, paddingBottom: spacing.l,
borderBottomLeftRadius: 24, paddingHorizontal: spacing.l,
borderBottomRightRadius: 24, borderBottomLeftRadius: 20,
marginBottom: 24, borderBottomRightRadius: 20,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.25,
shadowRadius: 12,
elevation: 8,
}, },
title: { headerRow: {
fontSize: 28, flexDirection: 'row',
fontWeight: '800', alignItems: 'flex-start',
color: colors.white, justifyContent: 'space-between',
marginBottom: 4,
}, },
subtitle: { closeButton: {
fontSize: 14, width: 36,
color: 'rgba(255,255,255,0.85)', height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255,255,255,0.2)',
justifyContent: 'center',
alignItems: 'center',
}, },
content: { scrollContainer: {
paddingHorizontal: 24, padding: spacing.l,
paddingBottom: 40, paddingBottom: 40,
}, },
section: { section: {
marginBottom: 28, backgroundColor: colors.surface,
borderRadius: 14,
padding: spacing.m,
marginBottom: spacing.m,
borderWidth: 1,
borderColor: colors.border,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: spacing.s,
paddingBottom: spacing.s,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
}, },
sectionTitle: { sectionTitle: {
fontSize: 20,
fontWeight: '700', fontWeight: '700',
color: colors.darkText, fontSize: typography.size.s,
marginBottom: 12,
}, },
paragraph: { paragraph: {
fontSize: 16,
lineHeight: 25,
color: colors.grayText,
},
footerText: {
fontSize: 15,
fontStyle: 'italic',
color: colors.lightText,
marginTop: 24,
textAlign: 'center',
lineHeight: 22, lineHeight: 22,
}, },
acceptButton: { footerText: {
fontStyle: 'italic',
textAlign: 'center',
lineHeight: 20,
marginTop: spacing.m,
},
acceptButtonWrapper: {
position: 'absolute', position: 'absolute',
bottom: 40, bottom: 0,
left: 24, left: 0,
right: 24, right: 0,
padding: spacing.m,
paddingBottom: spacing.xl,
backgroundColor: colors.surface,
borderTopWidth: 1,
borderTopColor: colors.border,
},
acceptButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
borderRadius: 16, borderRadius: 14,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 18, paddingVertical: 16,
paddingHorizontal: 20,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 16,
elevation: 12,
},
acceptButtonText: {
color: colors.white,
fontSize: 18,
fontWeight: '700',
marginLeft: 12,
letterSpacing: 0.5,
}, },
}); });
export default TermsAndConditionsModal;
+8 -1
View File
@@ -5,6 +5,9 @@ import Diamond from '../Images/diamond.svg'
import Hill from '../Images/Svg/Hill.svg' import Hill from '../Images/Svg/Hill.svg'
const Logo = require('../Images/logo.png') const Logo = require('../Images/logo.png')
const Logo2 = require('../Images/logo2.png') const Logo2 = require('../Images/logo2.png')
const Icon = { const Icon = {
Logo, Logo,
Logo2 Logo2
@@ -15,7 +18,11 @@ const ImagePath = {
OnBoard2, OnBoard2,
OnBoard3, OnBoard3,
Diamond, Diamond,
Hill Hill,
DarkBg: require('../Images/Backgrounds/darkBackground.png'),
LightBg: require('../Images/Backgrounds/lightBackground.png'),
DarkBg2: require('../Images/Backgrounds/darkBackground2.png'),
LightBg2: require('../Images/Backgrounds/lightBackground2.png'),
} }
export default ImagePath; export default ImagePath;
+3 -1
View File
@@ -24,6 +24,8 @@ const NavigationString = {
ResetPassword:'ResetPassword', ResetPassword:'ResetPassword',
KycWebViewScreen:'KycWebViewScreen', KycWebViewScreen:'KycWebViewScreen',
StageThreeScreen:'StageThree', StageThreeScreen:'StageThree',
DecryptionScreen:'DecryptionScreen' DecryptionScreen:'DecryptionScreen',
DeviceBinding: 'DeviceBinding',
DscBinding: 'DscBinding'
} }
export default NavigationString export default NavigationString
+20
View File
@@ -0,0 +1,20 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useCreate = <T, V>(
url: string,
options?: UseMutationOptions<T, Error, V>
) => {
const queryClient = useQueryClient();
return useMutation<T, Error, V>({
mutationFn: async (variables: V) => {
const { data } = await apiClient.post<T>(url, variables);
return data;
},
onSuccess: () => {
// Invalidate queries if needed, handled by component or passed in options
},
...options,
});
};
+17
View File
@@ -0,0 +1,17 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useDelete = <T>(
url: string,
options?: UseMutationOptions<T, Error, void>
) => {
const queryClient = useQueryClient();
return useMutation<T, Error, void>({
mutationFn: async () => {
const { data } = await apiClient.delete<T>(url);
return data;
},
...options,
});
};
+17
View File
@@ -0,0 +1,17 @@
import { useQuery, UseQueryOptions, QueryKey } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useFetch = <T>(
key: QueryKey,
url: string,
options?: UseQueryOptions<T, Error>
) => {
return useQuery<T, Error>({
queryKey: key,
queryFn: async () => {
const { data } = await apiClient.get<T>(url);
return data;
},
...options,
});
};
+32
View File
@@ -0,0 +1,32 @@
import { useColorScheme } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { lightColors, darkColors, spacing, typography, shadows } from '../theme';
import { selectThemeMode, setThemeMode } from '../Redux/Slices/ThemeSlice';
export const useTheme = () => {
const systemScheme = useColorScheme();
const themeMode = useSelector(selectThemeMode);
const dispatch = useDispatch();
const isDarkMode =
themeMode === 'system'
? systemScheme === 'dark'
: themeMode === 'dark';
const themeColors = isDarkMode ? darkColors : lightColors;
const toggleTheme = (mode: 'light' | 'dark' | 'system') => {
dispatch(setThemeMode(mode));
};
return {
colors: themeColors,
isDarkMode,
themeMode,
toggleTheme,
spacing,
typography,
shadows,
};
};
+17
View File
@@ -0,0 +1,17 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useUpdate = <T, V>(
url: string,
options?: UseMutationOptions<T, Error, V>
) => {
const queryClient = useQueryClient();
return useMutation<T, Error, V>({
mutationFn: async (variables: V) => {
const { data } = await apiClient.put<T>(url, variables);
return data;
},
...options,
});
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+156 -81
View File
@@ -1,124 +1,173 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect } from 'react';
import { import {
View, View,
Text, Text,
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
Dimensions, Dimensions,
Animated, Platform,
} from 'react-native'; } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import * as LucideIcons from 'lucide-react-native'; import * as LucideIcons from 'lucide-react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
interpolate,
Extrapolation
} from 'react-native-reanimated';
import Screens from '../Screens'; import Screens from '../Screens';
import colors from '../Constant/Color';
import NavigationString from '../Constant/NavigationString'; import NavigationString from '../Constant/NavigationString';
import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview'; import { useTheme } from '../Hooks/useTheme';
import { typography } from '../theme/typography';
// import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const TAB_BAR_HEIGHT = 60; const TAB_BAR_HEIGHT = 70; // Slightly taller for premium feel
const TAB_BUTTON_WIDTH = width / 4; const TAB_BUTTON_WIDTH = width / 4;
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
const TabButton = ({ route, isFocused, onPress }: any) => { const TabButton = ({ route, isFocused, onPress }: any) => {
const icons: Record<string, { outline: any; filled: any }> = { const { colors } = useTheme();
Home: { outline: "Home", filled: "Home" }, const icons: Record<string, { outline: keyof typeof LucideIcons; filled: keyof typeof LucideIcons }> = {
Home: { outline: "House", filled: "House" },
Auction: { outline: "Gavel", filled: "Gavel" }, Auction: { outline: "Gavel", filled: "Gavel" },
Tender: { outline: "FileText", filled: "FileText" }, Tender: { outline: "FileText", filled: "FileText" },
Profile: { outline: "User", filled: "User" }, Profile: { outline: "User", filled: "User" },
}; };
const IconName = LucideIcons[isFocused ? icons[route.name].filled : icons[route.name].outline]; // Lucide icons don't always have 'filled' variants in the set, so we might use the same icon
const scaleAnim = useRef(new Animated.Value(1)).current; // or rely on color/strokeWidth to indicate state.
const translateYAnim = useRef(new Animated.Value(0)).current; const iconKey = icons[route.name]?.outline || "HelpCircle"; // Fallback
const opacityAnim = useRef(new Animated.Value(1)).current; const IconName = LucideIcons[iconKey] as React.ElementType;
const scale = useSharedValue(0);
const translateY = useSharedValue(0);
useEffect(() => { useEffect(() => {
if (isFocused) { if (isFocused) {
Animated.parallel([ scale.value = withSpring(1, { damping: 12, stiffness: 100 });
Animated.sequence([ translateY.value = withSpring(-4, { damping: 12, stiffness: 100 });
Animated.timing(translateYAnim, {
toValue: -8,
duration: 120,
useNativeDriver: true,
}),
Animated.spring(translateYAnim, {
toValue: 0,
friction: 3,
tension: 80,
useNativeDriver: true,
}),
]),
Animated.spring(scaleAnim, {
toValue: 1.3,
friction: 5,
tension: 150,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
]).start();
} else { } else {
Animated.parallel([ scale.value = withTiming(0, { duration: 200 });
Animated.spring(scaleAnim, { translateY.value = withSpring(0, { damping: 12, stiffness: 100 });
toValue: 1,
friction: 5,
tension: 150,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0.6,
duration: 150,
useNativeDriver: true,
}),
]).start();
} }
}, [isFocused]); }, [isFocused]);
const animatedPillStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
opacity: interpolate(scale.value, [0, 1], [0, 1], Extrapolation.CLAMP),
};
});
const animatedIconStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: interpolate(scale.value, [0, 1], [1, 0.9]) }], // Slight shrink on press
};
});
const animatedTextStyle = useAnimatedStyle(() => {
const opacity = interpolate(scale.value, [0, 1], [0, 1]);
const translateX = interpolate(scale.value, [0, 1], [20, 0]); // Slide in from right
return {
opacity,
transform: [{ translateX }],
display: scale.value === 0 ? 'none' : 'flex', // Hide when inactive
marginLeft: 4, // Reduced space
};
});
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={onPress} onPress={onPress}
style={[styles.tabButton, { width: TAB_BUTTON_WIDTH }]} style={styles.tabButton}
activeOpacity={0.8} activeOpacity={0.8}
> >
<Animated.View <View style={styles.tabContent}>
style={[ <Animated.View style={animatedIconStyle}>
styles.iconContainer,
{
transform: [{ scale: scaleAnim }, { translateY: translateYAnim }],
opacity: opacityAnim,
},
]}
>
{IconName && ( {IconName && (
<IconName <IconName
size={20} size={24}
color={isFocused ? colors.primary : '#888'} color={isFocused ? colors.primary : colors.text}
strokeWidth={2} strokeWidth={isFocused ? 2.5 : 2}
/> />
)} )}
</Animated.View> </Animated.View>
<Text {isFocused && (
<Animated.Text
style={[ style={[
styles.label, styles.label,
animatedTextStyle,
{ {
color: isFocused ? colors.primary : '#888', color: colors.primary,
fontWeight: isFocused ? '600' : '400', fontFamily: typography.fonts.medium,
fontWeight: undefined, // Remove fontWeight as we use fontFamily
}, },
]} ]}
numberOfLines={1}
> >
{route.name === 'BrandDay' ? 'Coupons' : route.name} {route.name === 'BrandDay' ? 'Coupons' : route.name}
</Text> </Animated.Text>
)}
</View>
</TouchableOpacity> </TouchableOpacity>
); );
}; };
const CustomTabBar = ({ state, descriptors, navigation }: any) => { const CustomTabBar = ({ state, descriptors, navigation }: any) => {
const { colors, shadows } = useTheme();
const { width: windowWidth } = Dimensions.get('window');
const TAB_BAR_WIDTH = windowWidth - 40; // 20 margin each side
const TAB_PADDING_H = 10; // 5 padding left + 5 padding right
const TAB_WIDTH = (TAB_BAR_WIDTH - TAB_PADDING_H) / state.routes.length;
const translateX = useSharedValue(0);
useEffect(() => {
translateX.value = withSpring(state.index * TAB_WIDTH, {
damping: 15,
stiffness: 150,
mass: 0.5,
});
}, [state.index]);
const animatedIndicatorStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }],
};
});
return ( return (
<View style={styles.tabBar}> <View style={[
styles.tabBar,
{
backgroundColor: colors.background, // Use transparent to allow shadow to show
shadowColor: colors.shadow, // Dynamic shadow color
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.2, // Consistent opacity
shadowRadius: 10,
borderColor: colors.border, // Subtle border for definition
borderWidth: 3,
elevation: 20,
}
]}>
<Animated.View
style={[
styles.slidingIndicator,
{
width: TAB_WIDTH,
backgroundColor: colors.primary,
opacity: 0.3,
elevation: 10, // Subtle elevation for the indicator
},
animatedIndicatorStyle,
]}
/>
{state.routes.map((route: any, index: number) => { {state.routes.map((route: any, index: number) => {
const isFocused = state.index === index; const isFocused = state.index === index;
@@ -136,8 +185,9 @@ const CustomTabBar = ({ state, descriptors, navigation }: any) => {
}; };
export default function BottomTab() { export default function BottomTab() {
const { colors } = useTheme();
return ( return (
<View style={styles.container}> <View style={[styles.container, { backgroundColor: colors.background }]}>
<Tab.Navigator <Tab.Navigator
tabBar={(props) => <CustomTabBar {...props} />} tabBar={(props) => <CustomTabBar {...props} />}
screenOptions={{ screenOptions={{
@@ -159,29 +209,54 @@ export default function BottomTab() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#fff',
}, },
tabBar: { tabBar: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
flexDirection: 'row', flexDirection: 'row',
height: TAB_BAR_HEIGHT, height: 70,
backgroundColor: '#fff', borderRadius: 35,
borderTopWidth: 1, elevation: 20, // stronger Android elevation
borderTopColor: '#eee', shadowColor: '#000', // iOS shadow color
elevation: 5, shadowOffset: { width: 0, height: 10 },
shadowOpacity: Platform.OS === 'ios' ? 0.25 : 0.2,
shadowRadius: 12,
overflow: 'visible', // allow shadow to render outside rounded corners
zIndex: 9999,
paddingBottom: 0,
justifyContent: 'space-around',
alignItems: 'center',
paddingHorizontal: 5,
}, },
tabButton: { tabButton: {
flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
height: '100%', height: '100%',
}, },
iconContainer: { tabContent: {
marginBottom: 4, flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 4, // Drastically reduced from 12
paddingVertical: 4,
borderRadius: 20,
},
slidingIndicator: {
position: 'absolute',
top: 12,
left: 5,
height: 40,
borderRadius: 30,
zIndex: 0,
}, },
label: { label: {
fontSize: 11, fontSize: 11, // Slightly smaller text
textAlign: 'center', textAlign: 'left',
includeFontPadding: false, textTransform: 'capitalize',
letterSpacing: 0.2, // Tighter spacing
}, },
}); });
+100
View File
@@ -0,0 +1,100 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, Switch, Image } from 'react-native';
import { DrawerContentScrollView, DrawerItemList } from '@react-navigation/drawer';
import { useTheme } from '../Hooks/useTheme';
import AppText from '../components/AppText';
import { Moon, Sun, Monitor } from 'lucide-react-native';
import ImagePath from '../Constant/ImagePath';
const CustomDrawer = (props: any) => {
const { colors, isDarkMode, themeMode, toggleTheme } = useTheme();
const renderThemeOption = (mode: 'light' | 'dark' | 'system', label: string, Icon: any) => {
const isActive = themeMode === mode;
return (
<TouchableOpacity
style={[
styles.themeOption,
isActive && { backgroundColor: colors.primary + '20' } // 20% opacity
]}
onPress={() => toggleTheme(mode)}
>
<Icon size={20} color={isActive ? colors.primary : colors.textSecondary} />
<AppText variant="medium" style={{ marginLeft: 10, color: isActive ? colors.primary : colors.textSecondary }}>
{label}
</AppText>
</TouchableOpacity>
);
};
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<DrawerContentScrollView {...props} contentContainerStyle={{ paddingTop: 0 }}>
{/* Header */}
<View style={[styles.header, { backgroundColor: colors.surface, borderBottomColor: colors.border }]}>
<Image source={ImagePath.Icon.Logo} style={styles.logo} resizeMode="contain" />
<AppText variant="bold" size={20} style={{ color: colors.text }}>ProcureLinc</AppText>
<AppText variant="regular" size={12} style={{ color: colors.textSecondary }}>Smart Procurement</AppText>
</View>
<View style={{ flex: 1, paddingTop: 10 }}>
<DrawerItemList {...props} />
</View>
{/* Theme Switch Section */}
<View style={[styles.section, { borderTopColor: colors.border }]}>
<AppText variant="bold" style={{ marginLeft: 16, marginBottom: 10, color: colors.text }}>Running Theme</AppText>
<View style={styles.themeRow}>
{renderThemeOption('light', 'Light', Sun)}
{renderThemeOption('dark', 'Dark', Moon)}
{renderThemeOption('system', 'System', Monitor)}
</View>
</View>
</DrawerContentScrollView>
<View style={[styles.footer, { borderTopColor: colors.border }]}>
<AppText variant="regular" size={12} style={{ color: colors.textSecondary, textAlign: 'center' }}>
Version 1.0.0
</AppText>
</View>
</View>
);
};
const styles = StyleSheet.create({
header: {
padding: 20,
paddingTop: 50, // safe area approximation
alignItems: 'center',
borderBottomWidth: 1,
},
logo: {
width: 80,
height: 80,
borderRadius: 40,
marginBottom: 10,
},
section: {
padding: 10,
borderTopWidth: 1,
marginTop: 'auto',
marginBottom: 20
},
themeRow: {
flexDirection: 'row',
justifyContent: 'space-around',
},
themeOption: {
alignItems: 'center',
justifyContent: 'center',
padding: 10,
borderRadius: 12,
flexDirection: 'row'
},
footer: {
padding: 20,
borderTopWidth: 1,
}
});
export default CustomDrawer;
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import { createDrawerNavigator } from '@react-navigation/drawer';
import BottomTab from './Bottom';
import CustomDrawer from './CustomDrawer';
import { useTheme } from '../Hooks/useTheme';
import NavigationString from '../Constant/NavigationString';
import { Dimensions } from 'react-native';
const Drawer = createDrawerNavigator();
const { width } = Dimensions.get('window');
const DrawerStack = () => {
const { colors } = useTheme();
return (
<Drawer.Navigator
drawerContent={(props) => <CustomDrawer {...props} />}
screenOptions={{
headerShown: false,
drawerType: 'slide',
drawerStyle: {
backgroundColor: colors.background,
width: width * 0.75,
},
drawerActiveTintColor: colors.primary,
drawerInactiveTintColor: colors.textSecondary,
overlayColor: 'rgba(0,0,0,0.5)',
}}
>
<Drawer.Screen
name="MainTabs"
component={BottomTab}
options={{ title: 'Home' }}
/>
{/* Add other drawer screens here that are NOT in the bottom tab */}
</Drawer.Navigator>
);
};
export default DrawerStack;
+6 -19
View File
@@ -4,35 +4,20 @@ import NavigationString from '../Constant/NavigationString';
import Screens from '../Screens'; import Screens from '../Screens';
import colors from '../Constant/Color'; import colors from '../Constant/Color';
import BottomTab from './Bottom'; import BottomTab from './Bottom';
import Login from '../Screens/Login/Login';
export default function MainStack() { export default function MainStack() {
const Stack = createStackNavigator(); const Stack = createStackNavigator();
const option = { headerShown: false }; const option = { headerShown: false };
const fadeTransition = { // ...
gestureDirection: 'horizontal',
transitionSpec: {
open: { animation: 'timing', config: { duration: 300 } },
close: { animation: 'timing', config: { duration: 300 } },
},
cardStyleInterpolator: ({ current }) => ({
cardStyle: {
opacity: current.progress,
},
}),
};
return ( return (
<Stack.Navigator <Stack.Navigator
screenOptions={{ // ...
cardStyle: { backgroundColor: colors.white },
...fadeTransition
}}
// initialRouteName={NavigationString.DecryptionScreen}
> >
<Stack.Screen name={NavigationString.Splash} component={Screens.Splash} options={option} /> <Stack.Screen name={NavigationString.Splash} component={Screens.Splash} options={option} />
<Stack.Screen name={NavigationString.Signup} component={Screens.Signup} options={option} /> <Stack.Screen name={NavigationString.Signup} component={Screens.Signup} options={option} />
<Stack.Screen name={NavigationString.Onboarding} component={Screens.Onboarding} options={option} /> <Stack.Screen name={NavigationString.Onboarding} component={Screens.Onboarding} options={option} />
<Stack.Screen name={NavigationString.Login} component={Screens.Login} options={option} /> <Stack.Screen name={NavigationString.Login} component={Login} options={option} />
<Stack.Screen name={NavigationString.Home} component={BottomTab} options={option} /> <Stack.Screen name={NavigationString.Home} component={BottomTab} options={option} />
<Stack.Screen name={NavigationString.PurchaseList} component={Screens.PurchaseList} options={option} /> <Stack.Screen name={NavigationString.PurchaseList} component={Screens.PurchaseList} options={option} />
<Stack.Screen name={NavigationString.Emd} component={Screens.Emd} options={option} /> <Stack.Screen name={NavigationString.Emd} component={Screens.Emd} options={option} />
@@ -50,6 +35,8 @@ export default function MainStack() {
<Stack.Screen name={NavigationString.KycWebViewScreen} component={Screens.KycWebViewScreen} 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.StageThreeScreen} component={Screens.StageThreeScreen} options={option} />
<Stack.Screen name={NavigationString.DecryptionScreen} component={Screens.DecryptionScreen} options={option} /> <Stack.Screen name={NavigationString.DecryptionScreen} component={Screens.DecryptionScreen} options={option} />
<Stack.Screen name={NavigationString.DeviceBinding} component={Screens.DeviceBinding} options={option} />
<Stack.Screen name={NavigationString.DscBinding} component={Screens.DscBinding} options={option} />
</Stack.Navigator> </Stack.Navigator>
); );
} }
+27
View File
@@ -0,0 +1,27 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type ThemeMode = 'light' | 'dark' | 'system';
interface ThemeState {
mode: ThemeMode;
}
const initialState: ThemeState = {
mode: 'system',
};
export const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
setThemeMode: (state, action: PayloadAction<ThemeMode>) => {
state.mode = action.payload;
},
},
});
export const { setThemeMode } = themeSlice.actions;
export const selectThemeMode = (state: any) => state.theme.mode;
export default themeSlice.reducer;
+1
View File
@@ -17,4 +17,5 @@ export const tokenSlice = createSlice({
}, },
}); });
export const { setToken, removeToken } = tokenSlice.actions; export const { setToken, removeToken } = tokenSlice.actions;
export const getToken = (state: any) => state.token.token;
export default tokenSlice.reducer; export default tokenSlice.reducer;
+3 -1
View File
@@ -7,6 +7,7 @@ import {auctionSlice} from './Slices/SelectedAuction';
import selectPurchaseAuctionReducer from './Slices/selectPurchaseAuction' import selectPurchaseAuctionReducer from './Slices/selectPurchaseAuction'
import selectPurchaseTenderReducer from './Slices/SelectedPurchaseTender'; import selectPurchaseTenderReducer from './Slices/SelectedPurchaseTender';
import { tenderSlice } from './Slices/SelectedTender'; import { tenderSlice } from './Slices/SelectedTender';
import { themeSlice } from './Slices/ThemeSlice';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
token: tokenSlice.reducer, token: tokenSlice.reducer,
@@ -14,11 +15,12 @@ const rootReducer = combineReducers({
selectPurchaseAuction: selectPurchaseAuctionReducer, selectPurchaseAuction: selectPurchaseAuctionReducer,
selectPurchaseTender: selectPurchaseTenderReducer, selectPurchaseTender: selectPurchaseTenderReducer,
tender: tenderSlice.reducer, tender: tenderSlice.reducer,
theme: themeSlice.reducer,
}); });
const persistConfig = { const persistConfig = {
key: 'root', key: 'root',
storage: AsyncStorage, storage: AsyncStorage,
whitelist: ['token'], whitelist: ['token', 'theme'],
}; };
const persistReducers = persistReducer(persistConfig, rootReducer); const persistReducers = persistReducer(persistConfig, rootReducer);
export const store = configureStore({ export const store = configureStore({
+203 -153
View File
@@ -1,22 +1,29 @@
import React, { useState, useRef, useMemo } from 'react'; import React, { useState, useRef, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Animated, Dimensions } from 'react-native'; import {
View,
StyleSheet,
TouchableOpacity,
Animated,
Dimensions,
} from 'react-native';
import { InsideHeader } from '../../Component/Index'; import { InsideHeader } from '../../Component/Index';
import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks'; import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
import colors from '../../Constant/Color';
import LiveAuction from './LiveAuction'; import LiveAuction from './LiveAuction';
import PurchaseAuction from './PurchaseAuction'; import PurchaseAuction from './PurchaseAuction';
import { ShoppingCart, Lock } from 'lucide-react-native'; import { ShoppingCart, Lock, Radio, Package } from 'lucide-react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import FontsFamily from '../../Constant/FontsFamily';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
const HORIZONTAL_PADDING = 32; const HORIZONTAL_PADDING = 32;
const SWITCH_INSETS = 2; const SWITCH_INSETS = 3;
const TAB_HEIGHT = 44; const TAB_HEIGHT = 48;
const Auction = () => { const Auction = () => {
const { colors, spacing, shadows } = useTheme();
const Navigation = useNavigation() as any; const Navigation = useNavigation() as any;
const { data: liveAuctionData = {} }: any = useFetchLiveAuction(); const { data: liveAuctionData = {} }: any = useFetchLiveAuction();
const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction(); const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction();
@@ -31,14 +38,14 @@ const Auction = () => {
? purchaseAuctionData.data.length ? purchaseAuctionData.data.length
: 0; : 0;
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('live');
const translateX = useRef(new Animated.Value(0)).current;
const TAB_WIDTH = useMemo(() => { const TAB_WIDTH = useMemo(() => {
const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2; const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2;
return availableWidth / 2.4; return availableWidth / 2;
}, []); }, []);
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('purchase');
const translateX = useRef(new Animated.Value(TAB_WIDTH)).current;
const onTabPress = (tab: 'live' | 'purchase', index: number) => { const onTabPress = (tab: 'live' | 'purchase', index: number) => {
setSelectedTab(tab); setSelectedTab(tab);
Animated.spring(translateX, { Animated.spring(translateX, {
@@ -48,252 +55,295 @@ const Auction = () => {
}).start(); }).start();
}; };
// Check if user is tender type
const isTenderUser = reduxToken?.userType?.toLowerCase() === 'tender'; const isTenderUser = reduxToken?.userType?.toLowerCase() === 'tender';
const cartCount = SelectedPurchaseList.length;
// Render restricted access message const styles = makeStyles(colors, spacing, shadows, TAB_WIDTH, TAB_HEIGHT);
const renderRestrictedAccess = () => (
if (isTenderUser) {
return (
<View style={styles.container}>
<InsideHeader title="Auction" showArrow />
<View style={styles.restrictedContainer}> <View style={styles.restrictedContainer}>
<View style={styles.restrictedCard}> <View style={styles.restrictedCard}>
<View style={styles.iconContainer}> {/* Icon ring */}
<Lock size={64} color={colors.primary} /> <View style={styles.lockRing}>
<View style={styles.lockInner}>
<Lock size={40} color={colors.primary} strokeWidth={1.8} />
</View>
</View> </View>
<Text style={styles.restrictedTitle}>Auction Access Restricted</Text> <AppText variant="h2" centered color={colors.text} style={styles.restrictedTitle as any}>
Access Restricted
</AppText>
<Text style={styles.restrictedMessage}> <AppText variant="body" centered color={colors.textSecondary} style={styles.restrictedMessage as any}>
You are currently logged in as a Tender user. Auction features are not available for your account type. You're logged in as a{' '}
</Text> <AppText variant="body" color={colors.primary} style={{ fontWeight: '700' } as any}>
Tender user
</AppText>
{'. '}Auction features are not available for your account type.
</AppText>
<Text style={styles.restrictedSubMessage}> <View style={styles.hintBox}>
To access auction features, please login with an Auction user account. <AppText variant="bodySmall" centered color={colors.textSecondary}>
</Text> Switch to an Auction account to access live & purchase auction features.
</AppText>
</View>
<TouchableOpacity <TouchableOpacity
style={styles.loginButton} style={styles.loginButton}
activeOpacity={0.85}
onPress={() => Navigation.navigate(NavigationString.Login)} onPress={() => Navigation.navigate(NavigationString.Login)}
> >
<Text style={styles.loginButtonText}>Switch to Auction Login</Text> <AppText variant="button" color={colors.buttonText} style={styles.loginBtnText as any}>
Switch to Auction Login
</AppText>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
</View>
); );
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<InsideHeader title="Auction" showArrow /> <InsideHeader title="Auction" showArrow />
{isTenderUser ? ( {/* Tab Row */}
// Show restricted access message for tender users
renderRestrictedAccess()
) : (
// Show normal auction UI for auction users
<>
<View style={styles.topRow}> <View style={styles.topRow}>
{/* Segmented Control */}
<View <View
style={[ style={[
styles.switchWrapper, styles.switchWrapper,
{ { width: TAB_WIDTH * 2 + SWITCH_INSETS * 2 },
width: TAB_WIDTH * 2 + SWITCH_INSETS * 2,
},
]} ]}
> >
{/* Sliding pill */}
<Animated.View <Animated.View
pointerEvents="none" pointerEvents="none"
style={[ style={[
styles.activeTab, styles.activeTab,
{ { width: TAB_WIDTH, transform: [{ translateX }] },
width: TAB_WIDTH,
transform: [{ translateX }],
},
]} ]}
/> />
{/* Live Tab */}
<TouchableOpacity <TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]} style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8} activeOpacity={0.8}
onPress={() => onTabPress('live', 0)} onPress={() => onTabPress('live', 0)}
> >
<Text <Radio
size={13}
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
style={[ style={[
styles.tabText, styles.tabText,
selectedTab === 'live' && styles.activeText, selectedTab === 'live' && (styles.activeTabText as any),
]} ]}
> >
Live ({liveCount}) Live {liveCount ? `(${liveCount})` : ''}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
{/* Purchase Tab */}
<TouchableOpacity <TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]} style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8} activeOpacity={0.8}
onPress={() => onTabPress('purchase', 1)} onPress={() => onTabPress('purchase', 1)}
> >
<Text <Package
size={13}
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
style={[ style={[
styles.tabText, styles.tabText,
selectedTab === 'purchase' && styles.activeText, selectedTab === 'purchase' && (styles.activeTabText as any),
]} ]}
> >
Purchased ({purchaseCount}) Purchased {purchaseCount ? `(${purchaseCount})` : ''}
</Text> </AppText>
</TouchableOpacity>
</View>
<View>
{SelectedPurchaseList.length ? (
<View
style={{
borderWidth: 2,
borderColor: colors.white,
backgroundColor: colors.primaryLight,
position: 'absolute',
borderRadius: 50,
zIndex: 20,
right: 0,
top: -10,
paddingHorizontal: 2,
}}
>
<Text style={{ fontSize: 12, color: colors.black }}>
{SelectedPurchaseList.length}
</Text>
</View>
) : null}
<TouchableOpacity
style={{
padding: 10,
backgroundColor: colors.primaryLight,
borderRadius: 20,
}}
onPress={() => {
Navigation.navigate(NavigationString.PurchaseList);
}}
>
<ShoppingCart size={22} color={colors.black} strokeWidth={2} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
{/* Content */}
{selectedTab === 'live' ? <LiveAuction /> : <PurchaseAuction />} {selectedTab === 'live' ? <LiveAuction /> : <PurchaseAuction />}
</>
{/* Cart Floating Button */}
<TouchableOpacity
style={styles.cartFloatingButton}
activeOpacity={0.9}
onPress={() => Navigation.navigate(NavigationString.PurchaseList)}
>
<ShoppingCart size={24} color={colors.buttonText} strokeWidth={2} />
{cartCount > 0 && (
<View style={styles.cartBadge}>
<AppText variant="caption" color={colors.primary} style={styles.cartBadgeText as any}>
{cartCount}
</AppText>
</View>
)} )}
</TouchableOpacity>
</View> </View>
); );
}; };
export default Auction; export default Auction;
const styles = StyleSheet.create({ /* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any, TAB_WIDTH: number, TAB_HEIGHT: number) =>
StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.white, backgroundColor: colors.background,
}, },
/* ── Tab Row ── */
topRow: { topRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 16, justifyContent: 'center',
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
}, },
switchWrapper: { switchWrapper: {
flexDirection: 'row', flexDirection: 'row',
marginVertical: 14, backgroundColor: colors.surface,
backgroundColor: '#f1f5f9', borderRadius: 16,
borderRadius: 18, padding: SWITCH_INSETS,
padding: 2,
height: TAB_HEIGHT, height: TAB_HEIGHT,
...shadows.light,
}, },
tab: { tab: {
height: TAB_HEIGHT - 4, height: TAB_HEIGHT - SWITCH_INSETS * 2,
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 5,
zIndex: 2, zIndex: 2,
}, },
tabText: { tabText: {
fontSize: 13, fontWeight: '600',
color: '#64748b', letterSpacing: 0.3,
lineHeight: 18, } as any,
fontFamily: FontsFamily.poppinsSemiBold, activeTabText: {
}, fontWeight: '700',
activeText: { } as any,
color: colors.primary,
},
activeTab: { activeTab: {
position: 'absolute', position: 'absolute',
top: 2, top: SWITCH_INSETS,
left: 2, left: SWITCH_INSETS,
height: TAB_HEIGHT - 4, height: TAB_HEIGHT - SWITCH_INSETS * 2,
backgroundColor: '#fff', backgroundColor: colors.background,
borderRadius: 16, borderRadius: 13,
shadowColor: '#000', ...shadows.medium,
shadowOpacity: 0.1,
shadowRadius: 6,
elevation: 3,
}, },
/* ── Cart Floating Button ── */
cartFloatingButton: {
position: 'absolute',
bottom: spacing.xl + 80,
right: spacing.l,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
...shadows.strong,
zIndex: 999,
},
cartBadge: {
position: 'absolute',
top: 0,
right: 0,
minWidth: 20,
height: 20,
borderRadius: 10,
backgroundColor: colors.background,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 4,
borderWidth: 1.5,
borderColor: colors.primary,
},
cartBadgeText: {
fontSize: 10,
fontWeight: '800',
lineHeight: 13,
} as any,
/* ── Restricted Access ── */
restrictedContainer: { restrictedContainer: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
padding: 24, padding: spacing.l,
backgroundColor: colors.lightGray, backgroundColor: colors.background,
}, },
restrictedCard: { restrictedCard: {
backgroundColor: colors.white, backgroundColor: colors.surface,
borderRadius: 24, borderRadius: 28,
padding: 32, padding: spacing.l,
alignItems: 'center', alignItems: 'center',
maxWidth: 400,
width: '100%', width: '100%',
maxWidth: 400,
...shadows.strong,
}, },
iconContainer: { lockRing: {
width: 120, width: 100,
height: 120, height: 100,
borderRadius: 60, borderRadius: 50,
backgroundColor: colors.primaryHighlight, backgroundColor: `${colors.primary}12`,
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginBottom: 24, 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: { restrictedTitle: {
fontSize: 24, marginBottom: spacing.s + 4,
fontWeight: '700', } as any,
color: colors.darkText,
textAlign: 'center',
marginBottom: 16,
fontFamily: FontsFamily.poppinsBold,
},
restrictedMessage: { restrictedMessage: {
fontSize: 16, marginBottom: spacing.s,
color: colors.grayText,
textAlign: 'center',
lineHeight: 24, lineHeight: 24,
marginBottom: 12, } as any,
fontFamily: FontsFamily.poppinsRegular, hintBox: {
}, backgroundColor: colors.overlay,
restrictedSubMessage: { borderRadius: 14,
fontSize: 14, paddingHorizontal: spacing.m,
color: colors.grayText, paddingVertical: spacing.s + 2,
textAlign: 'center', marginBottom: spacing.l,
lineHeight: 22, width: '100%',
marginBottom: 32,
fontFamily: FontsFamily.poppinsRegular,
}, },
loginButton: { loginButton: {
backgroundColor: colors.primary, backgroundColor: colors.buttonPrimary,
paddingHorizontal: 32, paddingHorizontal: spacing.l,
paddingVertical: 14, paddingVertical: 14,
borderRadius: 12, borderRadius: 16,
shadowColor: colors.primary, width: '100%',
shadowOffset: { width: 0, height: 4 }, alignItems: 'center',
shadowOpacity: 0.3, ...shadows.medium,
shadowRadius: 8,
elevation: 4,
},
loginButtonText: {
color: colors.white,
fontSize: 16,
fontWeight: '600',
fontFamily: FontsFamily.poppinsSemiBold,
}, },
loginBtnText: {
letterSpacing: 1.2,
} as any,
}); });
+112 -67
View File
@@ -1,55 +1,76 @@
import React, { useCallback, useState, useEffect, useRef } from 'react'; import React, { useCallback, useState, useEffect, useRef } from 'react';
import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native'; import {
View,
FlatList,
ActivityIndicator,
RefreshControl,
StyleSheet,
} from 'react-native';
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
import { RefreshCw } from 'lucide-react-native';
import LiveAuctionTile from './LiveAuctionTile'; import LiveAuctionTile from './LiveAuctionTile';
import { useFetchLiveAuction } from '../../Services/ApiCall/Hooks'; import { useFetchLiveAuction } from '../../Services/ApiCall/Hooks';
import { SearchInput } from '../../Component/Serach/SeacrhInput'; import { SearchInput } from '../../Component/Serach/SeacrhInput';
import { Skelton } from '../../Component/Index'; import { Skelton } from '../../Component/Index';
import colors from '../../Constant/Color'; import SkeltonList from '../../Component/Skelton/SkeltonList';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction'; import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { apiCall } from '../../Axios/Axios'; import { useTheme } from '../../Hooks/useTheme';
import { GET_MY_AUCTION } from '../../Services/ApiCall/ApiUrls'; import AppText from '../../components/AppText';
import SkeltonList from '../../Component/Skelton/SkeltonList';
import Skeleton from '../../Component/Skelton/Skelton';
const LiveAuction = () => { const LiveAuction = () => {
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const user = useSelector((state: any) => state.token?.token); const user = useSelector((state: any) => state.token?.token);
const onEndReachedCalledDuringMomentum = useRef(true); const onEndReachedCalledDuringMomentum = useRef(true);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const [showRefreshBanner, setShowRefreshBanner] = useState(false); const [showRefreshBanner, setShowRefreshBanner] = useState(false);
const [allBoolean, setAllBoolean] = useState({ liveAuctionData: [], isLoading: false })
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch, isPending }: any = useFetchLiveAuction();
// My Methods
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isRefetching,
refetch,
isPending,
}: any = useFetchLiveAuction();
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: any) => { ({ item, index }: any) => {
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.auctionNumber === item.auctionNumber) const isPurchase = SelectedPurchaseList.findIndex(
return <LiveAuctionTile (prev: any) => prev.auctionNumber === item.auctionNumber,
);
return (
<LiveAuctionTile
data={item} data={item}
index={index} index={index}
isPurchasedByContractor={item.contractorId?.includes(String(user.contractorId))} isPurchasedByContractor={item.contractorId?.includes(
String(user.contractorId),
)}
auctionId={item.auctionId} auctionId={item.auctionId}
auctionDispatchDate={item.auctionDispatchDate} auctionDispatchDate={item.auctionDispatchDate}
isPurchased={isPurchase !== -1 ? true : false} isPurchased={isPurchase !== -1}
purchaseEnd={item.purchaseEnd} purchaseEnd={item.purchaseEnd}
auctionOfWork={item.auctionOfWork} auctionOfWork={item.auctionOfWork}
auctionFees={item.auctionFees} auctionFees={item.auctionFees}
auctionNumber={item.auctionNumber} auctionNumber={item.auctionNumber}
onPurchasePress={() => { Dispatch(togglePurchaseAuction(item)) }} onPurchasePress={() => {
console.log('[LiveAuction] purchase toggled:', item?.auctionNumber);
Dispatch(togglePurchaseAuction(item));
}}
onDetailsPress={() => {}} onDetailsPress={() => {}}
/> />
);
}, },
[SelectedPurchaseList] [SelectedPurchaseList],
); );
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
if (onEndReachedCalledDuringMomentum.current) return; if (onEndReachedCalledDuringMomentum.current) return;
if (!hasNextPage || isFetchingNextPage) return; if (!hasNextPage || isFetchingNextPage) return;
console.log('[LiveAuction] loadMore: fetching next page');
fetchNextPage(); fetchNextPage();
onEndReachedCalledDuringMomentum.current = true; onEndReachedCalledDuringMomentum.current = true;
}, [hasNextPage, isFetchingNextPage, fetchNextPage]); }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
@@ -57,86 +78,80 @@ const LiveAuction = () => {
const renderFooter = useCallback(() => { const renderFooter = useCallback(() => {
if (!isFetchingNextPage) return null; if (!isFetchingNextPage) return null;
return ( return (
<View style={{ paddingVertical: 12 }}> <View style={styles.footerLoader}>
<ActivityIndicator size="small" color={colors.primary} /> <ActivityIndicator size="small" color={colors.primary} />
<AppText variant="caption" color={colors.textSecondary}>
Loading more…
</AppText>
</View> </View>
); );
}, [isFetchingNextPage]); }, [isFetchingNextPage, colors]);
useEffect(() => { useEffect(() => {
if (isRefetching) { if (isRefetching) {
console.log('[LiveAuction] refetch started');
setShowRefreshBanner(true); setShowRefreshBanner(true);
} else { } else {
const timer = setTimeout(() => { console.log('[LiveAuction] refetch completed');
setShowRefreshBanner(false); const timer = setTimeout(() => setShowRefreshBanner(false), 1000);
}, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isRefetching]); }, [isRefetching]);
useEffect(() => {
console.log('[LiveAuction] data updated:', {
count: data?.result?.length ?? 0,
hasNextPage,
isFetchingNextPage,
isPending,
});
}, [data, hasNextPage, isFetchingNextPage, isPending]);
if (isLoading) { if (isLoading) {
return <Skelton />; return <Skelton />;
} }
return ( return (
<View style={{ flex: 1 }}> <View style={styles.root}>
<SearchInput onChange={() => { }} placeholder="Search...." filter={false} /> {/* Search */}
<SearchInput onChange={() => {}} placeholder="Search auctions…" filter={false} />
{/* Refresh Banner */}
{showRefreshBanner && ( {showRefreshBanner && (
<Animated.View <Animated.View
entering={FadeInDown.duration(200)} entering={FadeInDown.duration(200)}
exiting={FadeOutUp.duration(500)} exiting={FadeOutUp.duration(400)}
style={{ style={[
alignSelf: 'center', styles.refreshBanner,
flexDirection: 'row', {
alignItems: 'center', backgroundColor: colors.surface,
gap: 8, ...shadows.medium,
paddingVertical: 8, },
paddingHorizontal: 14, ]}
marginTop: 6,
borderRadius: 20,
backgroundColor: '#FFFFFF',
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 8,
shadowOffset: { width: 0, height: 3 },
elevation: 4,
zIndex: 10, // Ensure it stays on top
position: 'absolute',
top: 60, // Adjust based on your SearchInput height
}}
> >
<ActivityIndicator size="small" color={colors.primary} /> <ActivityIndicator size="small" color={colors.primary} />
<Text <AppText variant="caption" color={colors.text} style={styles.refreshText as any}>
style={{ Refreshing auctions…
fontSize: 13, </AppText>
color: '#374151',
fontWeight: '500',
}}
>
Refreshing auctions
</Text>
</Animated.View> </Animated.View>
)} )}
{
isPending ? <Skeleton /> : {/* List */}
{isPending ? (
<SkeltonList />
) : (
<FlatList <FlatList
data={data?.result ?? []} data={data?.result ?? []}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item: any) => item?.auctionId?.toString()}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 50, paddingTop: 10 }} contentContainerStyle={styles.listContent}
onEndReached={loadMore} onEndReached={loadMore}
onEndReachedThreshold={0.1} onEndReachedThreshold={0.1}
onMomentumScrollBegin={() => { onMomentumScrollBegin={() => {
onEndReachedCalledDuringMomentum.current = false; onEndReachedCalledDuringMomentum.current = false;
}} }}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={isRefetching} refreshing={isRefetching}
@@ -145,18 +160,48 @@ const LiveAuction = () => {
colors={[colors.primary]} colors={[colors.primary]}
/> />
} }
initialNumToRender={6} initialNumToRender={6}
maxToRenderPerBatch={6} maxToRenderPerBatch={6}
windowSize={7} windowSize={7}
removeClippedSubviews removeClippedSubviews
/> />
} )}
</View> </View>
); );
}; };
export default LiveAuction; export default LiveAuction;
const styles = StyleSheet.create({
root: {
flex: 1,
},
listContent: {
paddingHorizontal: 16,
paddingBottom: 60,
paddingTop: 10,
},
refreshBanner: {
alignSelf: 'center',
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 8,
paddingHorizontal: 16,
marginTop: 6,
borderRadius: 20,
zIndex: 10,
position: 'absolute',
top: 60,
},
refreshText: {
fontWeight: '500',
} as any,
footerLoader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 16,
},
});
+231 -197
View File
@@ -1,13 +1,26 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import moment from 'moment'; import moment from 'moment';
import colors from '../../Constant/Color';
import { LiveAuctionTileProps } from './types/types'; import { LiveAuctionTileProps } from './types/types';
import AuctionDetail from './Modal/AuctionDetail'; import AuctionDetail from './Modal/AuctionDetail';
import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native'; import {
Tag,
Calendar,
Clock,
ShoppingCart,
Trash2,
CheckCircle2,
XCircle,
} from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import NavigationString from '../../Constant/NavigationString';
import { useNavigation } from '@react-navigation/native';
const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({ const LiveAuctionTile: React.FC<LiveAuctionTileProps & { index?: number }> = ({
data, data,
index = 0,
isPurchased = false, isPurchased = false,
onPurchasePress, onPurchasePress,
auctionDispatchDate, auctionDispatchDate,
@@ -15,231 +28,252 @@ const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({
purchaseEnd, purchaseEnd,
auctionFees, auctionFees,
auctionNumber, auctionNumber,
isPurchasedByContractor isPurchasedByContractor,
}) => { }) => {
const { colors, spacing, shadows } = useTheme();
const [showDetail, setShowDetail] = React.useState(false); const [showDetail, setShowDetail] = React.useState(false);
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss')); const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss'));
const deadlineFormatted = moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY');
const Navigation = useNavigation() as any;
const getStatus = () => {
if (isPurchasedByContractor) return 'purchased';
if (purchaseEnded) return 'expired';
if (isPurchased) return 'in_cart';
return 'active';
};
const status = getStatus();
const styles = makeStyles(colors, spacing, shadows);
if (showDetail) {
return ( return (
<View style={styles.cardWrapper}> <View style={styles.card}>
{ <AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
!showDetail ?
<>
<View
style={[
styles.statusTag,
{ backgroundColor: isPurchased ? colors.error : purchaseEnded ? '#94a3b8' : '#4ade80' },
]}
/>
<View
style={styles.container}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity style={styles.badge} onPress={() => setShowDetail(true)}>
<Tag size={14} color={colors.primary} />
<Text style={styles.badgeText}>#{auctionNumber}</Text>
</TouchableOpacity>
<Text style={styles.priceText}>₹{auctionFees}</Text>
</View> </View>
);
{/* Work Title */}
<Text style={styles.workTitle} numberOfLines={2}>
{auctionOfWork ?? 'General Auction Work'}
</Text>
{/* Info Grid */}
<View style={styles.infoGrid}>
<View style={styles.infoItem}>
<Calendar size={16} color="#64748b" />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Start</Text>
<Text style={styles.infoValue}>{auctionDispatchDate ?? '-'}</Text>
</View>
</View>
<View style={styles.infoItem}>
<Calendar size={16} color={purchaseEnded ? "#ef4444" : "#22c55e"} />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Deadline</Text>
<Text style={[styles.infoValue, purchaseEnded && { color: '#ef4444' }]}>
{moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY')}
</Text>
</View>
</View>
</View>
{/* Action Bar */}
<View style={styles.actionBar}>
<TouchableOpacity style={styles.iconBtn} onPress={() => setShowDetail(true)}>
<Info size={18} color={colors.primary} />
<Text style={styles.iconBtnText}>Details</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.mainButton,
purchaseEnded
? styles.btnDisabled
: isPurchasedByContractor ? { backgroundColor: 'lightgray' } : isPurchased
? styles.btnPurchased
: styles.btnActive,
]}
onPress={onPurchasePress}
disabled={purchaseEnded ? true : !isPurchasedByContractor ? false : true}
>
<Text
style={[
styles.mainButtonText,
isPurchased && !purchaseEnded ? { color: colors.white } : { color: '#fff' },
]}
>
{purchaseEnded ? (isPurchasedByContractor ? 'Purchased' : 'Expired') : isPurchasedByContractor ? 'Purchased' : isPurchased ? 'Remove' : 'Add to Cart'}
</Text>
{!purchaseEnded && !isPurchasedByContractor && (
<ArrowRight size={16} color={isPurchased ? colors.white : '#fff'} />
)}
</TouchableOpacity>
</View>
{/* Auction Detail Modal */}
</View>
</>
: <AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
} }
return (
<Animated.View
entering={FadeInDown.delay(index * 60).springify()}
style={styles.card}
>
{/* ── Row 1: Number + Fee ── */}
<View style={styles.topRow}>
<View style={styles.numberRow}>
<Tag size={14} color={colors.primary} strokeWidth={2} />
<AppText variant="bodySmall" color={colors.primary} style={styles.auctionNo as any}>
#{auctionNumber}
</AppText>
</View> </View>
<AppText variant="h4" color={colors.primary} style={styles.fee as any}>
₹{auctionFees}
</AppText>
</View>
{/* ── Row 2: Title ── */}
<AppText variant="h4" color={colors.text} numberOfLines={2} style={styles.title as any}>
{auctionOfWork ?? 'General Auction Work'}
</AppText>
{/* ── Row 3: Date strip ── */}
<View style={[styles.dateStrip, { backgroundColor: colors.overlay }]}>
{/* Start */}
<View style={styles.dateCol}>
<View style={styles.dateHeader}>
<Calendar size={13} color={colors.textSecondary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary} style={styles.dateLabel as any}>
Start Date
</AppText>
</View>
<AppText variant="caption" color={colors.text} style={styles.dateValue as any}>
{auctionDispatchDate ?? '—'}
</AppText>
</View>
{/* Separator */}
<View style={[styles.separator, { backgroundColor: colors.border }]} />
{/* Deadline */}
<View style={styles.dateCol}>
<View style={styles.dateHeader}>
<Clock
size={13}
color={purchaseEnded ? colors.error : colors.success}
strokeWidth={2}
/>
<AppText variant="caption" color={colors.textSecondary} style={styles.dateLabel as any}>
Deadline
</AppText>
</View>
<AppText
variant="caption"
color={purchaseEnded ? colors.error : colors.text}
style={styles.dateValue as any}
>
{deadlineFormatted}
</AppText>
</View>
</View>
{/* ── Row 4: Actions ── */}
<View style={styles.actionRow}>
{/* Ghost Details button */}
<TouchableOpacity
style={[styles.detailsBtn, { borderColor: colors.border }]}
onPress={() => setShowDetail(true)}
activeOpacity={0.7}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.detailsText as any}>
View Details
</AppText>
</TouchableOpacity>
{/* CTA */}
{status === 'active' && (
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.primary }]}
onPress={onPurchasePress}
activeOpacity={0.85}
>
<ShoppingCart size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
ADD TO CART
</AppText>
</TouchableOpacity>
)}
{status === 'in_cart' && (
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.success }]}
onPress={() => { Navigation.navigate(NavigationString.PurchaseList) }}
activeOpacity={0.85}
>
<ShoppingCart size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
GO TO CART
</AppText>
</TouchableOpacity>
)}
{status === 'purchased' && (
<View style={[styles.ctaBtn, { backgroundColor: colors.secondary }]}>
<CheckCircle2 size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
PURCHASED
</AppText>
</View>
)}
{status === 'expired' && (
<View style={[styles.ctaBtn, { backgroundColor: colors.secondary }]}>
<XCircle size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
EXPIRED
</AppText>
</View>
)}
</View>
</Animated.View>
); );
}; };
export default memo(LiveAuctionTile); export default memo(LiveAuctionTile);
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, shadows: any) =>
cardWrapper: { StyleSheet.create({
marginVertical: 10, card: {
marginHorizontal: 16, backgroundColor: colors.surface,
backgroundColor: '#fff', borderRadius: 18,
borderRadius: 24, borderWidth: 1,
flexDirection: 'row', borderColor: colors.border,
overflow: 'hidden', marginVertical: 7,
padding: spacing.m,
...shadows.medium,
},
// Modern shadow /* Top row */
shadowColor: '#000', topRow: {
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.08,
shadowRadius: 20,
elevation: 8,
},
statusTag: {
width: 6,
height: '100%',
},
container: {
flex: 1,
padding: 20,
},
header: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: spacing.s,
}, },
badge: { numberRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.primary + '25', gap: 5,
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 12,
gap: 6,
}, },
badgeText: { auctionNo: {
fontSize: 12,
fontWeight: '700', fontWeight: '700',
color: colors.primary, } as any,
}, fee: {
priceText: {
fontSize: 20,
fontWeight: '800', fontWeight: '800',
color: '#0f172a', } as any,
},
workTitle: { /* Title */
fontSize: 16, title: {
fontWeight: '600', fontWeight: '700',
color: '#334155',
lineHeight: 24, lineHeight: 24,
marginBottom: 16, marginBottom: spacing.m,
}, } as any,
infoGrid: {
flexDirection: 'row', /* Date strip */
justifyContent: 'space-between', dateStrip: {
backgroundColor: '#f0f4f8',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 16,
marginBottom: 18,
},
infoItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 10, borderRadius: 12,
paddingVertical: spacing.s + 2,
paddingHorizontal: spacing.m,
marginBottom: spacing.m,
}, },
infoTextGroup: { dateCol: {
flexDirection: 'column',
},
infoLabel: {
fontSize: 10,
color: '#94a3b8',
textTransform: 'uppercase',
fontWeight: '700',
},
infoValue: {
fontSize: 13,
fontWeight: '700',
color: '#1e293b',
},
actionBar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
},
iconBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 10,
backgroundColor: '#e2e8f0',
},
iconBtnText: {
fontSize: 14,
fontWeight: '700',
color: colors.primary,
},
mainButton: {
flex: 1, flex: 1,
height: 46, },
borderRadius: 16, dateHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginBottom: 3,
},
dateLabel: {
fontWeight: '500',
} as any,
dateValue: {
fontWeight: '700',
} as any,
separator: {
width: StyleSheet.hairlineWidth,
height: 36,
marginHorizontal: spacing.m,
},
/* Actions */
actionRow: {
flexDirection: 'row',
gap: spacing.s,
alignItems: 'center',
},
detailsBtn: {
flex: 1,
paddingVertical: 10,
borderWidth: 1,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
detailsText: {
fontWeight: '600',
} as any,
ctaBtn: {
flex: 2,
height: 44,
borderRadius: 12,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 8, gap: 6,
}, },
btnActive: { ctaText: {
backgroundColor: '#4ade80', // light green
},
btnPurchased: {
backgroundColor: colors.error,
borderWidth: 2,
borderColor: colors.error,
},
btnDisabled: {
backgroundColor: '#e2e8f0',
},
mainButtonText: {
fontSize: 14,
fontWeight: '800', fontWeight: '800',
}, letterSpacing: 0.8,
} as any,
}); });
+320 -293
View File
@@ -1,64 +1,66 @@
import React, { memo, useEffect, useState } from 'react'; import React, { memo, useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, StatusBar, Modal } from 'react-native'; import {
import { Briefcase, Calendar, FileText, Download, Eye, Layers, ArrowLeft, Clock } from 'lucide-react-native'; View,
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 { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls'; import { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
import ReactNativeBlobUtil from 'react-native-blob-util'; import ReactNativeBlobUtil from 'react-native-blob-util';
import AuctionItemDetail from './AuctionItemDetail'; import AuctionItemDetail from './AuctionItemDetail';
import { Loader } from '../../../Component/Index'; import { useTheme } from '../../../Hooks/useTheme';
import colors from '../../../Constant/Color'; import AppText from '../../../components/AppText';
import moment from 'moment'; import moment from 'moment';
interface Props { visible: boolean; onClose: () => void; data?: any } interface Props { visible: boolean; onClose: () => void; data?: any }
const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => { const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets();
const { colors, spacing, shadows } = useTheme();
const inset = useSafeAreaInsets() const { data: auctionData } = useGetAuciton({ auctionId: data?.auctionId });
const { data: auctionData, isLoading } = useGetAuciton({ auctionId: data?.auctionId }) const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData);
const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData) const [allBoolean, setAllBoolean] = useState({
const [allBoolean, setAllBoolean] = useState({ showItemDetail: false, selectedItem: {}, fileLoading: false }); showItemDetail: false,
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId }) selectedItem: {},
const { mutate } = useAuctionItem() fileLoading: false,
});
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId });
const { mutate } = useAuctionItem();
const formatDate = (date?: string) => { const formatDate = (date?: string) => {
if (!date) return '-'; if (!date) return '—';
return moment(date, 'DD-MM-YYYY HH:mm:ss').format('DD MMM YYYY, hh:mm A'); return moment(date, 'DD-MM-YYYY HH:mm:ss').format('DD MMM YYYY, hh:mm A');
}; };
const isLive = data?.purchaseEnd ? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss')) : false; const isLive = data?.purchaseEnd
? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss'))
const Section = ({ title, icon: Icon, children }: any) => ( : false;
<View style={styles.sectionContainer}>
<View style={styles.sectionHeader}>
<Icon size={20} color={colors.primary} />
<Text style={styles.sectionTitle}>{title}</Text>
</View>
{children}
</View>
);
const InfoBox = ({ label, value, full = false }: any) => (
<View style={[styles.infoBox, full ? { width: '100%' } : { width: '48%' }]}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value || '-'}</Text>
</View>
);
const handleDownload = async (id: number, fileName: string) => { const handleDownload = async (id: number, fileName: string) => {
try { try {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: true })); setAllBoolean(prev => ({ ...prev, fileLoading: true }));
const { dirs } = ReactNativeBlobUtil.fs; const { dirs } = ReactNativeBlobUtil.fs;
const path = `${dirs.DownloadDir}/${fileName}.pdf`; const path = `${dirs.DownloadDir}/${fileName}.pdf`;
const downloadUrl = `${AUCTION_DOCUMENT_DOWNLOAD}?documentId=${id}&isAmendment=false`; const downloadUrl = `${AUCTION_DOCUMENT_DOWNLOAD}?documentId=${id}&isAmendment=false`;
await ReactNativeBlobUtil.config({
const response = await ReactNativeBlobUtil.config({
fileCache: false, fileCache: false,
addAndroidDownloads: { addAndroidDownloads: {
useDownloadManager: true, useDownloadManager: true,
@@ -69,315 +71,340 @@ const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
description: 'Downloading document', description: 'Downloading document',
mediaScannable: true, mediaScannable: true,
}, },
}).fetch('GET', downloadUrl, { }).fetch('GET', downloadUrl, { Accept: 'application/pdf' });
Accept: 'application/pdf',
});
const stat = await ReactNativeBlobUtil.fs.stat(path);
} catch (error: any) { } catch (error: any) {
console.log('Error:', error?.message || error); console.log('Error:', error?.message || error);
} finally { } finally {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: false })); setAllBoolean(prev => ({ ...prev, fileLoading: false }));
} }
}; };
useEffect(() => { useEffect(() => {
mutate({ auctionNumber: data?.auctionNumber, status: data?.auctionPattern }, { mutate(
onSuccess: (res) => { { auctionNumber: data?.auctionNumber, status: data?.auctionPattern },
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments })) {
}, onError: (err) => { onSuccess: (res: any) => {
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments }));
},
onError: (err: any) => {
console.log(err, '-----auction item error'); console.log(err, '-----auction item error');
} },
}); },
);
}, [data.auctionNumber, auctionDocuments]); }, [data.auctionNumber, auctionDocuments]);
const isAmended = auctionData?.auctionStatus === 'Amended';
const kd = (key: any, amended: any) => isAmended ? amended : key;
const styles = makeStyles(colors, spacing, shadows);
return ( return (
<Modal <Modal
visible={visible} visible={visible}
transparent={false} transparent={false}
animationType="none" animationType="slide"
statusBarTranslucent statusBarTranslucent
onRequestClose={onClose} onRequestClose={onClose}
> >
<View style={styles.safeArea}> <View style={styles.root}>
<StatusBar barStyle="dark-content" /> {/* ── Header ── */}
<View style={[styles.header, { paddingTop: inset.top + 6 }]}>
{/* Full Header */} <TouchableOpacity onPress={onClose} style={styles.backBtn} activeOpacity={0.7}>
<View style={[styles.header, { paddingTop: inset.top }]}> <ArrowLeft size={22} color={colors.text} strokeWidth={2.5} />
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" />
</TouchableOpacity> </TouchableOpacity>
<View style={styles.headerTitleContainer}>
<Text style={styles.headerTitleText}>Auction Overview</Text> <View style={styles.headerCenter}>
<Text style={styles.headerIdText}>Ref: {data?.auctionNumber || 'N/A'}</Text> <AppText variant="h4" color={colors.text} style={styles.headerTitle as any}>
Auction Overview
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Ref: {data?.auctionNumber ?? 'N/A'}
</AppText>
</View> </View>
<View style={styles.liveIndicator}>
<View style={[styles.dot, { backgroundColor: isLive ? '#22c55e' : '#94a3b8' }]} /> <View style={[styles.statusChip, { borderColor: isLive ? `${colors.success}50` : colors.border }]}>
<Text style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</Text> <View style={[styles.statusDot, { backgroundColor: isLive ? colors.success : colors.textSecondary }]} />
<AppText variant="caption" color={isLive ? colors.success : colors.textSecondary} style={styles.statusText as any}>
{isLive ? 'LIVE' : 'CLOSED'}
</AppText>
</View> </View>
</View> </View>
{/* Scrollable Body */} {/* ── Body ── */}
<ScrollView <ScrollView
style={styles.scrollView} showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={true} style={{ backgroundColor: colors.background }}
> >
{/* 1. Basic Info Grid */}
<Section title="Details" icon={Layers}>
<View style={styles.grid}>
<InfoBox label="Auction No." value={data?.auctionNumber} />
<InfoBox label="Pattern" value={data?.auctionPattern || 'Forward'} />
<InfoBox label="Mode" value={data?.auctionMode || 'Open'} />
<InfoBox label="Category" value={data?.categoryOfAuction || 'General'} />
<InfoBox label="EMD Type" value={data?.emd || 'Online'} />
<InfoBox label="EMD Figure" value={data?.emdFigure || 'Item Wise'} />
</View>
</Section>
{/* 2. Work Description */} {/* BASIC DETAILS */}
<Section title="Auction Details" icon={Briefcase}> <SectionCard title="Basic Details" icon={Layers} colors={colors} spacing={spacing} shadows={shadows}>
<View style={styles.grid}> <InfoRow label="Auction No." value={data?.auctionNumber} colors={colors} />
<InfoBox label="Auction Name" value={auctionData?.auctionOfWork} /> <InfoRow label="Pattern" value={data?.auctionPattern || 'Forward'} colors={colors} />
<InfoBox label="Category Of Auction " value={auctionData?.auctionCategories} /> <InfoRow label="Mode" value={data?.auctionMode || 'Open'} colors={colors} />
<InfoBox label="Auction Fees (In Figure) " value={auctionData?.auctionFees} /> <InfoRow label="Category" value={data?.categoryOfAuction || 'General'} colors={colors} />
<InfoBox label="Auction Fees (In Words)" value={auctionData?.auctionFeesInWord} /> <InfoRow label="EMD Type" value={data?.emd || 'Online'} colors={colors} last />
<InfoBox label="Auction Keyword" value={auctionData?.auctionKeywordName} /> </SectionCard>
</View>
</Section>
{/* 3. Timeline Section */} {/* AUCTION DETAILS */}
<Section title="Key Dates" icon={Calendar}> <SectionCard title="Auction Details" icon={Briefcase} colors={colors} spacing={spacing} shadows={shadows}>
<View style={styles.timelineContainer}> <InfoRow label="Name" value={auctionData?.auctionOfWork} colors={colors} multiline />
<View style={styles.timelineRow}> <InfoRow label="Category" value={auctionData?.auctionCategories} colors={colors} />
<View style={styles.iconCircle}><Clock size={16} color="#64748b" /></View> <InfoRow label="Fees" value={auctionData?.auctionFees ? `₹ ${auctionData.auctionFees}` : '—'} colors={colors} valueColor={colors.primary} />
<View> <InfoRow label="Fees (Words)" value={auctionData?.auctionFeesInWord} colors={colors} multiline />
<Text style={styles.label}>Auction Purchase (Start)</Text> <InfoRow label="Keyword" value={auctionData?.auctionKeywordName} colors={colors} last />
<Text style={styles.value}>{formatDate( </SectionCard>
auctionData?.auctionStatus === "Amended" ? auctionData?.amendedkeyDates.auctionPurchase.startDateTime : auctionData?.keyDates.auctionPurchase.startDateTime)}</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 Purchase (End)</Text>
<Text style={styles.value}>{formatDate(
auctionData?.auctionStatus === "Amended" ? auctionData?.amendedkeyDates.auctionPurchase.endDateTime : auctionData?.keyDates.auctionPurchase.endDateTime)}</Text>
</View>
</View> {/* KEY DATES */}
<View style={styles.timelineConnector} /> <SectionCard title="Key Dates" icon={Calendar} colors={colors} spacing={spacing} shadows={shadows}>
<View style={styles.timelineRow}> <InfoRow label="Purchase Start" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.startDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.startDateTime))} colors={colors} />
<View style={styles.iconCircle}><Clock size={16} color="#64748b" /></View> <InfoRow label="Purchase End" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.endDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.endDateTime))} colors={colors} valueColor={colors.error} />
<View> <InfoRow label="Bidding Start" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.startDateTime, auctionData?.amendedkeyDates?.auctionBidding?.startDateTime))} colors={colors} />
<Text style={styles.label}>Bidding Start (Start)</Text> <InfoRow label="Bidding End" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.endDateTime, auctionData?.amendedkeyDates?.auctionBidding?.endDateTime))} colors={colors} valueColor={colors.error} last />
<Text style={styles.value}>{formatDate( </SectionCard>
auctionData?.auctionStatus === "Amended" ? auctionData?.amendedkeyDates.auctionBidding.startDateTime : auctionData?.keyDates.auctionBidding.startDateTime)}</Text>
</View>
{/* AUCTION ITEMS */}
{aucitonDetail?.auctionItems?.length > 0 && (
<SectionCard title="Auction Items" icon={FileText} colors={colors} spacing={spacing} shadows={shadows}>
{aucitonDetail.auctionItems.map((item: any, index: number) => (
<View
key={index}
style={[
styles.itemRow,
{ borderBottomColor: colors.border },
index === aucitonDetail.auctionItems.length - 1 && { borderBottomWidth: 0 },
]}
>
<View style={styles.itemLeft}>
<AppText variant="bodySmall" color={colors.text} style={styles.itemName as any}>
{item.productName}
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
{item?.categoryName} · {item.timeDurationType}
</AppText>
</View> </View>
<View style={styles.timelineConnector} /> <TouchableOpacity
<View style={styles.timelineRow}> style={[styles.eyeBtn, { backgroundColor: `${colors.primary}15` }]}
<View style={[styles.iconCircle, { backgroundColor: '#fee2e2' }]}><Clock size={16} color="#ef4444" /></View> onPress={() => setAllBoolean(prev => ({ ...prev, showItemDetail: true, selectedItem: item }))}
<View> >
<Text style={styles.label}>Bidding Deadline (End)</Text> <Eye size={16} color={colors.primary} strokeWidth={2} />
<Text style={styles.value}>{formatDate(
auctionData?.auctionStatus === "Amended" ? auctionData?.amendedkeyDates.auctionBidding.endDateTime : auctionData?.keyDates.auctionBidding.endDateTime)}</Text>
</View>
</View>
</View>
</Section>
{/* 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>
</View>
<Text style={styles.itemQty}>{item.timeDurationType}</Text>
<TouchableOpacity style={styles.actionIcon} onPress={() => { setAllBoolean((prev: any) => ({ ...prev, showItemDetail: true, selectedItem: item })) }}>
<Eye size={18} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
))} ))}
</View> </SectionCard>
</Section>
{/* 5. Downloads */}
<Section title="Downloads" icon={Download}>
{aucitonDetail?.auctionDocuments?.map((doc: any, index: number) => (
<TouchableOpacity style={styles.fileCard} onPress={() => {
handleDownload(
doc.auctionDocumentId,
doc.fileName.replace('.pdf', '')
)
}}>
<View style={styles.fileIcon}><FileText size={20} color="#ef4444" /></View>
<View style={{ flex: 1 }}>
<Text style={styles.fileName}>{doc.fileName}</Text>
</View>
{allBoolean.fileLoading ? (
<View >
<Loader size='small' color='#94a3b8' />
</View>
) : (
<Download size={20} color="#94a3b8" />
)} )}
{/* DOCUMENTS */}
{aucitonDetail?.auctionDocuments?.length > 0 && (
<SectionCard title="Documents" icon={Download} colors={colors} spacing={spacing} shadows={shadows}>
{aucitonDetail.auctionDocuments.map((doc: any, index: number) => (
<TouchableOpacity
key={index}
style={[
styles.docRow,
{ borderBottomColor: colors.border },
index === aucitonDetail.auctionDocuments.length - 1 && { borderBottomWidth: 0 },
]}
activeOpacity={0.7}
onPress={() => handleDownload(doc.auctionDocumentId, doc.fileName.replace('.pdf', ''))}
>
<View style={[styles.docIcon, { backgroundColor: `${colors.error}15` }]}>
<FileText size={16} color={colors.error} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} style={styles.docName as any} numberOfLines={1}>
{doc.fileName}
</AppText>
{allBoolean.fileLoading
? <ActivityIndicator size="small" color={colors.primary} />
: <Download size={18} color={colors.textSecondary} strokeWidth={2} />
}
</TouchableOpacity> </TouchableOpacity>
))} ))}
</Section> </SectionCard>
)}
</ScrollView> </ScrollView>
{allBoolean.showItemDetail && <AuctionItemDetail data={allBoolean.selectedItem} onClose={() => { setAllBoolean((prev: any) => ({ ...prev, showItemDetail: false })) }} visible={allBoolean.showItemDetail} />} {allBoolean.showItemDetail && (
<AuctionItemDetail
data={allBoolean.selectedItem}
onClose={() => setAllBoolean(prev => ({ ...prev, showItemDetail: false }))}
visible={allBoolean.showItemDetail}
/>
)}
</View> </View>
</Modal> </Modal>
); );
}; };
const styles = StyleSheet.create({ export default memo(AuctionFullDetail);
fullModal: {
margin: 0, /* ─── Sub-components ─── */
backgroundColor: '#fff',
const SectionCard = ({ title, icon: Icon, children, colors, spacing, shadows }: any) => {
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Icon size={18} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.text} style={styles.cardTitle as any}>
{title.toUpperCase()}
</AppText>
</View>
<View style={[styles.cardDivider, { backgroundColor: colors.border }]} />
{children}
</View>
);
};
const InfoRow = ({ label, value, colors, last = false, multiline = false, valueColor }: any) => (
<View style={[
{
flexDirection: 'row',
alignItems: multiline ? 'flex-start' : 'center',
justifyContent: 'space-between',
paddingVertical: 13,
borderBottomWidth: last ? 0 : StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.06)',
gap: 12,
}, },
safeArea: { ]}>
<AppText variant="bodySmall" color={colors.textSecondary} style={{ flex: 1 } as any}>
{label}
</AppText>
<AppText
variant="bodySmall"
color={valueColor || colors.text}
style={{ flex: 1, textAlign: 'right', fontWeight: '600' } as any}
numberOfLines={multiline ? 4 : 1}
>
{value || '—'}
</AppText>
</View>
);
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
root: {
flex: 1, flex: 1,
backgroundColor: '#fff', backgroundColor: colors.background,
}, },
/* Header */
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: 16, paddingHorizontal: spacing.m,
borderBottomWidth: 1, paddingBottom: spacing.m,
borderBottomColor: '#f1f5f9', backgroundColor: colors.surface,
backgroundColor: '#fff', borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
}, },
backBtn: { backBtn: {
padding: 8, width: 36,
marginRight: 8, height: 36,
}, borderRadius: 10,
headerTitleContainer: {
flex: 1,
},
headerTitleText: {
fontSize: 18,
fontWeight: '800',
color: '#0f172a',
},
headerIdText: {
fontSize: 13,
color: '#64748b',
},
liveIndicator: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f8fafc',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
gap: 6,
borderWidth: 1,
borderColor: '#e2e8f0',
},
dot: { width: 8, height: 8, borderRadius: 4 },
liveText: { fontSize: 11, fontWeight: '800', color: '#475569' },
scrollView: { flex: 1, backgroundColor: '#f8fafc' },
scrollContent: { padding: 16, paddingBottom: 30 },
sectionContainer: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: '#eef2f6',
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 20,
paddingBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#f8fafc',
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: '#1e293b',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
infoBox: { marginBottom: 16 },
label: { fontSize: 12, color: '#94a3b8', fontWeight: '600', marginBottom: 4 },
value: { fontSize: 15, fontWeight: '700', color: '#334155' },
spacer: { height: 12 },
timelineContainer: { paddingLeft: 8 },
timelineRow: { flexDirection: 'row', alignItems: 'center', gap: 15 },
iconCircle: { width: 34, height: 34, borderRadius: 10, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
timelineConnector: { width: 2, height: 20, backgroundColor: '#f1f5f9', marginLeft: 16 },
tableWrapper: { backgroundColor: '#f8fafc', borderRadius: 12, padding: 12 },
tableHead: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#e2e8f0', paddingBottom: 8, marginBottom: 12 },
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 },
tableRow: { flexDirection: 'row', alignItems: 'center' },
itemMain: { fontSize: 14, fontWeight: '700', color: '#1e293b' },
itemSub: { fontSize: 12, color: '#64748b' },
itemQty: { flex: 1, fontSize: 14, fontWeight: '700', color: '#334155' },
actionIcon: { padding: 6 },
fileCard: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 },
fileIcon: { width: 40, height: 40, borderRadius: 8, backgroundColor: '#fee2e2', alignItems: 'center', justifyContent: 'center' },
fileName: { fontSize: 14, fontWeight: '700', color: '#334155' },
fileSize: { fontSize: 12, color: '#94a3b8' },
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#fff',
padding: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1,
borderTopColor: '#f1f5f9',
},
mainBtn: {
backgroundColor: colors.primary,
height: 56,
borderRadius: 16,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
}, },
btnDisabled: { backgroundColor: '#cbd5e1' }, headerCenter: {
mainBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' }, flex: 1,
}); },
headerTitle: {
fontWeight: '800',
} as any,
statusChip: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.s + 4,
paddingVertical: 6,
borderRadius: 20,
borderWidth: 1.2,
gap: 5,
},
statusDot: {
width: 7,
height: 7,
borderRadius: 4,
},
statusText: {
fontWeight: '800',
letterSpacing: 0.5,
} as any,
export default memo(AuctionFullDetail); scrollContent: {
padding: spacing.m,
paddingBottom: 50,
gap: spacing.m,
},
/* Card */
card: {
backgroundColor: colors.surface,
borderRadius: 18,
paddingHorizontal: spacing.m,
paddingTop: spacing.m,
paddingBottom: spacing.s,
marginBottom: spacing.m,
...shadows.light,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
marginBottom: spacing.s + 2,
},
cardTitle: {
fontWeight: '800',
letterSpacing: 1.2,
} as any,
cardDivider: {
height: StyleSheet.hairlineWidth,
marginBottom: spacing.s,
},
/* Items */
itemRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.s + 2,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: spacing.s,
},
itemLeft: {
flex: 1,
},
itemName: {
fontWeight: '700',
marginBottom: 2,
} as any,
eyeBtn: {
width: 32,
height: 32,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
/* Docs */
docRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.s + 2,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: spacing.s,
},
docIcon: {
width: 32,
height: 32,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
docName: {
flex: 1,
fontWeight: '600',
} as any,
});
+67 -69
View File
@@ -1,38 +1,43 @@
import React, { memo, useState } from 'react'; import React, { memo, useState, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, Modal } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, Modal } from 'react-native';
import { Package, IndianRupee, FileText, Download, ArrowLeft, Info, } from 'lucide-react-native'; import { Package, IndianRupee, FileText, Download, ArrowLeft, Info, } from 'lucide-react-native';
import colors from '../../../Constant/Color';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useAucitonItemDetail } from '../../../Services/ApiCall/Hooks'; import { useAucitonItemDetail } from '../../../Services/ApiCall/Hooks';
import ReactNativeBlobUtil from 'react-native-blob-util'; import ReactNativeBlobUtil from 'react-native-blob-util';
import { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls'; import { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
import { Loader } from '../../../Component/Index'; import { Loader } from '../../../Component/Index';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props { visible: boolean; onClose: () => void; data?: any; } interface Props { visible: boolean; onClose: () => void; data?: any; }
const Section = ({ title, icon: Icon, children }: any) => ( const Section = ({ title, icon: Icon, children, styles, colors }: any) => (
<View style={styles.sectionCard}> <View style={styles.sectionCard}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<View style={styles.iconCircle}> <View style={styles.iconCircle}>
<Icon size={18} color={colors.primary} strokeWidth={2.5} /> <Icon size={18} color={colors.primary} strokeWidth={2.5} />
</View> </View>
<Text style={styles.sectionTitle}>{title}</Text> <AppText variant="caption" bold style={styles.sectionTitle}>{title}</AppText>
</View> </View>
{children} {children}
</View> </View>
); );
const PriceTile = ({ label, fig, words, full = false }: any) => ( const PriceTile = ({ label, fig, words, full = false, styles }: any) => (
<View style={[styles.priceTile, full ? { width: '100%' } : { width: '48%' }]}> <View style={[styles.priceTile, full ? { width: '100%' } : { width: '48%' }]}>
<Text style={styles.label}>{label}</Text> <AppText variant="caption" color={styles.label.color} bold style={styles.label}>{label}</AppText>
<Text style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</Text> <AppText variant="h3" bold style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</AppText>
<Text style={styles.priceWords}>{words}</Text> <AppText variant="caption" style={styles.priceWords}>{words}</AppText>
</View> </View>
); );
const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => { const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets() const inset = useSafeAreaInsets()
const { colors, typography } = useTheme();
const styles = useMemo(() => createStyles(colors, typography), [colors, typography]);
const [allBoolean, setAllBoolean] = useState({ fileLoading: false }); const [allBoolean, setAllBoolean] = useState({ fileLoading: false });
const { data: itemData } = useAucitonItemDetail({ auctionItemId: data.id }); const { data: itemData } = useAucitonItemDetail({ auctionItemId: data?.id });
const handleDownload = async (id: number, fileName: string) => { const handleDownload = async (id: number, fileName: string) => {
try { try {
@@ -41,11 +46,8 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const path = `${dirs.DownloadDir}/${fileName}.pdf`; const path = `${dirs.DownloadDir}/${fileName}.pdf`;
const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`; const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`;
const response = await ReactNativeBlobUtil.config({ const response = await ReactNativeBlobUtil.config({
fileCache: false, fileCache: false,
addAndroidDownloads: { addAndroidDownloads: {
@@ -83,11 +85,11 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
{/* Navbar */} {/* Navbar */}
<View style={[styles.header, { paddingTop: inset.top }]}> <View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}> <TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" /> <ArrowLeft size={24} color={colors.text} />
</TouchableOpacity> </TouchableOpacity>
<View> <View>
<Text style={styles.headerTitle}>Item Information</Text> <AppText variant="body" bold style={styles.headerTitle}>Item Information</AppText>
<Text style={styles.headerSub}>Detail View</Text> <AppText variant="caption" style={styles.headerSub}>Detail View</AppText>
</View> </View>
</View> </View>
@@ -96,69 +98,66 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
<Section title="Product Specification" icon={Package}> <Section title="Product Specification" icon={Package} styles={styles} colors={colors}>
<View style={styles.identityHeader}> <View style={styles.identityHeader}>
<Text style={styles.productNameText}>{data.productName.toUpperCase()}</Text> <AppText variant="h4" bold style={styles.productNameText}>{data?.productName?.toUpperCase()}</AppText>
<View style={styles.categoryBadge}> <View style={styles.categoryBadge}>
<Text style={styles.categoryText}>{data.categoryName}</Text> <AppText variant="caption" bold style={styles.categoryText}>{data?.categoryName}</AppText>
</View> </View>
</View> </View>
<View style={styles.specGrid}> <View style={styles.specGrid}>
<View style={styles.specItem}> <View style={styles.specItem}>
<Text style={styles.label}>Sub Category</Text> <AppText variant="caption" bold style={styles.label}>Sub Category</AppText>
<Text style={styles.specValue}>{data.subCategoryName}</Text> <AppText variant="bodySmall" bold style={styles.specValue}>{data?.subCategoryName}</AppText>
</View> </View>
<View style={styles.specItem}> <View style={styles.specItem}>
<Text style={styles.label}>Duration</Text> <AppText variant="caption" bold style={styles.label}>Duration</AppText>
<Text style={styles.specValue}>{data.timeDurationType}</Text> <AppText variant="bodySmall" bold style={styles.specValue}>{data?.timeDurationType}</AppText>
</View> </View>
</View> </View>
</Section> </Section>
<Section title="Reserve & Bid Values" icon={IndianRupee}> <Section title="Reserve & Bid Values" icon={IndianRupee} styles={styles} colors={colors}>
<PriceTile <PriceTile
label="Bidding Price" label="Bidding Price"
fig={data.reservePrice || data.emdAmount} fig={data?.reservePrice || data?.emdAmount}
words={data.reservePriceInWords || data.emdAmountInWords} words={data?.reservePriceInWords || data?.emdAmountInWords}
full full
styles={styles}
/> />
<View style={styles.hDivider} /> <View style={styles.hDivider} />
<PriceTile <PriceTile
label="Auction Start Value" label="Auction Start Value"
fig={data.reservePrice || data.auctionStartValueFigure} fig={data?.reservePrice || data?.auctionStartValueFigure}
words={data.reservePriceInWords || data.auctionStartValueWords} words={data?.reservePriceInWords || data?.auctionStartValueWords}
full full
styles={styles}
/> />
<View style={styles.hDivider} /> <View style={styles.hDivider} />
{/* <View style={styles.row}>
<PriceTile label="Bid Variation" fig={data.bidVariationFig} words={data.bidVariationWords} />
<PriceTile label="Auction Start Value" fig={data.auctionStartValue} words={data.auctionStartValueInWords
} />
</View> */}
</Section> </Section>
<Section title="Brief Description" icon={Info}> <Section title="Brief Description" icon={Info} styles={styles} colors={colors}>
<Text style={styles.descContent}>{data.briefDescription}</Text> <AppText variant="bodySmall" style={styles.descContent}>{data?.briefDescription}</AppText>
</Section> </Section>
{itemData?.length && <Section title="View Documents" icon={FileText}> {itemData?.length && <Section title="View Documents" icon={FileText} styles={styles} colors={colors}>
<View style={styles.docTableHead}> <View style={styles.docTableHead}>
<Text style={styles.tableLabel}>Sr No.</Text> <AppText variant="caption" bold style={styles.tableLabel}>Sr No.</AppText>
<Text style={[styles.tableLabel, { flex: 2 }]}>Document Name</Text> <AppText variant="caption" bold style={[styles.tableLabel, { flex: 2 }]}>Document Name</AppText>
<Text style={styles.tableLabel}>Action</Text> <AppText variant="caption" bold style={styles.tableLabel}>Action</AppText>
</View> </View>
{itemData?.map((doc: any, index: any) => ( {itemData?.map((doc: any, index: any) => (
<TouchableOpacity style={styles.docRow} activeOpacity={0.7} onPress={() => { handleDownload(doc.assetId, doc.fileName) }} key={doc.assetId}> <TouchableOpacity style={styles.docRow} activeOpacity={0.7} onPress={() => { handleDownload(doc.assetId, doc.fileName) }} key={doc.assetId}>
<Text style={styles.docIndex}>{index + 1}</Text> <AppText variant="bodySmall" bold style={styles.docIndex}>{index + 1}</AppText>
<View style={{ flex: 2 }}> <View style={{ flex: 2 }}>
<Text style={styles.docTitle} numberOfLines={1}>{doc.fileName}</Text> <AppText variant="bodySmall" bold style={styles.docTitle} numberOfLines={1}>{doc.fileName}</AppText>
</View> </View>
<View style={styles.downloadIconBox}> <View style={styles.downloadIconBox}>
{allBoolean.fileLoading ? ( {allBoolean.fileLoading ? (
<View > <View >
<Loader size='small' color='#94a3b8' /> <Loader size='small' color={colors.textSecondary} />
</View> </View>
) : ( ) : (
<Download size={20} color={colors.primary} /> <Download size={20} color={colors.primary} />
@@ -173,26 +172,26 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
); );
}; };
const styles = StyleSheet.create({ const createStyles = (colors: any, typography: any) => StyleSheet.create({
fullModal: { margin: 0, backgroundColor: '#f8fafc' }, fullModal: { margin: 0, backgroundColor: colors.background },
safeArea: { flex: 1 }, safeArea: { flex: 1, backgroundColor: colors.background },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: 16, padding: 16,
backgroundColor: '#fff', backgroundColor: colors.surface,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f1f5f9', borderBottomColor: colors.border,
}, },
backBtn: { padding: 8, marginRight: 8 }, backBtn: { padding: 8, marginRight: 8 },
headerTitle: { fontSize: 18, fontWeight: '800', color: '#0f172a' }, headerTitle: { fontSize: 18, color: colors.text },
headerSub: { fontSize: 12, color: '#94a3b8', fontWeight: '600', textTransform: 'uppercase' }, headerSub: { fontSize: 12, color: colors.textSecondary, textTransform: 'uppercase' },
scrollView: { flex: 1 }, scrollView: { flex: 1 },
scrollContent: { padding: 16, paddingBottom: 50 }, scrollContent: { padding: 16, paddingBottom: 50 },
sectionCard: { sectionCard: {
backgroundColor: '#fff', backgroundColor: colors.surface,
borderRadius: 24, borderRadius: 24,
padding: 20, padding: 20,
marginBottom: 16, marginBottom: 16,
@@ -217,55 +216,54 @@ const styles = StyleSheet.create({
}, },
sectionTitle: { sectionTitle: {
fontSize: 14, fontSize: 14,
fontWeight: '800', color: colors.text,
color: '#334155',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: 0.5,
}, },
// Product Identity // Product Identity
identityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }, identityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
productNameText: { fontSize: 20, fontWeight: '900', color: '#1e293b', flex: 1 }, productNameText: { fontSize: 20, color: colors.text, flex: 1 },
categoryBadge: { backgroundColor: '#f1f5f9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 }, categoryBadge: { backgroundColor: colors.border, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
categoryText: { fontSize: 11, fontWeight: '800', color: '#64748b', textTransform: 'uppercase' }, categoryText: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase' },
specGrid: { flexDirection: 'row', gap: 24, marginTop: 5 }, specGrid: { flexDirection: 'row', gap: 24, marginTop: 5 },
specItem: { flex: 1 }, specItem: { flex: 1 },
specValue: { fontSize: 15, fontWeight: '700', color: '#334155', marginTop: 2 }, specValue: { fontSize: 15, color: colors.text, marginTop: 2 },
// Pricing // Pricing
priceTile: { marginBottom: 16 }, priceTile: { marginBottom: 16 },
label: { fontSize: 11, color: '#94a3b8', fontWeight: '700', textTransform: 'uppercase', marginBottom: 4 }, label: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase', marginBottom: 4 },
priceFig: { fontSize: 22, fontWeight: '900', color: colors.black }, priceFig: { fontSize: 22, color: colors.text },
priceWords: { fontSize: 13, color: '#64748b', fontStyle: 'italic', marginTop: 2 }, priceWords: { fontSize: 13, color: colors.textSecondary, fontStyle: 'italic', marginTop: 2 },
hDivider: { height: 1, backgroundColor: '#f1f5f9', marginBottom: 16 }, hDivider: { height: 1, backgroundColor: colors.border, marginBottom: 16 },
row: { flexDirection: 'row', justifyContent: 'space-between' }, row: { flexDirection: 'row', justifyContent: 'space-between' },
// Description // Description
descContent: { fontSize: 15, color: '#475569', lineHeight: 22, fontWeight: '500' }, descContent: { fontSize: 15, color: colors.textSecondary, lineHeight: 22, fontFamily: typography.fonts.medium },
// Table // Table
docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', marginBottom: 10 }, docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: colors.border, marginBottom: 10 },
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 }, tableLabel: { fontSize: 11, color: colors.textSecondary, flex: 1 },
docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 }, docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background, padding: 12, borderRadius: 12 },
docIndex: { flex: 1, fontSize: 14, fontWeight: '700', color: '#64748b' }, docIndex: { flex: 1, fontSize: 14, color: colors.textSecondary },
docTitle: { fontSize: 14, fontWeight: '700', color: '#1e293b' }, docTitle: { fontSize: 14, color: colors.text },
downloadIconBox: { backgroundColor: '#fff', padding: 8, borderRadius: 10, borderWidth: 1, borderColor: '#e2e8f0' }, downloadIconBox: { backgroundColor: colors.surface, padding: 8, borderRadius: 10, borderWidth: 1, borderColor: colors.border },
// Footer // Footer
footer: { footer: {
position: 'absolute', position: 'absolute',
bottom: 0, width: '100%', bottom: 0, width: '100%',
backgroundColor: '#fff', backgroundColor: colors.surface,
padding: 20, padding: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20, paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1, borderTopColor: '#f1f5f9' borderTopWidth: 1, borderTopColor: colors.border
}, },
primaryBtn: { primaryBtn: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
height: 56, borderRadius: 18, height: 56, borderRadius: 18,
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10 flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10
}, },
primaryBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' } primaryBtnText: { color: colors.buttonText, fontSize: 16, fontFamily: typography.fonts.bold }
}); });
export default memo(AuctionItemDetail); export default memo(AuctionItemDetail);
+1 -1
View File
@@ -64,7 +64,7 @@ const PurchaseAuction = () => {
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} /> <SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} filter={false} />
{isPending ? <Skelton /> {isPending ? <Skelton />
: :
<FlatList <FlatList
+374 -176
View File
@@ -1,8 +1,24 @@
import React, { use, useEffect, useState } from 'react'; import React, { useState } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native'; import {
import { InsideHeader, Loader } from '../../Component/Index'; View,
import colors from '../../Constant/Color'; FlatList,
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native'; StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import Animated, { FadeInDown, FadeIn } from 'react-native-reanimated';
import {
Calendar,
Hash,
Wifi,
WifiOff,
Receipt,
CheckCircle2,
Gavel,
Building2,
BadgeIndianRupee,
} from 'lucide-react-native';
import { InsideHeader } from '../../Component/Index';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/ApiCall/Hooks'; import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/ApiCall/Hooks';
import { showSuccessAlert } from '../../Constant/ShowDailog'; import { showSuccessAlert } from '../../Constant/ShowDailog';
@@ -10,42 +26,33 @@ import { clearPurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native'; import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import Skeleton from '../../Component/Skelton/Skelton'; import Skeleton from '../../Component/Skelton/Skelton';
import { useTheme } from '../../Hooks/useTheme';
const dummyAuctionList = [ import AppText from '../../components/AppText';
{
auctionId: 1,
auctionNumber: 'AUC-2025-001',
auctionPattern: 'Single Stage',
auctionDispatchDate: '25 Feb 2025',
auctionDispatcherNumber: 'DISP-001',
auctionFees: 2500,
gst: 18,
gstAmount: 450,
portalCharge: 100,
payableAmount: 3050,
},
];
const PurchaseAuctionList = () => { const PurchaseAuctionList = () => {
const Navigation = useNavigation() const Navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [list, setList] = useState([]); const [list, setList] = useState<any[]>([]);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const LoginUser = useSelector((state: any) => state.token.token); const LoginUser = useSelector((state: any) => state.token.token);
const { mutate, isPending } = useOfflineAuctionPurchase()
const { mutate, isPending } = useOfflineAuctionPurchase();
const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList(); const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList();
const calculateTotalAmount = (item: any) => { const calculateTotalAmount = (item: any) => {
const gstAmount = const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) * (Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100); (Number(item.gst) / 100);
return Number(item.auctionFees) + Number(item.portalCharge) + gstAmount;
};
const payableAmount = const grandTotal = list.reduce(
Number(item.auctionFees) + Number(item.portalCharge) + gstAmount; (sum: number, item: any) => sum + calculateTotalAmount(item),
return payableAmount 0,
} );
const handleOfflinePurchase = async () => { const handleOfflinePurchase = async () => {
const body = { const body = {
@@ -59,235 +66,426 @@ const PurchaseAuctionList = () => {
gst: b?.gst, gst: b?.gst,
gstAmount: b?.gstAmount, gstAmount: b?.gstAmount,
payableAmount: calculateTotalAmount(b), payableAmount: calculateTotalAmount(b),
portalCharge: b?.portalCharge portalCharge: b?.portalCharge,
})), })),
orderNo: '' orderNo: '',
}; };
try { try {
mutate(body, { mutate(body, {
onSuccess: (res) => { onSuccess: res => {
if (res.statusCode == 200) { if (res.statusCode === 200) {
showSuccessAlert(res.message) showSuccessAlert(res.message);
Dispatch(clearPurchaseAuction()) Dispatch(clearPurchaseAuction());
Navigation.dispatch( Navigation.dispatch(
CommonActions.reset({ CommonActions.reset({ index: 0, routes: [{ name: NavigationString.Home }] }),
index: 0,
routes: [{ name: NavigationString.Home }],
})
); );
} }
}, },
onError: (error) => { onError: error => console.log(error, 'purchase Error'),
console.log(error, "purchse Error"); });
}
})
} catch (error) { } catch (error) {
console.log("purchase offline error", error); console.log('purchase offline error', error);
}
} }
};
const handlePurchase = (type: 'online' | 'offline') => { const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true); setLoading(true);
setTimeout(() => { setTimeout(() => {
setLoading(false); setLoading(false);
type === 'online' ? Alert.alert('Online Purchase Successful!') : handleOfflinePurchase(); type === 'online' ? undefined : handleOfflinePurchase();
}, 1200); }, 1200);
}; };
const renderItem = ({ item }: { item: typeof dummyAuctionList[0] }) => (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.title}>{item.auctionNumber}</Text>
<Text style={styles.pattern}>{item.auctionPattern}</Text>
</View>
<View style={styles.cardRow}>
<Calendar size={18} color={colors.primary} />
<Text style={styles.text}>Dispatch Date: {item.auctionDispatchDate}</Text>
</View>
<View style={styles.cardRow}>
<User size={18} color={colors.primary} />
<Text style={styles.text}>Dispatcher No: {item.auctionDispatcherNumber}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Auction Fees: ₹{item.auctionFees}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>GST ({item.gst}%): ₹{item.gstAmount}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Portal Charge: ₹{item.portalCharge}</Text>
</View>
<View style={styles.totalRow}>
<CreditCard size={20} color={colors.white} />
<Text style={styles.total}>Total: ₹{item.payableAmount}</Text>
</View>
</View>
);
const getPurchaseList = () => { const getPurchaseList = () => {
const auctionNumbers: string[] = SelectedPurchaseList.map( const auctionNumbers: string[] = SelectedPurchaseList.map(
(item: { auctionNumber: string }) => item.auctionNumber (item: { auctionNumber: string }) => item.auctionNumber,
); );
purchaseListMutation({ auctionNumbers, contractorId: LoginUser.contractorId }, { purchaseListMutation(
onSuccess: (res) => { { auctionNumbers, contractorId: LoginUser.contractorId },
console.log("purchaseListMutation",res) {
if (res.statusCode == 200) { onSuccess: res => {
if (res.statusCode === 200) setList(res.data);
setList(res.data)
}
}, },
onError: (error) => { onError: error => console.log(error, 'purchase list error'),
console.log(error, "purchase list error"); },
} );
}) };
}
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
getPurchaseList() getPurchaseList();
}, [SelectedPurchaseList]) }, [SelectedPurchaseList]),
); );
const styles = makeStyles(colors, spacing, shadows);
/* ── Render Item ── */
const renderItem = ({ item, index }: { item: any; index: number }) => {
const total = calculateTotalAmount(item);
const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return (
<Animated.View
entering={FadeInDown.delay(index * 90).springify()}
style={styles.card}
>
{/* Top accent stripe */}
<View style={styles.cardAccent} />
<View style={styles.cardBody}>
{/* Header */}
<View style={styles.cardHeader}>
<View style={[styles.indexBadge, { backgroundColor: colors.primary }]}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '800' } as any}>
{String(index + 1).padStart(2, '0')}
</AppText>
</View>
<View style={{ flex: 1, marginLeft: spacing.s }}>
<AppText variant="h4" color={colors.text} numberOfLines={2}>
{item.auctionNumber}
</AppText>
{item.auctionPattern ? (
<View style={[styles.patternBadge, { backgroundColor: `${colors.primary}15` }]}>
<Gavel size={11} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} style={{ fontWeight: '600' } as any}>
{item.auctionPattern}
</AppText>
</View>
) : null}
</View>
</View>
<View style={styles.divider} />
{/* Info Rows */}
<InfoRow
icon={<Hash size={15} color={colors.primary} />}
label="Dispatcher No"
value={item.auctionDispatcherNumber ?? '—'}
colors={colors}
/>
<InfoRow
icon={<Calendar size={15} color={colors.primary} />}
label="Dispatch Date"
value={item.auctionDispatchDate ?? '—'}
colors={colors}
/>
<View style={[styles.divider, { marginVertical: spacing.s }]} />
{/* Fee Breakdown */}
<View style={[styles.feeSection, { backgroundColor: colors.overlay }]}>
<AppText variant="caption" color={colors.textSecondary} style={styles.feeHeader as any}>
FEE BREAKDOWN
</AppText>
<FeeRow label="Auction Fees" value={`₹ ${Number(item.auctionFees).toFixed(2)}`} colors={colors} />
<FeeRow label={`GST (${item.gst}%)`} value={`₹ ${gstAmount.toFixed(2)}`} colors={colors} />
<FeeRow label="Portal Charge" value={`₹ ${Number(item.portalCharge).toFixed(2)}`} colors={colors} />
</View>
{/* Total Banner */}
<View style={[styles.totalBanner, { backgroundColor: colors.primary }]}>
<View style={styles.totalLeft}>
<Receipt size={18} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '600' } as any}>
Total Payable
</AppText>
</View>
<AppText variant="h4" color={colors.buttonText}>
₹ {total.toFixed(2)}
</AppText>
</View>
</View>
</Animated.View>
);
};
return ( return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<InsideHeader title="Purchase List" showArrow />
<View style={styles.screen}> <View style={styles.screen}>
{purchaseListPending ? <Skeleton /> : <FlatList <InsideHeader title="Auction Purchase" showArrow />
{/* Grand Total Strip */}
<Animated.View entering={FadeInDown.springify()} style={styles.grandTotalStrip}>
<View>
<AppText variant="caption" color={colors.textSecondary}>
{list.length} auction{list.length !== 1 ? 's' : ''}
</AppText>
<AppText variant="h3" color={colors.text}>
₹ {grandTotal.toFixed(2)}
</AppText>
</View>
<View style={[styles.grandTotalRight, { backgroundColor: `${colors.success}15` }]}>
<CheckCircle2 size={18} color={colors.success} />
<AppText variant="caption" color={colors.success} style={{ fontWeight: '600' } as any}>
Ready to pay
</AppText>
</View>
</Animated.View>
{/* List */}
{purchaseListPending ? (
<Skeleton />
) : (
<FlatList
data={list} data={list}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item) => item.auctionId.toString()} keyExtractor={item => item.auctionId?.toString()}
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
/>} showsVerticalScrollIndicator={false}
</View> />
)}
<View style={styles.bottomButtonContainer}> {/* Bottom Buttons */}
<View style={styles.footer}>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { marginRight: 8 }]} style={[styles.button, styles.onlineButton, { borderColor: colors.primary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('online')} onPress={() => handlePurchase('online')}
> >
<Text style={styles.buttonText}>Purchase Online</Text> <Wifi size={18} color={colors.primary} />
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} /> <AppText variant="button" color={colors.primary} style={styles.btnText as any}>
Online
</AppText>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { marginLeft: 8 }]} style={[styles.button, styles.offlineButton, { backgroundColor: colors.buttonPrimary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('offline')} onPress={() => handlePurchase('offline')}
> >
{ {isPending ? (
isPending ? <Loader size='small' color={colors.white} /> : <> <ActivityIndicator size="small" color="#fff" />
<Text style={styles.buttonText}>Purchase Offline</Text> ) : (
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} /> <>
<WifiOff size={18} color={colors.buttonText} />
<AppText variant="button" color={colors.buttonText} style={styles.btnText as any}>
Offline
</AppText>
</> </>
} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Loading Overlay */}
{(loading || isPending) && (
<Animated.View entering={FadeIn.duration(200)} style={styles.loadingOverlay}>
<View style={[styles.loadingCard, { backgroundColor: colors.surface }]}>
<ActivityIndicator size="large" color={colors.primary} />
<AppText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: spacing.s } as any}>
Processing purchase…
</AppText>
</View>
</Animated.View>
)}
</View> </View>
); );
}; };
export default PurchaseAuctionList; export default PurchaseAuctionList;
const styles = StyleSheet.create({ /* ─── Sub-components ─── */
const InfoRow = ({
icon,
label,
value,
colors,
}: {
icon: React.ReactNode;
label: string;
value: string;
colors: any;
}) => (
<View style={infoRowStyles.row}>
<View style={infoRowStyles.iconWrap}>{icon}</View>
<AppText variant="caption" color={colors.textSecondary} style={infoRowStyles.label as any}>
{label}
</AppText>
<AppText variant="bodySmall" color={colors.text} style={infoRowStyles.value as any}>
{value}
</AppText>
</View>
);
const infoRowStyles = StyleSheet.create({
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 5, gap: 6 },
iconWrap: { width: 22, alignItems: 'center' },
label: { flex: 1, fontWeight: '500' } as any,
value: { fontWeight: '600' } as any,
});
const FeeRow = ({
label,
value,
colors,
}: {
label: string;
value: string;
colors: any;
}) => (
<View style={feeRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary}>{label}</AppText>
<AppText variant="caption" color={colors.text} style={{ fontWeight: '600' } as any}>{value}</AppText>
</View>
);
const feeRowStyles = StyleSheet.create({
row: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 3 },
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: { screen: {
flex: 1, flex: 1,
backgroundColor: colors.background,
}, },
/* Grand total strip */
grandTotalStrip: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderRadius: 18,
...shadows.medium,
},
grandTotalRight: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
paddingHorizontal: spacing.s + 2,
paddingVertical: spacing.xs + 2,
borderRadius: 20,
},
container: { container: {
padding: 16, paddingHorizontal: spacing.m,
paddingTop: spacing.s,
paddingBottom: 120, paddingBottom: 120,
}, },
/* Card */
card: { card: {
backgroundColor: '#fff', backgroundColor: colors.surface,
borderRadius: 16, borderRadius: 20,
padding: 16, marginBottom: spacing.m,
marginBottom: 16, overflow: 'hidden',
elevation: 5, ...shadows.medium,
shadowColor: '#000', },
shadowOpacity: 0.12, cardAccent: {
shadowOffset: { width: 0, height: 3 }, height: 4,
shadowRadius: 8, backgroundColor: colors.primary,
},
cardBody: {
padding: spacing.m,
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'flex-start',
marginBottom: 12, marginBottom: spacing.s,
}, },
title: { indexBadge: {
fontSize: 16, width: 32,
fontWeight: '700', height: 32,
color: '#0F172A', borderRadius: 10,
},
pattern: {
fontSize: 14,
color: colors.primary,
fontWeight: '600',
},
cardRow: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 6,
marginBottom: 4,
},
text: {
fontSize: 14,
color: '#555',
},
totalRow: {
marginTop: 10,
backgroundColor: colors.primary,
borderRadius: 12,
paddingVertical: 10,
flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
marginTop: 2,
},
patternBadge: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 4,
alignSelf: 'flex-start',
paddingHorizontal: spacing.s,
paddingVertical: 2,
borderRadius: 8,
marginTop: 4,
}, },
total: { divider: {
fontSize: 15, height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginVertical: spacing.s,
},
feeSection: {
borderRadius: 12,
padding: spacing.s + 2,
marginBottom: spacing.s,
},
feeHeader: {
letterSpacing: 1.2,
marginBottom: spacing.xs + 2,
fontWeight: '700', fontWeight: '700',
color: '#fff', } as any,
totalBanner: {
borderRadius: 14,
paddingVertical: 12,
paddingHorizontal: spacing.m,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}, },
bottomButtonContainer: { totalLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
/* Footer */
footer: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0,
flexDirection: 'row', flexDirection: 'row',
backgroundColor: colors.white, paddingHorizontal: spacing.m,
width: '100%', paddingTop: spacing.s + 4,
paddingVertical: 20, paddingBottom: spacing.m + 4,
paddingHorizontal: 10, backgroundColor: colors.surface,
elevation: 5 gap: spacing.s,
...shadows.strong,
}, },
button: { button: {
flex: 1, flex: 1,
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 14,
borderRadius: 16,
gap: spacing.s,
}, },
buttonText: { onlineButton: {
color: '#fff', backgroundColor: colors.surface,
fontWeight: '700', borderWidth: 1.5,
fontSize: 14,
}, },
offlineButton: {
...shadows.medium,
},
btnText: {
letterSpacing: 1.2,
} as any,
/* Loading overlay */
loadingOverlay: { loadingOverlay: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.2)', backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
zIndex: 10, zIndex: 10,
}, },
loadingCard: {
borderRadius: 20,
padding: spacing.l,
alignItems: 'center',
...shadows.strong,
},
}); });
+221 -185
View File
@@ -1,262 +1,298 @@
import React from 'react'; import React, { useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native'; import { View, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import moment from 'moment'; import moment from 'moment';
import CountdownTimer from '../../Component/Counter/CountdownTimer'; import CountdownTimer from '../../Component/Counter/CountdownTimer';
import colors from '../../Constant/Color';
import AuctionDetail from './Modal/AuctionDetail'; import AuctionDetail from './Modal/AuctionDetail';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import {
Tag,
Calendar,
IndianRupee,
ArrowRight,
CircleDot,
Clock,
TimerOff,
} from 'lucide-react-native';
const PurchaseAuctionTile = ({ item, onPressDetails, onStartBidding, buttonEnable, isAuctionEnd }: any) => { const PurchaseAuctionTile = ({
item,
onPressDetails,
onStartBidding,
buttonEnable,
isAuctionEnd,
}: any) => {
const { colors, spacing, shadows } = useTheme();
const [showDetail, setShowDetail] = React.useState(false); const [showDetail, setShowDetail] = React.useState(false);
const scale = useSharedValue(1); const scale = useSharedValue(1);
const elevation = useSharedValue(8);
const animatedStyle = useAnimatedStyle(() => { const animatedStyle = useAnimatedStyle(() => ({
return {
transform: [{ scale: scale.value }], transform: [{ scale: scale.value }],
elevation: elevation.value, // Android-safe }));
};
}, []);
const handlePressIn = () => { const handlePressIn = () => scale.value = withTiming(0.98, { duration: 140 });
scale.value = withTiming(0.97, { duration: 120 }); const handlePressOut = () => scale.value = withTiming(1, { duration: 160 });
elevation.value = withTiming(3, { duration: 120 });
};
const handlePressOut = () => { const isAmended = item?.auctionStatus === 'Amended';
scale.value = withTiming(1, { duration: 160 }); const biddingEnd = isAmended
elevation.value = withTiming(8, { duration: 160 }); ? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '')
}; : item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', '');
const biddingStart = isAmended
? item?.amendedkeyDates?.auctionBidding?.startDateTime?.replace(' kk', '')
: item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', '');
const statusColor = isAuctionEnd ? '#EF4444' : buttonEnable ? colors.primary : '#94A3B8'; const statusConfig = useMemo(() => {
if (isAuctionEnd) return { label: 'ENDED', color: colors.error, bg: `${colors.error}15`, Icon: TimerOff };
if (buttonEnable) return { label: 'LIVE', color: colors.success, bg: `${colors.success}15`, Icon: CircleDot };
return { label: 'PENDING', color: colors.textSecondary, bg: `${colors.textSecondary}12`, Icon: Clock };
}, [isAuctionEnd, buttonEnable, colors]);
const lightStatusBg = isAuctionEnd ? '#FEF2F2' : buttonEnable ? '#EFF6FF' : '#F8FAFC'; const styles = makeStyles(colors, spacing);
if (showDetail) {
return <AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />;
}
return ( return (
<View style={{ flex: 1 }}> <Animated.View
{!showDetail ? <Animated.View entering={FadeInUp.duration(450).springify()}
entering={FadeInUp.duration(400)} style={[styles.container, shadows.medium, animatedStyle]}
style={[styles.shadowContainer, animatedStyle]}
> >
<TouchableOpacity <TouchableOpacity
activeOpacity={1} activeOpacity={1}
onPress={() => setShowDetail(true)} onPress={() => setShowDetail(true)}
onPressIn={handlePressIn} onPressIn={handlePressIn}
onPressOut={handlePressOut} onPressOut={handlePressOut}
style={styles.cardInternal} style={styles.card}
> >
<View style={[styles.statusAccent, { backgroundColor: statusColor }]} /> {/* ── Header ── */}
<View style={styles.header}>
<View style={styles.headerTop}>
{/* Status pill */}
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bg }]}>
<statusConfig.Icon size={11} color={statusConfig.color} strokeWidth={3} />
<AppText variant="caption" color={statusConfig.color} style={styles.statusText as any}>
{statusConfig.label}
</AppText>
</View>
{/* Auction number */}
<View style={styles.numberRow}>
<Tag size={12} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary} style={styles.numberText as any}>
{item?.auctionNumber}
</AppText>
</View>
</View>
<View style={styles.mainInfo}> {/* Work title */}
{/* Header */} <AppText variant="bodySmall" color={colors.text} numberOfLines={2} style={styles.workTitle as any}>
<View style={styles.headerRow}> {item?.auctionOfWork ?? item?.auctionNumber ?? 'Auction Details'}
</AppText>
</View>
<View style={styles.divider} />
{/* ── Info Grid ── */}
<View style={styles.infoGrid}>
{/* Fee */}
<View style={styles.infoRow}>
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<IndianRupee size={17} color={colors.primary} strokeWidth={2} />
</View>
<View> <View>
<View style={[styles.statusBadge, { backgroundColor: lightStatusBg }]}> <AppText variant="caption" color={colors.textSecondary}>Auction Fee</AppText>
<View style={[styles.statusDot, { backgroundColor: statusColor }]} /> <AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
<Text style={[styles.statusText, { color: statusColor }]}> ₹{Number(item?.auctionFees ?? 0).toLocaleString('en-IN')}
{isAuctionEnd ? 'ENDED' : buttonEnable ? 'LIVE' : 'PENDING'} </AppText>
</Text>
</View>
<Text style={styles.title}>{item.auctionNumber}</Text>
</View>
<View style={styles.amountContainer}>
<Text style={styles.amount}>
₹{item.auctionFees?.toLocaleString()}
</Text>
<Text style={styles.feeLabel}>Auction Fee</Text>
</View> </View>
</View> </View>
{/* Date */} {/* Bidding End */}
<View style={styles.dateRow}> <View style={styles.infoRow}>
<Text style={styles.dateText}> <View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<Text style={styles.dateLabel}>Bidding Start: </Text> <Calendar size={17} color={colors.primary} strokeWidth={2} />
{moment( </View>
item?.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.startDateTime?.replace(' kk', '') : item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', ''), <View>
'DD-MM-YYYY HH:mm:ss' <AppText variant="caption" color={colors.textSecondary}>Bidding Ends</AppText>
).format('DD MMM YYYY, hh:mm A')} <AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
</Text> {biddingEnd
<Text style={styles.dateText}> ? moment(biddingEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')
<Text style={styles.dateLabel}>Bidding Ends: </Text> : '—'}
{moment( </AppText>
item?.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '') : item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', ''), </View>
'DD-MM-YYYY HH:mm:ss' </View>
).format('DD MMM YYYY, hh:mm A')}
</Text>
</View> </View>
{/* Footer */} {/* ── Footer ── */}
<View style={styles.footer}> <View style={styles.footer}>
<View style={styles.footerTop}> <AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' } as any}>
<Text style={styles.footerLabel}>Remaining Time</Text> Time Remaining
</AppText>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
{/* Timer */}
<View style={styles.timerSection}>
{buttonEnable && !isAuctionEnd ? ( {buttonEnable && !isAuctionEnd ? (
<CountdownTimer <CountdownTimer endDate={biddingEnd} />
endDate={item?.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.endDateTime : item?.keyDates?.auctionBidding?.endDateTime}
/>
) : ( ) : (
<Text style={styles.endedText}>--:--:--</Text> <AppText variant="bodySmall" color={colors.textSecondary} style={{ fontWeight: '700' } as any}>
-- : -- : --
</AppText>
)} )}
</View> </View>
{/* CTA Button */}
<TouchableOpacity <TouchableOpacity
disabled={!buttonEnable} disabled={!buttonEnable || isAuctionEnd}
onPress={onStartBidding} onPress={onStartBidding}
activeOpacity={0.85} activeOpacity={0.85}
style={[ style={[
styles.actionButton, styles.ctaBtn,
{ backgroundColor: statusColor }, {
!buttonEnable && styles.disabledButton, backgroundColor:
isAuctionEnd
? colors.error
: buttonEnable
? colors.primary
: colors.border,
opacity: (!buttonEnable || isAuctionEnd) ? 0.8 : 1,
},
]} ]}
> >
<Text style={styles.addText}> <AppText
{isAuctionEnd variant="button"
? 'Bidding Closed' color={colors.buttonText}
: !buttonEnable style={styles.ctaText as any}
? 'Pending Bid' >
: 'Proceed to Start Bidding'} {isAuctionEnd ? 'Ended' : !buttonEnable ? 'Pending' : 'Bid Now'}
</Text> </AppText>
{buttonEnable && !isAuctionEnd && (
<ArrowRight size={15} color={colors.buttonText} strokeWidth={2.5} />
)}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</Animated.View> : </Animated.View>
<AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
}
</View>
); );
}; };
export default PurchaseAuctionTile; export default PurchaseAuctionTile;
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any) =>
shadowContainer: { StyleSheet.create({
marginHorizontal: 5, container: {
marginTop: 14, marginHorizontal: spacing.s,
marginBottom: 6, marginBottom: spacing.m,
borderRadius: 16, borderRadius: 16,
backgroundColor: '#fff', borderWidth: 1,
elevation: 8, borderColor: colors.border,
backgroundColor: colors.surface,
}, },
cardInternal: { card: {
borderRadius: 16, borderRadius: 16,
overflow: 'hidden', backgroundColor: colors.surface,
flexDirection: 'row', padding: spacing.m,
backgroundColor: '#fff', borderWidth: 1,
borderColor: 'rgba(0,0,0,0.03)',
}, },
statusAccent: {
width: 5, /* Header */
height: '100%', header: {
marginBottom: spacing.m,
}, },
mainInfo: { headerTop: {
flex: 1,
padding: 16,
},
headerRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'center',
marginBottom: 12, marginBottom: spacing.s,
}, },
statusBadge: { statusBadge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 8, paddingHorizontal: 10,
paddingVertical: 3, paddingVertical: 5,
borderRadius: 6, borderRadius: 20,
marginBottom: 6, gap: 5,
alignSelf: 'flex-start',
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 5,
}, },
statusText: { statusText: {
fontSize: 10,
fontWeight: '800', fontWeight: '800',
fontSize: 10,
textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: 0.5,
} as any,
numberRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
}, },
title: { numberText: {
fontWeight: '800', letterSpacing: 0.4,
fontSize: 16,
color: '#1E293B',
},
amountContainer: {
alignItems: 'flex-end',
},
amount: {
fontWeight: '900',
fontSize: 18,
color: '#0F172A',
},
feeLabel: {
fontSize: 10,
color: '#64748B',
fontWeight: '500',
},
dateRow: {
backgroundColor: '#F8FAFC',
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 10,
marginBottom: 16,
borderWidth: 1,
borderColor: '#F1F5F9',
},
dateText: {
fontSize: 12,
color: '#334155',
fontWeight: '600', fontWeight: '600',
} as any,
workTitle: {
fontWeight: '600',
lineHeight: 20,
} as any,
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginBottom: spacing.m,
opacity: 0.6,
}, },
dateLabel: {
fontWeight: '400', /* Info grid */
color: '#64748B', infoGrid: {
},
footer: {
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
paddingTop: 12,
},
footerTop: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: spacing.l,
gap: spacing.s,
},
infoRow: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, gap: 10,
flex: 1,
}, },
footerLabel: { iconBox: {
fontSize: 11, width: 36,
fontWeight: '600', height: 36,
color: '#94A3B8', borderRadius: 10,
textTransform: 'uppercase', alignItems: 'center',
},
endedText: {
fontSize: 13,
fontWeight: '700',
color: '#CBD5E1',
},
actionButton: {
width: '100%',
height: 48,
borderRadius: 12,
justifyContent: 'center', justifyContent: 'center',
},
infoValue: {
fontWeight: '700',
} as any,
/* Footer */
footer: {
justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
...Platform.select({ backgroundColor: colors.background,
ios: { marginHorizontal: -spacing.m,
shadowColor: '#000', marginBottom: -spacing.m,
shadowOffset: { width: 0, height: 2 }, padding: spacing.m,
shadowOpacity: 0.1, borderBottomLeftRadius: 16,
shadowRadius: 4, borderBottomRightRadius: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
}, },
}), timerSection: {
flex: 1,
}, },
disabledButton: { ctaBtn: {
backgroundColor: '#E2E8F0', flexDirection: 'row',
}, alignItems: 'center',
addText: { justifyContent: 'center',
color: '#fff', paddingVertical: 10,
fontSize: 14, paddingHorizontal: 18,
fontWeight: '800', borderRadius: 12,
letterSpacing: 0.2, gap: 5,
}, },
ctaText: {
letterSpacing: 0.8,
} as any,
}); });
+318 -158
View File
@@ -1,227 +1,387 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; import {
import Animated, { FadeIn, FadeOut, Layout, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; View,
import { ChevronDown, Trash2, Info } from 'lucide-react-native'; StyleSheet,
ScrollView,
TouchableOpacity,
} from 'react-native';
import Animated, {
FadeIn,
FadeOut,
FadeInDown,
Layout,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, Gavel, Tag } from 'lucide-react-native';
import { InsideHeader } from '../../Component/Index'; import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction'; import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const PurchaseList = () => { const PurchaseList = () => {
const Navigation: any = useNavigation() const Navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [expandedIndex, setExpandedIndex] = useState<number | null>(null); const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const toggleExpand = (index: number) => { const toggleExpand = (index: number) => {
setExpandedIndex(prev => (prev === index ? null : index)); setExpandedIndex(prev => (prev === index ? null : index));
}; };
useEffect(() => { useEffect(() => {
if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) { if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) {
Navigation.goBack(); Navigation.goBack();
} }
}, [SelectedPurchaseList]); }, [SelectedPurchaseList]);
const styles = makeStyles(colors, spacing, shadows);
return ( return (
<View style={styles.screen}> <View style={styles.screen}>
<InsideHeader title="Purchase" showArrow /> <InsideHeader title="Purchase Auctions" showArrow />
<ScrollView contentContainerStyle={styles.container}> {/* Summary Strip */}
{/* Header */} <View style={styles.summaryStrip}>
<View style={styles.header}> <View style={styles.summaryIcon}>
<Text style={styles.heading}>Purchase List</Text> <ShoppingBag size={16} color={colors.primary} />
{/* <Info size={20} color="#0F172A" /> */} </View>
<AppText variant="caption" color={colors.textSecondary}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' } as any}>
{SelectedPurchaseList.length}
</AppText>
{' '}auction{SelectedPurchaseList.length !== 1 ? 's' : ''} selected for purchase
</AppText>
</View> </View>
{/* Cards */} <ScrollView
{SelectedPurchaseList.map((item: any, index: any) => { contentContainerStyle={styles.container}
const expanded = expandedIndex === index; showsVerticalScrollIndicator={false}
return ( >
<Animated.View {SelectedPurchaseList.map((item: any, index: number) => (
<PurchaseListItem
key={item.auctionId} key={item.auctionId}
layout={Layout.springify()} item={item}
style={styles.card} index={index}
> expandedIndex={expandedIndex}
<TouchableOpacity toggleExpand={toggleExpand}
activeOpacity={0.9} colors={colors}
onPress={() => toggleExpand(index)} styles={styles}
> Dispatch={Dispatch}
<CardHeader
title={item.auctionOfWork}
expanded={expanded}
/> />
))}
<Text style={styles.subTitle}> <View style={{ height: 90 }} />
Auction No: {item.auctionNumber}
</Text>
<Text style={styles.subTitle}>
Status:{' '}
<Text
style={{
color: item.status ? '#16A34A' : '#DC2626',
fontWeight: '700',
}}
>
{item.status ? 'Open' : 'Closed'}
</Text>
</Text>
</TouchableOpacity>
{/* Expanded Content */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(180)}
layout={Layout.springify()}
style={styles.details}
>
{renderRow('Auction Mode', item.auctionMode)}
{renderRow('Pattern', item.auctionPattern)}
{renderRow('Fees', `₹ ${item.auctionFees}`)}
{renderRow('Department', item.deptName)}
{renderRow('Purchase End', item.purchaseEnd)}
<TouchableOpacity style={styles.removeButton} onPress={() => { Dispatch(togglePurchaseAuction(item)) }}>
<Trash2 size={18} color="#fff" />
<Text style={styles.removeText}>Remove</Text>
</TouchableOpacity>
</Animated.View>
)}
</Animated.View>
);
})}
</ScrollView> </ScrollView>
{/* Footer */} {/* Floating Proceed Button */}
<TouchableOpacity disabled={Boolean(!SelectedPurchaseList.length)} style={[styles.proceedButton, { backgroundColor: Boolean(!SelectedPurchaseList.length) ? colors.lightText : colors.primary }]} onPress={() => { Navigation.navigate(NavigationString.PurchaseAuctionList) }}> <View style={styles.footerContainer}>
<Text style={styles.proceedText}>Proceed to Purchase</Text> <TouchableOpacity
activeOpacity={0.9}
disabled={!SelectedPurchaseList.length}
style={[
styles.proceedButton,
{
backgroundColor: SelectedPurchaseList.length
? colors.buttonPrimary
: colors.secondary,
},
]}
onPress={() => Navigation.navigate(NavigationString.PurchaseAuctionList)}
>
<AppText variant="button" color={colors.buttonText} style={styles.proceedText as any}>
Proceed to Purchase
</AppText>
<View style={styles.countBadge}>
<AppText variant="caption" color={colors.buttonPrimary} style={{ fontWeight: '800' } as any}>
{SelectedPurchaseList.length}
</AppText>
</View>
<ChevronRight size={20} color={colors.buttonText} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View>
); );
}; };
export default PurchaseList; export default PurchaseList;
const CardHeader = ({ title, expanded }: { /* ─── Sub-components ─── */
title: string;
expanded: boolean; const PurchaseListItem = ({ item, index, expandedIndex, toggleExpand, colors, styles, Dispatch }: any) => {
}) => { const expanded = expandedIndex === index;
const rotation = useSharedValue(expanded ? 1 : 0); const isActive = item.status;
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 }); const chevronStyle = useChevronStyle(expanded);
const arrowStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
return ( return (
<View style={styles.cardHeader}> <Animated.View
<Text style={styles.title}>{title}</Text> entering={FadeInDown.delay(index * 80).springify()}
<Animated.View style={arrowStyle}> layout={Layout.springify()}
<ChevronDown size={22} color={colors.black} /> style={[styles.card, expanded && styles.cardExpanded]}
>
{/* Left Accent Bar */}
<View style={[styles.accentBar, { backgroundColor: isActive ? colors.success : colors.primary }]} />
<View style={styles.cardInner}>
<TouchableOpacity
activeOpacity={0.85}
onPress={() => toggleExpand(index)}
>
{/* Badge Row */}
<View style={styles.badgeRow}>
<View style={styles.auctionBadge}>
<Gavel size={11} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} style={styles.badgeText as any}>
{item.auctionNumber}
</AppText>
</View>
</View>
{/* Title + Chevron */}
<View style={styles.titleRow}>
<AppText variant="h4" style={styles.title as any} numberOfLines={2}>
{item.auctionOfWork ?? item.auctionNumber}
</AppText>
<Animated.View style={chevronStyle}>
<ChevronDown size={20} color={colors.textSecondary} />
</Animated.View> </Animated.View>
</View> </View>
{/* Dept Tag */}
<View style={styles.deptRow}>
<Tag size={12} color={colors.textSecondary} />
<AppText variant="caption" color={colors.textSecondary} style={styles.deptText as any}>
{item.deptName || 'Department'}
</AppText>
</View>
</TouchableOpacity>
{/* Expanded Section */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(160)}
layout={Layout.springify()}
style={styles.detailsContainer}
>
<View style={styles.divider} />
<DetailRow label="Auction Mode" value={item.auctionMode} colors={colors} />
<DetailRow label="Pattern" value={item.auctionPattern} colors={colors} />
<DetailRow label="Auction Fees" value={`₹ ${item.auctionFees}`} colors={colors} valueColor={colors.primary} />
<DetailRow label="Department" value={item.deptName} colors={colors} />
<DetailRow label="Purchase End" value={item.purchaseEnd} colors={colors} />
{/* Remove Button */}
<TouchableOpacity
style={styles.removeButton}
activeOpacity={0.85}
onPress={() => Dispatch(togglePurchaseAuction(item))}
>
<Trash2 size={16} color="#fff" />
<AppText variant="button" color="#fff" style={styles.removeText as any}>
Remove
</AppText>
</TouchableOpacity>
</Animated.View>
)}
</View>
</Animated.View>
); );
}; };
const renderRow = (label: string, value: string) => ( const DetailRow = ({
<View style={styles.row}> label,
<Text style={styles.label}>{label}</Text> value,
<Text style={styles.value}>{value}</Text> colors,
valueColor,
}: {
label: string;
value: string;
colors: any;
valueColor?: string;
}) => (
<View style={detailRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary} style={detailRowStyles.label as any}>
{label}
</AppText>
<AppText variant="caption" color={valueColor || colors.text} style={detailRowStyles.value as any}>
{value ?? '—'}
</AppText>
</View> </View>
); );
const detailRowStyles = StyleSheet.create({
const styles = StyleSheet.create({ row: {
screen: {
flex: 1,
backgroundColor: '#F8FAFC',
},
container: {
padding: 16,
paddingBottom: 110,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 14,
},
heading: {
fontSize: 22,
fontWeight: '800',
color: colors.black,
},
card: {
backgroundColor: '#fff',
borderRadius: 18,
padding: 16,
marginBottom: 14,
borderWidth: 1,
borderColor: '#E5E7EB',
},
cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
}, paddingVertical: 8,
title: { borderBottomWidth: StyleSheet.hairlineWidth,
fontSize: 16, borderBottomColor: 'rgba(0,0,0,0.06)',
fontWeight: '700',
color: '#0F172A',
flex: 1,
marginRight: 12,
},
subTitle: {
fontSize: 14,
color: '#64748B',
marginTop: 6,
},
details: {
marginTop: 16,
paddingTop: 14,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
row: {
marginVertical: 6,
}, },
label: { label: {
fontSize: 13, flex: 1,
color: '#64748B', fontWeight: '500',
}, } as any,
value: { value: {
fontSize: 15, flex: 1,
textAlign: 'right',
fontWeight: '600', fontWeight: '600',
color: '#0F172A', } as any,
});
/* ─── Chevron animated style hook ─── */
function useChevronStyle(expanded: boolean) {
const rotation = useSharedValue(expanded ? 1 : 0);
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
return useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
}
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: {
flex: 1,
backgroundColor: colors.background,
},
summaryStrip: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
borderRadius: 14,
gap: spacing.s,
...shadows.light,
},
summaryIcon: {
width: 28,
height: 28,
borderRadius: 8,
backgroundColor: `${colors.primary}15`,
alignItems: 'center',
justifyContent: 'center',
},
container: {
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
},
card: {
backgroundColor: colors.surface,
borderRadius: 20,
marginBottom: spacing.m,
flexDirection: 'row',
overflow: 'hidden',
...shadows.medium,
},
cardExpanded: {
...shadows.strong,
},
accentBar: {
width: 5,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
},
cardInner: {
flex: 1,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
},
badgeRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: spacing.s,
},
auctionBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: `${colors.primary}15`,
paddingHorizontal: spacing.s,
paddingVertical: 3,
borderRadius: 8,
gap: 4,
},
badgeText: {
fontWeight: '600',
letterSpacing: 0.2,
} as any,
titleRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: spacing.s,
},
title: {
flex: 1,
fontWeight: '700',
} as any,
deptRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
marginTop: spacing.xs + 2,
},
deptText: {
letterSpacing: 0.3,
} as any,
detailsContainer: {
marginTop: spacing.s,
},
divider: {
height: 1,
backgroundColor: colors.border,
marginBottom: spacing.s,
}, },
removeButton: { removeButton: {
marginTop: 18, marginTop: spacing.m,
backgroundColor: colors.primary, backgroundColor: colors.error,
borderRadius: 14, borderRadius: 14,
paddingVertical: 12, paddingVertical: 12,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
gap: 8, alignItems: 'center',
gap: spacing.s,
}, },
removeText: { removeText: {
color: '#fff', letterSpacing: 1,
fontWeight: '700', } as any,
footerContainer: {
position: 'absolute',
bottom: 20,
left: spacing.m,
right: spacing.m,
}, },
proceedButton: { proceedButton: {
position: 'absolute', flexDirection: 'row',
bottom: 16,
left: 16,
right: 16,
backgroundColor: colors.primary,
paddingVertical: 16,
borderRadius: 18,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
paddingVertical: 16,
borderRadius: 20,
gap: spacing.s,
...shadows.strong,
}, },
proceedText: { proceedText: {
color: '#fff', letterSpacing: 1.5,
fontSize: 16, } as any,
fontWeight: '800', countBadge: {
backgroundColor: '#fff',
borderRadius: 12,
minWidth: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
}, },
}); });
+50 -65
View File
@@ -1,9 +1,11 @@
import React, { memo, useCallback, useRef, useState } from 'react'; import React, { memo, useCallback, useRef, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native'; import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native';
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import colors from '../../../Constant/Color'; import colors_const from '../../../Constant/Color';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail'; import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle'; import MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import { Loader } from '../../../Component/Index'; import { Loader } from '../../../Component/Index';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import axios from 'axios'; import axios from 'axios';
@@ -11,6 +13,8 @@ import Variables from '../../../Constant/Variable';
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog'; import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => { const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const Token = useSelector((state: any) => state.token.token) const Token = useSelector((state: any) => state.token.token)
const [showDetail, setShowDetail] = useState(false); const [showDetail, setShowDetail] = useState(false);
const [value, setValue] = useState(`${item.bidAmount}`); const [value, setValue] = useState(`${item.bidAmount}`);
@@ -100,7 +104,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
!showDetail ? <Animated.View !showDetail ? <Animated.View
layout={Layout.springify()} layout={Layout.springify()}
entering={FadeInUp.delay(100)} entering={FadeInUp.delay(100)}
style={[styles.card, styles.leadingCard, { borderColor: item.me ? '#22C55E' : 'red', backgroundColor: item.me ? "#F0FDF4" : '#fdf0f0ff' }]} style={[styles.card, item.me ? styles.leadingCard : undefined, { borderColor: item.me ? colors.success : colors.error, backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10` }]}
> >
{/* HEADER */} {/* HEADER */}
<TouchableOpacity <TouchableOpacity
@@ -109,28 +113,28 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
style={styles.cardHeader} style={styles.cardHeader}
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text> <AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
{/* auctionStartValueFigure */} {/* auctionStartValueFigure */}
<Text style={styles.itemIdText}> <AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
Initial Amt : {item?.auctionStartValueFigure} Initial Amt : {item?.auctionStartValueFigure}
</Text> </AppText>
</View> </View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}> <TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
<View style={[MainStyle.flexBetween, { gap: 10 }]}> <View style={[MainStyle.flexBetween, { gap: 10 }]}>
<View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}> <View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}>
<Text style={[styles.badgeText, item.me ? styles.meText : styles.liveText]}> <AppText variant="caption" bold style={[styles.badgeText as any, item.me ? styles.meText : styles.liveText]}>
{item.me ? 'LEADING' : 'LIVE'} {item.me ? 'LEADING' : 'LIVE'}
</Text> </AppText>
</View> </View>
<Animated.Text style={[styles.arrow, arrowStyle]}> <Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
▼ ▼
</Animated.Text> </Animated.Text>
</View> </View>
<Text style={styles.itemIdText}> <AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
{SelectedAuction?.auctionNumber} {SelectedAuction?.auctionNumber}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
</TouchableOpacity> </TouchableOpacity>
@@ -138,15 +142,15 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
{/* PRICE INFO */} {/* PRICE INFO */}
<View style={styles.priceRow}> <View style={styles.priceRow}>
<View> <View>
<Text style={styles.label}>Current Bid</Text> <AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
<Text style={styles.mainPrice}>₹{item?.bidAmount || item.auctionStartValueFigure}</Text> <AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item?.bidAmount || item.auctionStartValueFigure}</AppText>
</View> </View>
<View style={{ alignItems: 'flex-end' }}> <View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Increment</Text> <AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Increment</AppText>
<Text style={styles.incrementText}> <AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
+₹{item.bidVariationValue} +₹{item.bidVariationValue}
</Text> </AppText>
</View> </View>
</View> </View>
@@ -154,7 +158,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
<Animated.View style={[styles.collapsibleContainer, (!allBoolean.rebid && item.contractorId == undefined) ? bodyStyle : bodyStyle2]}> <Animated.View style={[styles.collapsibleContainer, (!allBoolean.rebid && item.contractorId == undefined) ? bodyStyle : bodyStyle2]}>
<View style={styles.divider} /> <View style={styles.divider} />
<Text style={styles.bidLabel}>Set Your Bid</Text> <AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
{!allBoolean.oneTimeBid && <View style={styles.stepperContainer}> {!allBoolean.oneTimeBid && <View style={styles.stepperContainer}>
<TouchableOpacity <TouchableOpacity
@@ -162,29 +166,29 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
disabled={isMin} disabled={isMin}
style={[styles.stepBtn, isMin && styles.disabledBtn]} style={[styles.stepBtn, isMin && styles.disabledBtn]}
> >
<Text style={styles.stepText}>−</Text> <AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.inputBox}> <View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text> <AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
<Text style={styles.bidValueText}>{bidValue}</Text> <AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
</View> </View>
<TouchableOpacity onPress={onIncrease} style={styles.stepBtn}> <TouchableOpacity onPress={onIncrease} style={styles.stepBtn}>
<Text style={styles.stepText}>+</Text> <AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
</TouchableOpacity> </TouchableOpacity>
</View>} </View>}
{ {
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}> (allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
<Text style={styles.currency2}>₹</Text> <AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
<TextInput <TextInput
value={value} value={value}
onChangeText={handleChange} onChangeText={handleChange}
keyboardType="decimal-pad" keyboardType="decimal-pad"
placeholder="0.00" placeholder="0.00"
style={styles.input} style={[styles.input, { color: colors.text }]}
placeholderTextColor="#94A3B8" placeholderTextColor={colors.textSecondary}
/> />
</View> </View>
} }
@@ -194,7 +198,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid} onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
> >
{ {
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text> allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
} }
</TouchableOpacity> : null} </TouchableOpacity> : null}
</Animated.View> </Animated.View>
@@ -209,11 +213,11 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
export default memo(BiddingCard); export default memo(BiddingCard);
const styles = StyleSheet.create({ const makeStyles = (colors: any) => StyleSheet.create({
inputBox2: { inputBox2: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#F1F5F9', backgroundColor: colors.background,
borderRadius: 12, borderRadius: 12,
height: 45, height: 45,
paddingHorizontal: 12, paddingHorizontal: 12,
@@ -221,39 +225,23 @@ const styles = StyleSheet.create({
marginBottom: 20 marginBottom: 20
}, },
currency2: { currency2: {
fontSize: 16,
fontWeight: 'bold',
color: '#64748B',
marginRight: 4, marginRight: 4,
}, } as any,
input: { input: {
flex: 1, flex: 1,
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: '#0F172A',
width: '100%' width: '100%'
}, },
card: { card: {
backgroundColor: '#FFF',
borderRadius: 24, borderRadius: 24,
padding: 20, padding: 20,
marginBottom: 16, marginBottom: 16,
borderWidth: 1, borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden', overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
}, },
leadingCard: { leadingCard: {
borderColor: '#22C55E',
borderWidth: 2, borderWidth: 2,
backgroundColor: '#F0FDF4',
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
@@ -261,27 +249,24 @@ const styles = StyleSheet.create({
alignItems: 'flex-start', alignItems: 'flex-start',
}, },
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 }, headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' }, productName: {} as any,
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 }, itemIdText: { marginTop: 2 } as any,
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' }, liveBadge: { backgroundColor: `${colors.error}20` },
meBadge: { backgroundColor: '#BBF7D0' }, meBadge: { backgroundColor: `${colors.success}20` },
badgeText: { fontSize: 10, fontWeight: '900' }, badgeText: { fontSize: 10 } as any,
liveText: { color: '#EF4444' }, liveText: { color: colors.error },
meText: { color: '#15803D' }, meText: { color: colors.success },
arrow: { fontSize: 10, color: '#94A3B8' }, arrow: { fontSize: 10 },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 }, priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { label: {
fontSize: 11,
color: '#94A3B8',
textTransform: 'uppercase', textTransform: 'uppercase',
fontWeight: '600', } as any,
}, mainPrice: {} as any,
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' }, incrementText: {} as any,
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' }, collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 }, divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 }, bidLabel: { marginBottom: 10 } as any,
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 }, stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { stepBtn: {
width: 45, width: 45,
@@ -291,19 +276,19 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
disabledBtn: { backgroundColor: '#CBD5E1' }, disabledBtn: { backgroundColor: colors.border },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' }, stepText: {} as any,
inputBox: { inputBox: {
flex: 1, flex: 1,
height: 45, height: 45,
backgroundColor: '#F1F5F9', backgroundColor: colors.background,
borderRadius: 12, borderRadius: 12,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 }, currency: { marginRight: 4 } as any,
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' }, bidValueText: {} as any,
submitBtn: { submitBtn: {
height: 50, height: 50,
backgroundColor: colors.primary, backgroundColor: colors.primary,
@@ -311,5 +296,5 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' }, submitBtnText: {} as any,
}); });
@@ -0,0 +1,133 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { ShieldCheck, X } from 'lucide-react-native';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
visible: boolean;
onClose: () => void;
onConfirm: () => void;
bidAmount: number;
isLoading?: boolean;
}
const ConfirmBidModal = ({ visible, onClose, onConfirm, bidAmount, isLoading }: Props) => {
const { colors, shadows } = useTheme();
if (!visible) return null;
return (
<View style={styles.overlay}>
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
</TouchableOpacity>
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<ShieldCheck size={32} color={colors.primary} strokeWidth={2} />
</View>
<AppText variant="h3" color={colors.text} bold centered style={styles.title}>
Confirm Your Bid
</AppText>
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle}>
Are you sure you want to place this bid? This action cannot be undone.
</AppText>
<View style={[styles.amountBox, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}30` }]}>
<AppText variant="caption" color={colors.primary} bold>
BID AMOUNT
</AppText>
<AppText variant="h1" color={colors.primary} bold>
₹{bidAmount?.toLocaleString('en-IN') || 0}
</AppText>
</View>
<View style={styles.btnRow}>
<TouchableOpacity style={[styles.btn, styles.cancelBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
<AppText variant="button" color={colors.text} bold>Cancel</AppText>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, styles.confirmBtn, { backgroundColor: colors.primary }]} onPress={onConfirm} disabled={isLoading}>
{isLoading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<AppText variant="button" color="#fff" bold>Submit Bid</AppText>
)}
</TouchableOpacity>
</View>
</View>
</View>
);
};
export default ConfirmBidModal;
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
zIndex: 9999,
elevation: 9999,
},
modalCard: {
width: '100%',
borderRadius: 24,
padding: 24,
alignItems: 'center',
},
closeBtn: {
position: 'absolute',
top: 16,
right: 16,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
iconBox: {
width: 64,
height: 64,
borderRadius: 32,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
title: {
marginBottom: 8,
},
subtitle: {
marginBottom: 24,
paddingHorizontal: 10,
},
amountBox: {
width: '100%',
paddingVertical: 16,
borderRadius: 16,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
gap: 4,
},
btnRow: {
flexDirection: 'row',
gap: 12,
width: '100%',
},
btn: {
flex: 1,
height: 50,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
cancelBtn: {},
confirmBtn: {},
});
+178 -74
View File
@@ -1,105 +1,209 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, StyleSheet, TouchableOpacity } from 'react-native';
import LottieView from 'lottie-react-native'; import LottieView from 'lottie-react-native';
import Animated, { FadeInUp } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeInUp, ZoomIn } from 'react-native-reanimated';
import colors from '../../../Constant/Color'; import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import {
ShieldCheck,
CheckCircle2,
FileText,
Gavel,
ArrowRight,
} from 'lucide-react-native';
const STEPS = [
{ icon: FileText, label: 'Documents Submitted', done: true },
{ icon: ShieldCheck, label: 'EMD Verified', done: true },
{ icon: Gavel, label: 'Ready to Bid', done: true },
];
const EmdComplete = ({ onNext }: { onNext?: () => void }) => { const EmdComplete = ({ onNext }: { onNext?: () => void }) => {
return ( const { colors, spacing, shadows } = useTheme();
<View style={styles.container}>
<LottieView
source={require('../../../lottie/confetti.json')}
autoPlay
loop={true}
style={styles.confetti}
/>
return (
<View style={[styles.root, { backgroundColor: colors.background }]}>
{/* ── Center content ── */}
<View style={styles.centerContent}>
{/* Success animation bubble */}
<Animated.View entering={ZoomIn.delay(100).springify()} style={[styles.lottieBubble, { backgroundColor: `${colors.primary}12` }]}>
<LottieView <LottieView
source={require('../../../lottie/Success.json')} source={require('../../../lottie/Success.json')}
autoPlay autoPlay
loop={true} loop={false}
style={styles.success} style={styles.lottie}
/> />
</Animated.View>
<Animated.Text entering={FadeInUp.delay(400).duration(700)} style={styles.title}> {/* Title */}
EMD Complete <Animated.View entering={FadeInUp.delay(400).springify()} style={styles.titleBlock}>
</Animated.Text> <AppText variant="h2" color={colors.text} bold centered>
EMD Submitted!
</AppText>
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle as any}>
Your Earnest Money Deposit has been{'\n'}successfully recorded.
</AppText>
</Animated.View>
<Animated.Text entering={FadeInUp.delay(700).duration(700)} style={styles.subText} > {/* ── Step Completion Chips ── */}
Your submission was successful <Animated.View entering={FadeInDown.delay(600).springify()} style={[styles.stepsCard,{backgroundColor: colors.surface, shadowColor: colors.shadow}]}>
</Animated.Text> {STEPS.map((step, index) => (
<React.Fragment key={index}>
<View style={styles.stepRow}>
<View style={[styles.stepIconBox, { backgroundColor: `${colors.success}18` }]}>
<step.icon size={16} color={colors.success} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} semiBold style={styles.stepLabel as any}>
{step.label}
</AppText>
<CheckCircle2 size={18} color={colors.success} strokeWidth={2.5} />
</View>
{index < STEPS.length - 1 && (
<View style={[styles.stepConnector, { backgroundColor: `${colors.success}30` }]} />
)}
</React.Fragment>
))}
</Animated.View>
<Animated.View entering={FadeInUp.delay(900).duration(700)} style={styles.nextWrapper} > {/* ── Info badge ── */}
<TouchableOpacity activeOpacity={0.85} style={styles.nextBtn} onPress={onNext} > <Animated.View
<Text style={styles.nextText}>Next</Text> entering={FadeInUp.delay(900).springify()}
style={[styles.infoBadge, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}30` }]}
>
<AppText variant="caption" color={colors.primary} semiBold centered>
🎉 You are now eligible to participate in the bidding process
</AppText>
</Animated.View>
</View>
{/* ── Bottom CTA ── */}
<Animated.View entering={FadeInUp.delay(1100).springify()} style={styles.bottomArea}>
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.primary, ...shadows.medium }]}
onPress={onNext}
activeOpacity={0.85}
>
<AppText variant="button" color="#fff" bold style={styles.ctaText as any}>
Continue to Bidding
</AppText>
<ArrowRight size={18} color="#fff" strokeWidth={2.5} />
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
</View> </View>
); );
}; };
export default EmdComplete; export default EmdComplete;
/* ---------------- Styles ---------------- */ /* ─── Styles ─── */
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { root: {
flex: 1, flex: 1,
backgroundColor: '#F8FAFC', alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 40,
},
centerContent: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 24,
gap: 24,
width: '100%',
},
/* Lottie bubble */
lottieBubble: {
width: 160,
height: 160,
borderRadius: 80,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
lottie: {
confetti: { width: 130,
position: 'absolute', height: 130,
top: 0,
width: '100%',
height: '100%',
}, },
success: { /* Title */
width: 220, titleBlock: {
height: 220, alignItems: 'center',
marginTop: -100 gap: 8,
}, },
title: { title: {
marginTop: 16, letterSpacing: 0.3,
fontSize: 24, } as any,
fontWeight: '900', subtitle: {
color: '#0F172A', lineHeight: 22,
opacity: 0.75,
} as any,
/* Steps */
stepsCard: {
width: '100%',
borderRadius: 20,
paddingVertical: 16,
paddingHorizontal: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.06,
shadowRadius: 12,
elevation: 4,
},
stepRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
stepIconBox: {
width: 34,
height: 34,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
stepLabel: {
flex: 1,
} as any,
stepConnector: {
width: 2,
height: 14,
marginLeft: 16,
borderRadius: 1,
marginVertical: 2,
},
/* Info badge */
infoBadge: {
width: '100%',
borderRadius: 16,
borderWidth: 1.2,
paddingVertical: 12,
paddingHorizontal: 16,
},
infoText: {
lineHeight: 20,
} as any,
/* CTA */
bottomArea: {
width: '100%',
paddingHorizontal: 24,
},
ctaBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 16,
borderRadius: 18,
},
ctaText: {
letterSpacing: 0.5, letterSpacing: 0.5,
}, } as any,
subText: {
marginTop: 8,
fontSize: 14,
color: '#475569',
},
/* ---------- Floating Next Button ---------- */
nextWrapper: {
position: 'absolute',
right: 20,
bottom: 30,
},
nextBtn: {
backgroundColor: colors.primary,
paddingHorizontal: 26,
paddingVertical: 14,
borderRadius: 28,
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 14,
elevation: 6,
},
nextText: {
color: '#FFFFFF',
fontSize: 15,
fontWeight: '800',
letterSpacing: 0.4,
},
}); });
@@ -0,0 +1,108 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { LogOut, X } from 'lucide-react-native';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props {
visible: boolean;
onClose: () => void;
onConfirm: () => void;
}
const GoBackModal = ({ visible, onClose, onConfirm }: Props) => {
const { colors, shadows } = useTheme();
if (!visible) return null;
return (
<View style={styles.overlay}>
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose}>
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
</TouchableOpacity>
<View style={[styles.iconBox, { backgroundColor: `${colors.error}15` }]}>
<LogOut size={32} color={colors.error} strokeWidth={2} />
</View>
<AppText variant="h3" color={colors.text} bold centered style={styles.title as any}>
Leave Bidding?
</AppText>
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle as any}>
Are you sure you want to go back to the Dashboard? You can always return to this panel later.
</AppText>
<View style={styles.btnRow}>
<TouchableOpacity style={[styles.btn, styles.cancelBtn, { backgroundColor: colors.background }]} onPress={onClose}>
<AppText variant="button" color={colors.text} bold>Stay</AppText>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, styles.confirmBtn, { backgroundColor: colors.error }]} onPress={onConfirm}>
<AppText variant="button" color="#fff" bold>Leave</AppText>
</TouchableOpacity>
</View>
</View>
</View>
);
};
export default GoBackModal;
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
zIndex: 9999,
elevation: 9999,
},
modalCard: {
width: '100%',
borderRadius: 24,
padding: 24,
alignItems: 'center',
},
closeBtn: {
position: 'absolute',
top: 16,
right: 16,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
iconBox: {
width: 64,
height: 64,
borderRadius: 32,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
title: {
marginBottom: 8,
},
subtitle: {
marginBottom: 24,
paddingHorizontal: 10,
},
btnRow: {
flexDirection: 'row',
gap: 12,
width: '100%',
},
btn: {
flex: 1,
height: 50,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
cancelBtn: {},
confirmBtn: {},
});
@@ -1,9 +1,11 @@
import React, { memo, useCallback, useRef, useState } from 'react'; import React, { memo, useCallback, useRef, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput } from 'react-native'; import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput } from 'react-native';
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import colors from '../../../Constant/Color'; import colors_const from '../../../Constant/Color';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail'; import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle'; import MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import axios, { all } from 'axios'; import axios, { all } from 'axios';
import Variables from '../../../Constant/Variable'; import Variables from '../../../Constant/Variable';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@@ -11,6 +13,8 @@ import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
import { Loader } from '../../../Component/Index'; import { Loader } from '../../../Component/Index';
const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => { const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const Token = useSelector((state: any) => state.token.token) const Token = useSelector((state: any) => state.token.token)
const [allState, setAllState] = useState({ isLoading: false }) const [allState, setAllState] = useState({ isLoading: false })
const [showDetail, setShowDetail] = useState(false); const [showDetail, setShowDetail] = useState(false);
@@ -108,9 +112,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
entering={FadeInUp.delay(100)} entering={FadeInUp.delay(100)}
style={[ style={[
styles.card, styles.card,
item.me ? styles.leadingCard : undefined,
{ {
borderColor: item.me ? '#22C55E' : '#EF4444', borderColor: item.me ? colors.success : colors.error,
backgroundColor: item.me ? '#F0FDF4' : '#FDF0F0', backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10`,
}, },
]} ]}
> >
@@ -121,10 +126,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
style={styles.cardHeader} style={styles.cardHeader}
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.productName}>{item.productName}</Text> <AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
<Text style={styles.itemIdText}> <AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
Initial Amt : ₹{item.auctionStartValue} Initial Amt : ₹{item.auctionStartValue}
</Text> </AppText>
</View> </View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}> <TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
@@ -135,38 +140,40 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
item.me ? styles.meBadge : styles.liveBadge, item.me ? styles.meBadge : styles.liveBadge,
]} ]}
> >
<Text <AppText
variant="caption"
bold
style={[ style={[
styles.badgeText, styles.badgeText as any,
item.me ? styles.meText : styles.liveText, item.me ? styles.meText : styles.liveText,
]} ]}
> >
{item.me ? 'LEADING' : 'LIVE'} {item.me ? 'LEADING' : 'LIVE'}
</Text> </AppText>
</View> </View>
<Animated.Text style={[styles.arrow, arrowStyle]}> <Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
▼ ▼
</Animated.Text> </Animated.Text>
</View> </View>
<Text style={styles.itemIdText}> <AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
{SelectedAuction?.auctionNumber} {SelectedAuction?.auctionNumber}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
</TouchableOpacity> </TouchableOpacity>
{/* PRICE INFO */} {/* PRICE INFO */}
<View style={styles.priceRow}> <View style={styles.priceRow}>
<View> <View>
<Text style={styles.label}>Current Bid</Text> <AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
<Text style={styles.mainPrice}>₹{item.bidAmount || item.auctionStartValue}</Text> <AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item.bidAmount || item.auctionStartValue}</AppText>
</View> </View>
<View style={{ alignItems: 'flex-end' }}> <View style={{ alignItems: 'flex-end' }}>
<Text style={styles.label}>Decrement</Text> <AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Decrement</AppText>
<Text style={styles.incrementText}> <AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
-₹{item.emdAmount} -₹{item.emdAmount}
</Text> </AppText>
</View> </View>
</View> </View>
@@ -175,7 +182,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
<Animated.View style={[styles.collapsibleContainer, bodyStyle]}> <Animated.View style={[styles.collapsibleContainer, bodyStyle]}>
<View style={styles.divider} /> <View style={styles.divider} />
<Text style={styles.bidLabel}>Set Your Bid</Text> <AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
<View style={styles.stepperContainer}> <View style={styles.stepperContainer}>
{/* DECREASE */} {/* DECREASE */}
@@ -187,25 +194,25 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
!canDecrease && styles.disabledBtn, !canDecrease && styles.disabledBtn,
]} ]}
> >
<Text style={styles.stepText}>−</Text> <AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
</TouchableOpacity>} </TouchableOpacity>}
{/* VALUE */} {/* VALUE */}
{!allBoolean.oneTimeBid && <View style={styles.inputBox}> {!allBoolean.oneTimeBid && <View style={styles.inputBox}>
<Text style={styles.currency}>₹</Text> <AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
<Text style={styles.bidValueText}>{bidValue}</Text> <AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
</View>} </View>}
{ {
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}> (allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
<Text style={styles.currency2}>₹</Text> <AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
<TextInput <TextInput
value={value} value={value}
onChangeText={handleChange} onChangeText={handleChange}
keyboardType="decimal-pad" keyboardType="decimal-pad"
placeholder="0.00" placeholder="0.00"
style={styles.input} style={[styles.input, { color: colors.text }]}
placeholderTextColor="#94A3B8" placeholderTextColor={colors.textSecondary}
/> />
</View> </View>
} }
@@ -219,7 +226,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
!canIncrease && styles.disabledBtn, !canIncrease && styles.disabledBtn,
]} ]}
> >
<Text style={styles.stepText}>+</Text> <AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
</TouchableOpacity>} </TouchableOpacity>}
</View> </View>
@@ -228,7 +235,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid} onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
> >
{ {
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text> allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
} }
</TouchableOpacity> </TouchableOpacity>
@@ -265,50 +272,34 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
export default ReverseBiddingCard; export default ReverseBiddingCard;
const styles = StyleSheet.create({ const makeStyles = (colors: any) => StyleSheet.create({
inputBox2: { inputBox2: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#F1F5F9', backgroundColor: colors.background,
borderRadius: 12, borderRadius: 12,
height: 45, height: 45,
paddingHorizontal: 12, paddingHorizontal: 12,
width: '100%' width: '100%'
}, },
currency2: { currency2: {
fontSize: 16,
fontWeight: 'bold',
color: '#64748B',
marginRight: 4, marginRight: 4,
}, } as any,
input: { input: {
flex: 1, flex: 1,
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: '#0F172A',
width: '100%' width: '100%'
}, },
card: { card: {
backgroundColor: '#FFF',
borderRadius: 24, borderRadius: 24,
padding: 20, padding: 20,
marginBottom: 16, marginBottom: 16,
borderWidth: 1, borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden', overflow: 'hidden',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
}, },
leadingCard: { leadingCard: {
borderColor: '#22C55E',
borderWidth: 2, borderWidth: 2,
backgroundColor: '#F0FDF4',
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
@@ -316,27 +307,24 @@ const styles = StyleSheet.create({
alignItems: 'flex-start', alignItems: 'flex-start',
}, },
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 }, headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' }, productName: {} as any,
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 }, itemIdText: { marginTop: 2 } as any,
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' }, liveBadge: { backgroundColor: `${colors.error}20` },
meBadge: { backgroundColor: '#BBF7D0' }, meBadge: { backgroundColor: `${colors.success}20` },
badgeText: { fontSize: 10, fontWeight: '900' }, badgeText: { fontSize: 10 } as any,
liveText: { color: '#EF4444' }, liveText: { color: colors.error },
meText: { color: '#15803D' }, meText: { color: colors.success },
arrow: { fontSize: 10, color: '#94A3B8' }, arrow: { fontSize: 10 },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 }, priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { label: {
fontSize: 11,
color: '#94A3B8',
textTransform: 'uppercase', textTransform: 'uppercase',
fontWeight: '600', } as any,
}, mainPrice: {} as any,
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' }, incrementText: {} as any,
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' }, collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 }, divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 }, bidLabel: { marginBottom: 10 } as any,
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 }, stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { stepBtn: {
width: 45, width: 45,
@@ -346,19 +334,19 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
disabledBtn: { backgroundColor: '#CBD5E1' }, disabledBtn: { backgroundColor: colors.border },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' }, stepText: {} as any,
inputBox: { inputBox: {
flex: 1, flex: 1,
height: 45, height: 45,
backgroundColor: '#F1F5F9', backgroundColor: colors.background,
borderRadius: 12, borderRadius: 12,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 }, currency: { marginRight: 4 } as any,
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' }, bidValueText: {} as any,
submitBtn: { submitBtn: {
height: 50, height: 50,
backgroundColor: colors.primary, backgroundColor: colors.primary,
@@ -366,5 +354,5 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' }, submitBtnText: {} as any,
}); });
@@ -1,5 +1,5 @@
import React, { useState } from 'react' import React, { useState, useMemo } from 'react'
import { Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView } from 'react-native' import { Modal, View, StyleSheet, TextInput, TouchableOpacity, ScrollView, Platform } from 'react-native'
import { Formik } from 'formik' import { Formik } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { pick } from '@react-native-documents/picker' import { pick } from '@react-native-documents/picker'
@@ -7,16 +7,20 @@ import { useSelector } from 'react-redux'
import { apiCall } from '../../../Axios/Axios' import { apiCall } from '../../../Axios/Axios'
import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/ApiCall/ApiUrls' import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/ApiCall/ApiUrls'
import { Props } from '../Type/BidType' import { Props } from '../Type/BidType'
import colors from '../../../Constant/Color'
import { Loader } from '../../../Component/Index' import { Loader } from '../../../Component/Index'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import NavigationString from '../../../Constant/NavigationString' import NavigationString from '../../../Constant/NavigationString'
import { useTheme } from '../../../Hooks/useTheme'
import AppText from '../../../components/AppText'
import { Upload, FileText, X } from 'lucide-react-native'
const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }) => { const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }) => {
if (!template) return null if (!template) return null
const SelectedAuction = useSelector((state: any) => state.auction.auction) const SelectedAuction = useSelector((state: any) => state.auction.auction)
const Navigation: any = useNavigation() const Navigation: any = useNavigation()
const { colors, spacing, typography } = useTheme()
const styles = useMemo(() => makeStyles(colors, spacing, typography), [colors, spacing, typography])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const initialValues = template?.labels?.reduce((acc: any, label: any) => { const initialValues = template?.labels?.reduce((acc: any, label: any) => {
acc[label.labelName] = '' acc[label.labelName] = ''
@@ -65,10 +69,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
if (label.labelType === '3') { if (label.labelType === '3') {
return ( return (
<View key={label.labelName} style={styles.field}> <View key={label.labelName} style={styles.field}>
<Text style={styles.label}> <AppText variant="caption" color={colors.textSecondary} style={styles.label as any}>
{label.labelName} {label.labelName}
{label.mandatory && <Text style={styles.required}> *</Text>} {label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
</Text> </AppText>
<TouchableOpacity <TouchableOpacity
style={[ style={[
@@ -79,18 +83,25 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
pickFile(label.labelName, setFieldValue) pickFile(label.labelName, setFieldValue)
} }
> >
<Text {value?.name ? (
style={[ <View style={styles.fileSelectedRow}>
styles.fileText, <FileText size={14} color={colors.success} strokeWidth={2} />
value?.name && styles.fileSelected, <AppText variant="caption" color={colors.text} bold style={{ flex: 1 } as any} numberOfLines={1}>
]} {value.name}
> </AppText>
{value?.name || 'Upload PDF'} </View>
</Text> ) : (
<View style={styles.fileSelectedRow}>
<Upload size={14} color={colors.textSecondary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary}>
Upload PDF
</AppText>
</View>
)}
</TouchableOpacity> </TouchableOpacity>
{showError && ( {showError && (
<Text style={styles.errorText}>{error}</Text> <AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
)} )}
</View> </View>
) )
@@ -98,10 +109,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
return ( return (
<View key={label.labelName} style={styles.field}> <View key={label.labelName} style={styles.field}>
<Text style={styles.label}> <AppText variant="caption" color={colors.textSecondary} style={styles.label as any}>
{label.labelName} {label.labelName}
{label.mandatory && <Text style={styles.required}> *</Text>} {label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
</Text> </AppText>
<TextInput <TextInput
style={[ style={[
@@ -109,7 +120,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
showError && styles.errorBorder, showError && styles.errorBorder,
]} ]}
placeholder={`Enter ${label.labelName}`} placeholder={`Enter ${label.labelName}`}
placeholderTextColor="#9CA3AF" placeholderTextColor={colors.textSecondary}
value={value} value={value}
onChangeText={(text) => onChangeText={(text) =>
setFieldValue(label.labelName, text) setFieldValue(label.labelName, text)
@@ -122,7 +133,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
/> />
{showError && ( {showError && (
<Text style={styles.errorText}>{error}</Text> <AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
)} )}
</View> </View>
) )
@@ -210,26 +221,26 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
<View style={styles.header}> <View style={styles.header}>
<View style={styles.headerLeft}> <View style={styles.headerLeft}>
<Text style={styles.headerTitle}>Technical Documents</Text> <AppText variant="h4" color={colors.text}>Technical Documents</AppText>
<Text style={styles.headerSubtitle}> <AppText variant="caption" color={colors.textSecondary}>
Fill the form, Click save when you're done. Fill the form, Click save when you're done.
</Text> </AppText>
</View> </View>
<TouchableOpacity onPress={onClose} style={styles.closeBtn}> <TouchableOpacity onPress={onClose} style={styles.closeBtn}>
<Text style={styles.closeText}>✕</Text> <X size={20} color={colors.textSecondary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<ScrollView contentContainerStyle={styles.content}> <ScrollView contentContainerStyle={styles.content}>
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.templateHeading}>Template</Text> <AppText variant="h3" color={colors.text} centered style={{ marginBottom: 16 } as any}>Template</AppText>
<View style={styles.templateBox}> <View style={styles.templateBox}>
<Text style={styles.templateName}> <AppText variant="bodySmall" color={colors.text} bold style={{ marginBottom: 12 } as any}>
{template.templateName} {template.templateName}
</Text> </AppText>
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
@@ -264,12 +275,12 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
{loading ? ( {loading ? (
<Loader <Loader
size="small" size="small"
color={colors.white} color="#fff"
/> />
) : ( ) : (
<Text style={styles.submitText}> <AppText variant="button" color="#fff" bold style={styles.submitText as any}>
Submit Submit
</Text> </AppText>
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -287,10 +298,11 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
export default TemplateFormModal export default TemplateFormModal
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, typography: any) =>
StyleSheet.create({
screen: { screen: {
flex: 1, flex: 1,
backgroundColor: '#F9FAFB', backgroundColor: colors.background,
}, },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
@@ -298,118 +310,86 @@ const styles = StyleSheet.create({
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 14, paddingVertical: 14,
backgroundColor: '#FFFFFF', backgroundColor: colors.surface,
borderBottomWidth: 1, borderBottomWidth: 1,
borderColor: '#E5E7EB', borderColor: colors.border,
}, },
headerLeft: { headerLeft: {
flex: 1, flex: 1,
}, },
headerTitle: {
fontSize: 16,
fontWeight: '700',
color: '#111827',
},
headerSubtitle: {
fontSize: 12,
color: '#6B7280',
marginTop: 2,
},
closeBtn: { closeBtn: {
padding: 6, padding: 8,
}, backgroundColor: colors.overlay,
closeText: { borderRadius: 10,
fontSize: 18,
color: '#6B7280',
}, },
content: { content: {
paddingHorizontal: 16, paddingHorizontal: 16,
}, },
card: { card: {
backgroundColor: '#FFFFFF', backgroundColor: colors.surface,
borderColor: '#E5E7EB', borderRadius: 18,
paddingVertical: 20 marginTop: spacing.m,
padding: spacing.m,
...Platform.select({
ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 12 },
android: { elevation: 3 },
}),
}, },
templateHeading: {
fontSize: 18,
fontWeight: '700',
textAlign: 'center',
marginBottom: 16,
color: '#111827',
},
templateBox: { templateBox: {
borderWidth: 1, borderWidth: 1,
borderColor: '#E5E7EB', borderColor: colors.border,
borderRadius: 10, borderRadius: 14,
padding: 16, padding: 16,
}, },
templateName: {
fontSize: 14,
fontWeight: '700',
color: '#111827',
marginBottom: 12,
},
field: { field: {
marginBottom: 14, marginBottom: 14,
}, },
label: { label: {
fontSize: 13, letterSpacing: 0.5,
fontWeight: '600', textTransform: 'uppercase',
color: '#374151',
marginBottom: 6, marginBottom: 6,
}, } as any,
required: {
color: '#EF4444',
},
input: { input: {
backgroundColor: '#FFFFFF', backgroundColor: colors.background,
borderWidth: 1, borderWidth: 1.2,
borderColor: '#E5E7EB', borderColor: colors.border,
borderRadius: 8, borderRadius: 12,
padding: 12, padding: 12,
fontSize: 14, fontSize: 14,
color: '#111827', color: colors.text,
}, },
filePicker: { filePicker: {
borderWidth: 1, borderWidth: 1.2,
borderColor: '#E5E7EB', borderColor: colors.border,
borderRadius: 8, backgroundColor: colors.background,
borderRadius: 12,
padding: 12, padding: 12,
}, },
fileText: { fileSelectedRow: {
color: '#6B7280', flexDirection: 'row',
fontSize: 14, alignItems: 'center',
}, gap: 8,
fileSelected: {
color: '#111827',
fontWeight: '600',
}, },
errorText: { errorText: {
color: '#EF4444',
fontSize: 12,
marginTop: 4, marginTop: 4,
}, } as any,
errorBorder: { errorBorder: {
borderColor: '#EF4444', borderColor: colors.error,
}, },
submitRow: { submitRow: {
marginTop: 20, marginTop: 20,
alignItems: 'flex-end', alignItems: 'flex-end',
height: 50 height: 50,
}, },
submitBtn: { submitBtn: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
paddingVertical: 10, paddingVertical: 12,
paddingHorizontal: 22, paddingHorizontal: 24,
borderRadius: 8, borderRadius: 14,
minWidth: 120, minWidth: 120,
alignItems: 'center', alignItems: 'center',
}, },
submitText: { submitText: {
color: '#FFFFFF', letterSpacing: 0.5,
fontWeight: '700', } as any,
fontSize: 14,
},
}) })
File diff suppressed because it is too large Load Diff
+233 -242
View File
@@ -1,52 +1,74 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { View, FlatList, StyleSheet, Platform, Alert, BackHandler, Text } from 'react-native'; import {
View,
FlatList,
StyleSheet,
Alert,
BackHandler,
} from 'react-native';
import LottieView from 'lottie-react-native'; import LottieView from 'lottie-react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { InsideHeader } from '../../Component/Index'; import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useWebSocketService } from '../../socket/Socket'; import { useWebSocketService } from '../../socket/Socket';
import Variables from '../../Constant/Variable'; import Variables from '../../Constant/Variable';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { apiCall } from '../../Axios/Axios'; import { apiCall } from '../../Axios/Axios';
import { AUCTION_ONE_TIME_BID, FORWARD_AUCTION_ITEM, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECTED_AUCTION_ITEM } from '../../Services/ApiCall/ApiUrls'; import {
AUCTION_ONE_TIME_BID,
FORWARD_AUCTION_ITEM,
GET_AUCTION_DETAILS,
REVERSE_AUCTION_ITEM,
SELECTED_AUCTION_ITEM,
} from '../../Services/ApiCall/ApiUrls';
import BiddingCard from './Component/BiddingCard'; import BiddingCard from './Component/BiddingCard';
import ConfirmBidModal from './Component/ConfirmBidModal';
import GoBackModal from './Component/GoBackModal';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native'; import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import CountdownTimer from '../../Component/Counter/CountdownTimer'; import CountdownTimer from '../../Component/Counter/CountdownTimer';
import FontsFamily from '../../Constant/FontsFamily'; import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Gavel, Building2, Clock4 } from 'lucide-react-native';
const StartBidding = () => { const StartBidding = () => {
const { colors, spacing, shadows } = useTheme();
const SelectedAuction = useSelector((state: any) => state.auction.auction); const SelectedAuction = useSelector((state: any) => state.auction.auction);
const loginUser = useSelector((state: any) => state.token.token) const loginUser = useSelector((state: any) => state.token.token);
const Navigation: any = useNavigation() const Navigation: any = useNavigation();
const [items, setItems] = useState<any>([]); const [items, setItems] = useState<any>([]);
const [bidValues, setBidValues] = useState<Record<number, number>>({}); const [bidValues, setBidValues] = useState<Record<number, number>>({});
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, bidPayload: {}, oneTimeBid: false, rebid: false, reload: false }) const [allBoolean, setAllBoolean] = useState({
showSuccess: false,
showDialog: false,
showBackDialog: false,
bidPayload: {} as any,
oneTimeBid: false,
rebid: false,
reload: false,
});
const { subscribe, createRoom, placeBid } = useWebSocketService( const { subscribe, createRoom, placeBid } = useWebSocketService(
Variables.webSocketUrl, Variables.webSocketUrl,
() => console.log('Connected!'), () => console.log('Connected!'),
(error) => console.log('WebSocket Error:', error), (error: any) => console.log('WebSocket Error:', error),
loginUser.token, loginUser.token,
); );
const getAuction = async () => { const getAuction = async () => {
const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId }); const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId });
if (selectedRes.statusCode == 200 && selectedRes.data.contractorId.length) { if (selectedRes.statusCode === 200 && selectedRes.data.contractorId.length) {
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true })) setAllBoolean(prev => ({ ...prev, oneTimeBid: true }));
}
} }
};
const getOneTimeBidValue = async () => { const getOneTimeBidValue = async () => {
try { try {
const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId }); const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId });
if (selectedRes.statusCode == 200) { return selectedRes.statusCode === 200 ? selectedRes.data : [];
return selectedRes.data } catch { return []; }
} else { };
return []
}
} catch (error) {
}
}
const getAllAcutionItems = async () => { const getAllAcutionItems = async () => {
try { try {
const payload = { auctionNumber: SelectedAuction.auctionNumber }; const payload = { auctionNumber: SelectedAuction.auctionNumber };
@@ -58,87 +80,40 @@ const StartBidding = () => {
} }
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload); const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
const uniqueArray: number[] = Array.from(new Set(selectedRes?.data || []));
const filteredItems = auctionItem.filter(item => uniqueArray.includes(item.id));
const uniqueArray: number[] = Array.from( if (SelectedAuction.auctionCategories === 'Live') {
new Set(selectedRes?.data || [])
);
const filteredItems = auctionItem.filter(item =>
uniqueArray.includes(item.id)
);
if (SelectedAuction.auctionCategories == 'Live') {
const result = await createRoom(SelectedAuction.auctionNumber); const result = await createRoom(SelectedAuction.auctionNumber);
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data: any) => {
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, data => {
console.log('🔥 BID UPDATE:', data); console.log('🔥 BID UPDATE:', data);
}); });
const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>(); const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>();
(result || []).forEach((r: any) => { (result || []).forEach((r: any) => {
bidAmountMap2.set(r.auctionItemId, { bidAmountMap2.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId });
bidAmount: r.bidAmount,
contractorId: r.contractorId,
}); });
});
// Check One Time bid enable butoon
if (SelectedAuction.auctionCategories != 'Live') {
const myBid = result.find(
(bid: any) => bid.contractorId === loginUser.contractorId
);
if (myBid) {
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
}
}
const bidAmountMap = new Map<number, number>();
(result || []).forEach((r: any) => {
bidAmountMap.set(r.auctionItemId, r.bidAmount);
});
const updatedFilteredItems = filteredItems.map(item => { const updatedFilteredItems = filteredItems.map(item => {
const bidData = bidAmountMap2.get(item.id); const bidData = bidAmountMap2.get(item.id);
return { return {
...item, ...item,
bidAmount: bidData?.bidAmount ?? item.auctionStartValueFigure, bidAmount: bidData?.bidAmount ?? item.auctionStartValueFigure,
bidAvilable: bidData?.bidAmount ?? 0, bidAvilable: bidData?.bidAmount ?? 0,
me: bidData?.contractorId == loginUser.contractorId ? true : false me: bidData?.contractorId === loginUser.contractorId,
}; };
}); });
const initialBids: Record<number, number> = {}; const initialBids: Record<number, number> = {};
updatedFilteredItems.forEach(item => { updatedFilteredItems.forEach(item => { initialBids[item.id] = item.bidAmount; });
initialBids[item.id] = item.bidAmount;
});
setBidValues(initialBids); setBidValues(initialBids);
setItems(updatedFilteredItems); setItems(updatedFilteredItems);
} else { } else {
let result = [] if (SelectedAuction.rebidAllowed) setAllBoolean(prev => ({ ...prev, rebid: true }));
if (SelectedAuction.rebidAllowed) { let result: any[] = [];
setAllBoolean((prev: any) => ({ ...prev, rebid: true })) if (SelectedAuction.auctionCategories !== 'Live') result = await getOneTimeBidValue();
}
if (SelectedAuction.auctionCategories != 'Live') {
result = await getOneTimeBidValue()
}
const bidMap = new Map<number, any>(); const bidMap = new Map<number, any>();
result.forEach((bid: any) => bidMap.set(bid.auctionItemId, bid));
result.forEach((bid: any) => {
bidMap.set(bid.auctionItemId, bid);
});
const mergedData = auctionItem.map((item: any) => { const mergedData = auctionItem.map((item: any) => {
const bid = bidMap.get(item.id); const bid = bidMap.get(item.id);
if (!bid) return { ...item, bidAmount: item.auctionStartValueFigure, contractorId: item.contractorId };
if (!bid) {
return {
...item,
bidAmount: item.auctionStartValueFigure,
contractorId: item.contractorId,
};
}
return { return {
...item, ...item,
bidAmount: bid.bidAmount, bidAmount: bid.bidAmount,
@@ -150,56 +125,36 @@ const StartBidding = () => {
}; };
}); });
const initialBids: Record<number, number> = {}; const initialBids: Record<number, number> = {};
mergedData.forEach(item => { mergedData.forEach(item => { initialBids[item.id] = item.bidAmount; });
initialBids[item.id] = item.bidAmount;
});
setBidValues(initialBids);
setItems(mergedData)
setBidValues(initialBids); setBidValues(initialBids);
setItems(mergedData);
} }
} catch (error) { } catch (error) {
console.error('Error fetching auction items:', error); console.error('Error fetching auction items:', error);
} }
}; };
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message) => { subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message: any) => {
const result = message[message.length - 1];
const result = message[message.length - 1] setItems((prev: any[]) =>
const updatedBidItem = (prev: any[]) =>
prev.map(item => prev.map(item =>
item.id === result.auctionItemId item.id === result.auctionItemId
? { ...item, bidAmount: result.bidAmount, me: result.contractorId == loginUser.contractorId ? true : false } ? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
: item : item,
),
); );
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
setItems(updatedBidItem);
setBidValues(prev => ({
...prev,
[result.auctionItemId]: result.bidAmount,
}));
try {
} catch (err) {
console.log('JSON parse error:', err);
}
}); });
useEffect(() => { useEffect(() => {
if (SelectedAuction.auctionCategories == 'One-time') { if (SelectedAuction.auctionCategories === 'One-time') getAuction();
getAuction()
}
getAllAcutionItems(); getAllAcutionItems();
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data) => { }); subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, () => {});
}, [allBoolean.reload]); }, [allBoolean.reload]);
useEffect(() => { useEffect(() => {
const initial: Record<number, number> = {}; const initial: Record<number, number> = {};
items.forEach((item: any) => { items.forEach((item: any) => { initial[item.id] = item.bidAmount + item.bidVariationValue; });
initial[item.id] = item.bidAmount + item.bidVariationValue;
});
setBidValues(initial); setBidValues(initial);
}, []); }, []);
@@ -216,51 +171,14 @@ const StartBidding = () => {
fullName: loginUser.fullName, fullName: loginUser.fullName,
roomId: SelectedAuction.auctionNumber, roomId: SelectedAuction.auctionNumber,
}; };
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, })); setAllBoolean(prev => ({ ...prev, bidPayload: payload, showDialog: true }));
Alert.alert(
'Confirm Bid',
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Submit',
onPress: () => {
placeBidSubmit(payload);
},
},
],
{ cancelable: true }
);
}; };
const placeBidSubmit = async (data: any) => { const placeBidSubmit = async (data: any) => {
setAllBoolean(prev => ({ ...prev, showDialog: false }));
try { try {
const result: any = await placeBid(data); const result: any = await placeBid(data);
if (result?.status && result.data) getAllAcutionItems();
if (result?.status && result.data) {
const updatedBidItem = (prev: any[]) =>
prev.map(item =>
item.id === result.data.auctionItemId
? { ...item, bidAmount: result.data.bidAmount, me: result.data?.contractorId == loginUser.contractorId ? true : false }
: item
);
// setItems(updatedBidItem);
// setBidValues((prev: any) => ({
// ...prev,
// [result.data.auctionItemId]: {
// ...prev[result.data.auctionItemId],
// bidAmount: result.data.bidAmount,
// },
// }));
getAllAcutionItems()
}
} catch (error) { } catch (error) {
console.error('Error placing bid:', error); console.error('Error placing bid:', error);
} }
@@ -269,55 +187,75 @@ const StartBidding = () => {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
const onBackPress = () => { const onBackPress = () => {
Alert.alert( setAllBoolean(prev => ({ ...prev, showBackDialog: true }));
'Confirm', return true;
'Do you want to go back to Dashboard?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Yes',
onPress: () => {
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
},
},
],
{ cancelable: true }
);
return true; // block default back action
}; };
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
const subscription = BackHandler.addEventListener( return () => sub.remove();
'hardwareBackPress', }, [Navigation]),
onBackPress
); );
return () => subscription.remove(); const styles = makeStyles(colors, spacing, shadows);
}, [Navigation])
); const biddingEndDate =
SelectedAuction?.auctionStatus === 'Amended'
? SelectedAuction?.amendedkeyDates?.auctionBidding?.endDateTime
: SelectedAuction?.keyDates?.auctionBidding?.endDateTime;
return ( return (
<View style={styles.container}> <View style={styles.root}>
<InsideHeader title="Bidding Panel" showArrow /> <InsideHeader title="Bidding Panel" showArrow />
<View style={styles.container2}>
<Text style={styles.title}>
{SelectedAuction.deptName}
</Text>
<View style={styles.timerWrapper}> {/* ── Top Info Band ── */}
<Animated.View entering={FadeInDown.springify()} style={styles.infoBand}>
{/* Department row */}
<View style={styles.deptRow}>
<View style={[styles.deptIcon, { backgroundColor: `${colors.primary}15` }]}>
<Building2 size={16} color={colors.primary} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} bold numberOfLines={1} style={styles.deptName as any}>
{SelectedAuction.deptName ?? 'Auction Department'}
</AppText>
</View>
{/* Timer card */}
<View style={[styles.timerCard, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}25` }]}>
<View style={styles.timerLabelRow}>
<Clock4 size={13} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} bold style={styles.timerLabel as any}>
BIDDING ENDS IN
</AppText>
</View>
<CountdownTimer <CountdownTimer
endDate={SelectedAuction.keyDates.auctionBidding.endDateTime} endDate={biddingEndDate}
myStyle={{ fontSize: 30 }} myStyle={{ fontSize: 28, fontWeight: '900', color: colors.primary, letterSpacing: 1 }}
size={30} size={28}
/> />
</View> </View>
</View>
{/* Auction chip */}
<View style={styles.chipRow}>
<View style={[styles.chip, { backgroundColor: `${colors.primary}12` }]}>
<Gavel size={12} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} bold style={styles.chipText as any}>
{SelectedAuction.auctionNumber}
</AppText>
</View>
<View style={[styles.chip, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? `${colors.success}15` : `${colors.textSecondary}12` }]}>
<View style={[styles.liveDot, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary }]} />
<AppText
variant="caption"
color={SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary}
bold
style={styles.chipText as any}
>
{SelectedAuction.auctionCategories ?? 'Auction'}
</AppText>
</View>
</View>
</Animated.View>
{/* ── Items List ── */}
<FlatList <FlatList
data={items} data={items}
keyExtractor={item => item.id.toString()} keyExtractor={item => item.id.toString()}
@@ -327,15 +265,17 @@ const StartBidding = () => {
bidValue={bidValues[item.id] || 0} bidValue={bidValues[item.id] || 0}
onIncrease={() => handleUpdate(item.id, item.bidVariationValue)} onIncrease={() => handleUpdate(item.id, item.bidVariationValue)}
onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)} onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)}
onPlaceBid={() => { submitBid(item) }} onPlaceBid={() => submitBid(item)}
SelectedAuction={SelectedAuction} SelectedAuction={SelectedAuction}
allBoolean={allBoolean} allBoolean={allBoolean}
setAllBoolean={setAllBoolean} setAllBoolean={setAllBoolean}
/> />
)} )}
contentContainerStyle={{ padding: 16, paddingBottom: 300 }} contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/> />
{/* ── Success overlay ── */}
{allBoolean.showSuccess && ( {allBoolean.showSuccess && (
<View style={styles.overlay}> <View style={styles.overlay}>
<LottieView <LottieView
@@ -346,66 +286,117 @@ const StartBidding = () => {
/> />
</View> </View>
)} )}
<ConfirmBidModal
visible={allBoolean.showDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
bidAmount={allBoolean.bidPayload?.bidAmount}
/>
<GoBackModal
visible={allBoolean.showBackDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
onConfirm={() => {
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
}}
/>
</View> </View>
); );
}; };
export default StartBidding; export default StartBidding;
const styles = StyleSheet.create({ /* ─── Styles factory ─── */
container: { flex: 1, backgroundColor: colors.white }, const makeStyles = (colors: any, spacing: any, shadows: any) =>
container2: { StyleSheet.create({
paddingHorizontal: 16, root: {
paddingTop: 12, flex: 1,
paddingBottom: 8, backgroundColor: colors.background,
}, },
title: {
fontSize: 18, /* Info band */
fontFamily: FontsFamily.poppinsSemiBold, infoBand: {
color: colors.black, backgroundColor: colors.surface,
marginBottom: 6, paddingHorizontal: spacing.m,
// textAlign:'center' paddingVertical: spacing.m,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
...shadows.light,
}, },
timerWrapper: { deptRow: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: spacing.s,
}, },
card: { deptIcon: {
backgroundColor: '#FFF', width: 30,
borderRadius: 24, height: 30,
padding: 20, borderRadius: 9,
marginBottom: 16, alignItems: 'center',
borderWidth: 1, justifyContent: 'center',
borderColor: '#E2E8F0', },
overflow: 'hidden', deptName: {
...Platform.select({ ios: { shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, android: { elevation: 2 } }), flex: 1,
} as any,
/* Timer */
timerCard: {
borderRadius: 16,
borderWidth: 1.2,
paddingVertical: spacing.m,
paddingHorizontal: spacing.m,
alignItems: 'center',
gap: 4,
},
timerLabelRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
timerLabel: {
letterSpacing: 1.2,
} as any,
/* Chips */
chipRow: {
flexDirection: 'row',
gap: spacing.s,
},
chip: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: spacing.s + 4,
paddingVertical: 5,
borderRadius: 20,
},
chipText: {
letterSpacing: 0.3,
} as any,
liveDot: {
width: 6,
height: 6,
borderRadius: 3,
},
listContent: {
padding: spacing.m,
paddingBottom: 120,
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: colors.overlay,
justifyContent: 'center',
alignItems: 'center',
zIndex: 99,
}, },
leadingCard: { borderColor: '#22C55E', borderWidth: 2, backgroundColor: '#F0FDF4' },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { fontSize: 11, color: '#94A3B8', textTransform: 'uppercase', fontWeight: '600' },
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { width: 45, height: 45, backgroundColor: colors.primary, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
inputBox: { flex: 1, height: 45, backgroundColor: '#F1F5F9', borderRadius: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
submitBtn: { height: 50, backgroundColor: colors.primary, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(255,255,255,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 99 },
}); });
+37 -88
View File
@@ -11,16 +11,21 @@ import { AUCTION_ONE_TIME_BID, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECT
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native'; import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import CountdownTimer from '../../Component/Counter/CountdownTimer'; import CountdownTimer from '../../Component/Counter/CountdownTimer';
import FontsFamily from '../../Constant/FontsFamily'; import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import ReverseBiddingCard from './Component/ReverseBiddingCard'; import ReverseBiddingCard from './Component/ReverseBiddingCard';
import ConfirmBidModal from './Component/ConfirmBidModal';
import GoBackModal from './Component/GoBackModal';
const StartReverseBidding = () => { const StartReverseBidding = () => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const SelectedAuction = useSelector((state: any) => state.auction.auction); const SelectedAuction = useSelector((state: any) => state.auction.auction);
const loginUser = useSelector((state: any) => state.token.token) const loginUser = useSelector((state: any) => state.token.token)
const Navigation: any = useNavigation() const Navigation: any = useNavigation()
const [items, setItems] = useState<any>([]); const [items, setItems] = useState<any>([]);
const [bidValues, setBidValues] = useState<Record<number, number>>({}); const [bidValues, setBidValues] = useState<Record<number, number>>({});
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, bidPayload: {}, oneTimeBid: false, rebid: false, reload: false }) const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, showBackDialog: false, bidPayload: {} as any, oneTimeBid: false, rebid: false, reload: false })
const { subscribe, createRoom, placeBid } = useWebSocketService( const { subscribe, createRoom, placeBid } = useWebSocketService(
Variables.webSocketUrl, Variables.webSocketUrl,
() => console.log('Connected!'), () => console.log('Connected!'),
@@ -216,27 +221,11 @@ const StartReverseBidding = () => {
fullName: loginUser.fullName, fullName: loginUser.fullName,
roomId: SelectedAuction.auctionNumber, roomId: SelectedAuction.auctionNumber,
}; };
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, })); setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, showDialog: true }));
Alert.alert(
'Confirm Bid',
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Submit',
onPress: () => {
placeBidSubmit(payload);
},
},
],
{ cancelable: true }
);
}; };
const placeBidSubmit = async (data: any) => { const placeBidSubmit = async (data: any) => {
setAllBoolean((prev: any) => ({ ...prev, showDialog: false }));
try { try {
const result: any = await placeBid(data); const result: any = await placeBid(data);
@@ -258,26 +247,7 @@ const StartReverseBidding = () => {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
const onBackPress = () => { const onBackPress = () => {
Alert.alert( setAllBoolean((prev: any) => ({ ...prev, showBackDialog: true }));
'Confirm',
'Do you want to go back to Dashboard?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Yes',
onPress: () => {
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
},
},
],
{ cancelable: true }
);
return true; // block default back action return true; // block default back action
}; };
@@ -294,9 +264,9 @@ const StartReverseBidding = () => {
<View style={styles.container}> <View style={styles.container}>
<InsideHeader title="Bidding Panel" showArrow /> <InsideHeader title="Bidding Panel" showArrow />
<View style={styles.container2}> <View style={styles.container2}>
<Text style={styles.title}> <AppText variant="h3" color={colors.text} bold style={styles.title as any}>
{SelectedAuction.deptName} {SelectedAuction.deptName}
</Text> </AppText>
<View style={styles.timerWrapper}> <View style={styles.timerWrapper}>
<CountdownTimer <CountdownTimer
@@ -335,66 +305,45 @@ const StartReverseBidding = () => {
/> />
</View> </View>
)} )}
<ConfirmBidModal
visible={allBoolean.showDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
bidAmount={allBoolean.bidPayload?.bidAmount}
/>
<GoBackModal
visible={allBoolean.showBackDialog}
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
onConfirm={() => {
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
Navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: NavigationString.Home }],
})
);
}}
/>
</View> </View>
); );
}; };
export default StartReverseBidding; export default StartReverseBidding;
const styles = StyleSheet.create({ const makeStyles = (colors: any) => StyleSheet.create({
container: { flex: 1, backgroundColor: colors.white }, container: { flex: 1, backgroundColor: colors.background },
container2: { container2: {
paddingHorizontal: 16, paddingHorizontal: 16,
paddingTop: 12, paddingTop: 12,
paddingBottom: 8, paddingBottom: 8,
}, },
title: { title: {
fontSize: 18,
fontFamily: FontsFamily.poppinsSemiBold,
color: colors.black,
marginBottom: 6, marginBottom: 6,
// textAlign:'center' } as any,
},
timerWrapper: { timerWrapper: {
alignItems: 'center', alignItems: 'center',
}, },
card: { overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.overlay, justifyContent: 'center', alignItems: 'center', zIndex: 99 },
backgroundColor: '#FFF',
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: '#E2E8F0',
overflow: 'hidden',
...Platform.select({ ios: { shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, android: { elevation: 2 } }),
},
leadingCard: { borderColor: '#22C55E', borderWidth: 2, backgroundColor: '#F0FDF4' },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
liveBadge: { backgroundColor: '#FEE2E2' },
meBadge: { backgroundColor: '#BBF7D0' },
badgeText: { fontSize: 10, fontWeight: '900' },
liveText: { color: '#EF4444' },
meText: { color: '#15803D' },
arrow: { fontSize: 10, color: '#94A3B8' },
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
label: { fontSize: 11, color: '#94A3B8', textTransform: 'uppercase', fontWeight: '600' },
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
collapsibleContainer: { overflow: 'hidden' },
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
stepBtn: { width: 45, height: 45, backgroundColor: colors.primary, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
disabledBtn: { backgroundColor: '#CBD5E1' },
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
inputBox: { flex: 1, height: 45, backgroundColor: '#F1F5F9', borderRadius: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
submitBtn: { height: 50, backgroundColor: colors.primary, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(255,255,255,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 99 },
}); });
+142 -259
View File
@@ -1,226 +1,134 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { View, Text, StyleSheet, Dimensions, FlatList, ActivityIndicator, Alert } from 'react-native'; import { View, StyleSheet, Dimensions, FlatList, ActivityIndicator, Alert, ScrollView, ImageBackground, TouchableOpacity, Platform } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withDelay, withTiming } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeInUp } from 'react-native-reanimated';
import { Layers, Clock, ShoppingCart, X, FileText } from 'lucide-react-native'; import { Layers, Clock, ShoppingCart, X, FileText, ArrowUpRight, Menu, Radio } from 'lucide-react-native';
import { Header } from '../../Component/Index'; import { useTheme } from '../../Hooks/useTheme';
import colors from '../../Constant/Color';
import { useDashboard, useESetuAdharVarification, useTenderDashboard } from '../../Services/ApiCall/Hooks'; import { useDashboard, useESetuAdharVarification, useTenderDashboard } from '../../Services/ApiCall/Hooks';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog'; import { getToken } from '../../Redux/Slices/Token';
import { generateTransactionId } from '../../Helper/uniqueId';
import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import ImagePath from '../../Constant/ImagePath';
import AppText from '../../components/AppText';
import SidebarModal from '../../components/SidebarModal';
// Header is now integrated or minimal
// import { Header } from '../../Component/Index';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const CARD_WIDTH = (width - 48) / 2;
const StatCard = ({ item, index, isLoading }: any) => {
const translateY = useSharedValue(30);
const opacity = useSharedValue(0);
useEffect(() => {
translateY.value = withDelay(index * 80, withTiming(0, { duration: 400 }));
opacity.value = withDelay(index * 80, withTiming(1, { duration: 400 }));
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
opacity: opacity.value,
}));
const Icon = item.icon;
return (
<Animated.View style={[styles.card, animatedStyle]}>
<View style={styles.header}>
<Text style={styles.title} numberOfLines={2}>
{item.title}
</Text>
<View style={[styles.badge, { borderColor: item.color }]}>
<Icon size={14} color={item.color} />
<Text style={[styles.badgeText, { color: item.color }]}>
{item.value}
</Text>
</View>
</View>
{isLoading ? (
<ActivityIndicator
size="small"
color={item.color}
style={{ marginTop: 12 }}
/>
) : (
<Text style={styles.value}>{item.value}</Text>
)}
<Text style={styles.desc}>{item.desc}</Text>
</Animated.View>
);
};
const Home = () => { const Home = () => {
const { data, isLoading } = useDashboard(); const { colors, spacing, typography, shadows } = useTheme();
const { data: tenderData, isLoading: tenderLoading } = useTenderDashboard();
const [stats, setStats] = useState<any>({});
const [isMenuVisible, setIsMenuVisible] = useState(false);
const navigation = useNavigation<any>();
const userData = useSelector(getToken);
const userType = userData?.userType; // 'tender' | 'auction'
// Hooks for data
const { data: dashboardData, refetch: refetchDashboard, isPending: isLoadingDashboard } = useDashboard();
const { data: tenderData, refetch: refetchTender, isPending: isLoadingTender } = useTenderDashboard();
const { mutate: verifyAadhaar } = useESetuAdharVarification();
const Navigation: any = useNavigation();
const user = useSelector((state: any) => state.token?.token);
const { mutateAsync: eSetuAdharVarification, isPending } = useESetuAdharVarification();
const dashboard = data?.data || {};
const [showDialog, setShowDialog] = useState<boolean>(false);
const [isNavigating, setIsNavigating] = useState<boolean>(false);
const [DASHBOARD_CARDS,setDASHBOARD_CARDS]=useState<any>([])
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
setIsNavigating(false); if (userType === 'auction') refetchDashboard();
else refetchTender();
return () => { }, [userType])
// cleanup if needed
};
}, [])
); );
useEffect(() => {
if (user?.esetuVerified === 'Not Verified') {
setShowDialog(true);
}
}, []);
const handleProceedVerification = async () => {
try {
setIsNavigating(true);
const payload = {
contId: user.id,
transactionId: generateTransactionId(),
udf1: "23.77",
udf2: "77.34",
udf3: "string",
udf4: "string",
};
const res = await eSetuAdharVarification(payload);
setShowDialog(false);
Navigation.navigate(NavigationString.KycWebViewScreen, { res });
} catch (err) {
Alert.alert('Error', 'Failed to initiate verification. Please try again.');
setIsNavigating(false);
}
};
useEffect(() => { useEffect(() => {
if (user?.userType === "auction") { if (userType === 'auction' && dashboardData) {
setDASHBOARD_CARDS([ console.log(dashboardData.data, "lkkllklklklklklklklklk")
{ setStats(dashboardData.data || {});
title: 'Total Auctions', } else if (userType === 'tender' && tenderData) {
value: dashboard?.myAuctionCount ?? 0, console.log(tenderData.data, "bonds")
desc: 'Auctions purchased by you', setStats(tenderData.data || {});
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',
},
]);
} else {
setDASHBOARD_CARDS([
{
title: 'Total Tenders',
value: tenderData?.data?.[0]?.totalTender ?? 0,
desc: 'Tenders assigned',
icon: FileText,
color: '#16A34A',
},
{
title: 'Live Tenders',
value: tenderData?.data?.[0]?.liveTender ?? 0,
desc: 'Currently live',
icon: Clock,
color: '#059669',
},
{
title: 'Participated Tenders',
value: tenderData?.data?.[0]?.totalParticipate ?? 0,
desc: 'Participated in tender',
icon: ShoppingCart,
color: '#0EA5E9',
},
{
title: 'Cancelled Tenders',
value: tenderData?.data?.[0]?.cancelledTender ?? 0,
desc: 'Cancelled by authority',
icon: X,
color: '#EF4444',
},
]);
} }
},[tenderData,dashboard]) }, [dashboardData, tenderData, userType]);
return (
<View style={styles.screen}>
<Header title="Dashboard" />
<ConfirmDialog const renderGlassCard = (title: string, value: string | number, icon: React.ReactNode, index: number) => (
visible={showDialog} <Animated.View
title="Aadhaar Verification Required" entering={FadeInUp.duration(600).delay(index * 100)}
message="To access features, please complete your Aadhaar verification." style={[styles.glassCard, {
confirmText={isPending || isNavigating ? "Processing..." : "Proceed"} backgroundColor: colors.glass,
cancelText="Cancel" borderColor: colors.overlay,
onConfirm={handleProceedVerification} }]}
onClose={() => !isPending && !isNavigating && setShowDialog(false)} >
loading={isPending || isNavigating} <View style={styles.cardHeader}>
/> <View style={[styles.iconContainer, { backgroundColor: colors.overlay }]}>
{icon}
{isNavigating && (
<View style={styles.navigationOverlay}>
<ActivityIndicator size="large" color={colors.primary || '#2563EB'} />
<Text style={styles.loadingText}>Loading verification...</Text>
</View> </View>
<ArrowUpRight size={20} color={colors.textSecondary} />
</View>
<AppText variant="h2" color={colors.primary}>{value || '0'}</AppText>
<AppText variant="caption" color={colors.textSecondary}>{title}</AppText>
</Animated.View>
);
const StatGrid = () => (
<View style={styles.gridContainer}>
{userType === 'auction' ? (
<>
{renderGlassCard('Live Auctions', stats?.liveAuctionCount, <Radio size={24} color={colors.primary} />, 0)}
{renderGlassCard('My Purchased Auctions', stats?.myAuctionCount, <ShoppingCart size={24} color={colors.primary} />, 1)}
{renderGlassCard('Cancelled Auctions', stats?.cancelledAuctionCount, <X size={24} color={colors.primary} />, 2)}
{renderGlassCard('Participated Auctions', stats?.currentBiddingCount, <FileText size={24} color={colors.primary} />, 3)}
</>
) : (
<>
{renderGlassCard('Total Purchased', stats[0]?.totalPurchased, <ShoppingCart size={24} color={colors.primary} />, 0)}
{renderGlassCard('Participated', stats[0]?.totalParticipate, <FileText size={24} color={colors.primary} />, 1)}
{renderGlassCard('Total Tenders', stats[0]?.totalTender, <Layers size={24} color={colors.primary} />, 2)}
</>
)} )}
</View>
);
<FlatList
data={DASHBOARD_CARDS}
keyExtractor={(_, index) => index.toString()}
numColumns={2}
columnWrapperStyle={styles.row}
contentContainerStyle={styles.list}
renderItem={({ item, index }) => {
return ( return (
<StatCard <View style={[styles.container, { backgroundColor: colors.background }]}>
item={item}
index={index} <ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
isLoading={isLoading}
/> {/* Large Typography Header */}
) <Animated.View entering={FadeInDown.duration(800)} style={styles.headerContainer}>
}} <View>
showsVerticalScrollIndicator={false} <AppText variant="bodySmall" color={colors.textSecondary}>Hello, {userData?.firstName || 'User'}</AppText>
/> <AppText variant="h1" color={colors.primary} style={{ marginTop: 4 }}>
Find Your{'\n'}Perfect {userType === 'auction' ? 'Deal' : 'Tender'}
</AppText>
</View>
<TouchableOpacity onPress={() => setIsMenuVisible(true)}>
<Menu size={28} color={colors.text} />
</TouchableOpacity>
</Animated.View>
<SidebarModal isVisible={isMenuVisible} onClose={() => setIsMenuVisible(false)} />
{/* Floating/Glass Stats */}
<View style={styles.sectionContainer}>
<AppText variant="button" color={colors.text} style={{ marginBottom: 16 }}>Overview</AppText>
{isLoadingDashboard || isLoadingTender ? (
<ActivityIndicator size="large" color={colors.primary} style={{ marginTop: 20 }} />
) : (
<StatGrid />
)}
</View>
{/* Placeholder for Recent Activity list with same glass style */}
<View style={styles.sectionContainer}>
<AppText variant="button" color={colors.text} style={{ marginBottom: 16 }}>Recent Activity</AppText>
<View style={[styles.glassCard, { width: '100%', height: 100, backgroundColor: colors.glass, alignItems: 'center', justifyContent: 'center' }]}>
<AppText variant="body" color={colors.textSecondary}>No recent activity</AppText>
</View>
</View>
</ScrollView>
</View > </View >
); );
}; };
@@ -228,81 +136,56 @@ if (user?.userType === "auction") {
export default Home; export default Home;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
screen: { container: {
flex: 1, flex: 1,
backgroundColor: colors.white,
}, },
list: { scrollContent: {
padding: 16, paddingTop: 60,
paddingBottom: 24, paddingHorizontal: 20,
paddingBottom: 100, // Space for floating tab bar
}, },
row: { headerContainer: {
marginBottom: 40,
flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: 16, alignItems: 'flex-start'
}, },
card: { sectionContainer: {
width: CARD_WIDTH, marginBottom: 32,
backgroundColor: '#FFFFFF', },
borderRadius: 16, gridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
glassCard: {
width: (width - 48) / 2, // 2 column grid
padding: 16, padding: 16,
borderRadius: 24,
borderWidth: 1, borderWidth: 1,
borderColor: '#E5E7EB', marginBottom: 16,
shadowColor: '#000', // Glassmorphism creates a subtle depth without heavy shadows
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 3,
}, },
header: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'flex-start',
marginBottom: 12,
}, },
title: { iconContainer: {
fontSize: 13, width: 40,
fontWeight: '600', height: 40,
color: '#374151', borderRadius: 12,
flex: 1,
paddingRight: 8,
},
badge: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 6,
borderWidth: 1,
borderRadius: 20,
paddingHorizontal: 8,
paddingVertical: 4,
},
badgeText: {
fontSize: 12,
fontWeight: '700',
},
value: {
fontSize: 28,
fontWeight: '800',
color: '#111827',
marginTop: 5,
},
desc: {
fontSize: 12,
color: '#6B7280',
marginTop: 6,
},
navigationOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center',
zIndex: 999,
}, },
loadingText: { cardValue: {
marginTop: 12, fontSize: 28,
fontSize: 16,
fontWeight: '600', fontWeight: '600',
color: '#374151', marginBottom: 4,
},
cardLabel: {
fontSize: 14,
fontWeight: '500',
}, },
}); });
+18 -4
View File
@@ -32,13 +32,17 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
}; };
}, []); }, []);
const navigateToLogin = () => { const handleNavigationBack = () => {
if (data.returnRoute) {
navigation.replace(data.returnRoute, data.returnParams);
} else {
navigation.dispatch( navigation.dispatch(
CommonActions.reset({ CommonActions.reset({
index: 0, index: 0,
routes: [{ name: 'Login' }], // Adjust to your login screen route name routes: [{ name: 'Login' }],
}) })
); );
}
}; };
const startSuccessCountdown = () => { const startSuccessCountdown = () => {
@@ -57,7 +61,7 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
successTimeoutRef.current = setTimeout(() => { successTimeoutRef.current = setTimeout(() => {
clearInterval(countdownInterval); clearInterval(countdownInterval);
navigateToLogin(); handleNavigationBack();
}, SUCCESS_DELAY_MS); }, SUCCESS_DELAY_MS);
}; };
@@ -126,7 +130,17 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
onLoadStart={handleLoadStart} onLoadStart={handleLoadStart}
onLoadEnd={handleLoadEnd} onLoadEnd={handleLoadEnd}
onError={handleError} onError={handleError}
onNavigationStateChange={handleNavigationStateChange}
onNavigationStateChange={(navState) => {
console.log("Current URL:", navState.url);
handleNavigationStateChange(navState);
}}
onShouldStartLoadWithRequest={(request) => {
console.log("Request URL:", request.url);
return true;
}}
renderLoading={() => <LoadingIndicator />} renderLoading={() => <LoadingIndicator />}
/> />
)} )}
+2 -1
View File
@@ -17,6 +17,7 @@ import { useNavigation } from '@react-navigation/native';
import colors from '../../Constant/Color'; import colors from '../../Constant/Color';
import { showToast } from '../../Constant/ShowDailog'; import { showToast } from '../../Constant/ShowDailog';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
interface ForgotModalProps { interface ForgotModalProps {
visible: boolean; visible: boolean;
@@ -58,7 +59,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
try { try {
// Make API call to send OTP for forgot password // Make API call to send OTP for forgot password
const response = await fetch( const response = await fetch(
`https://procurelinc.in/EProcurementSB/eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`, `${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
{ {
method: 'POST', method: 'POST',
headers: { headers: {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -263,7 +263,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
try { try {
const res = await axios.post( const res = await axios.post(
`https://procurelinc.in/EProcurementSB/eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`, `${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
null, null,
{ {
headers: { headers: {
+143 -210
View File
@@ -1,7 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState, useCallback } from 'react';
import { import {
View, View,
Text,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
@@ -15,8 +14,11 @@ import {
TextInput, TextInput,
KeyboardAvoidingView, KeyboardAvoidingView,
Alert, Alert,
StatusBar,
Image,
} from 'react-native'; } from 'react-native';
import { LinearGradient } from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import Animated, { FadeInDown, FadeInUp, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import { import {
AlertCircle, AlertCircle,
UserCircle, UserCircle,
@@ -51,15 +53,18 @@ import {
Edit, Edit,
X, X,
Save, Save,
ChevronRight,
LogOut
} from 'lucide-react-native'; } from 'lucide-react-native';
import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { useContractorProfile, useUpdateUserProfile, useESetuAdharVarification } from '../../Services/ApiCall/Hooks'; import { useContractorProfile, useUpdateUserProfile, useESetuAdharVarification } from '../../Services/ApiCall/Hooks';
import colors from '../../Constant/Color';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog'; import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog';
import { generateTransactionId } from '../../Helper/uniqueId'; import { generateTransactionId } from '../../Helper/uniqueId';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import ImagePath from '../../Constant/ImagePath';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@@ -165,8 +170,11 @@ const hasSectionData = (values: any[]): boolean => {
// Main Component // Main Component
const ProfileTab: React.FC = () => { const ProfileTab: React.FC = () => {
const { colors, spacing, shadows, isDarkMode } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const { data, refetch, isLoading, error } = useContractorProfile(); const { data, refetch, isLoading, error } = useContractorProfile();
const user = useSelector((state: any) => state.token?.token); const user = useSelector((state: any) => state.token?.token);
console.log("User: ", user)
const Navigation: any = useNavigation(); const Navigation: any = useNavigation();
const { mutateAsync: eSetuAdharVarification, isPending } = useESetuAdharVarification(); const { mutateAsync: eSetuAdharVarification, isPending } = useESetuAdharVarification();
@@ -182,7 +190,7 @@ const ProfileTab: React.FC = () => {
const displayMobile = isAuctionUser ? user?.mobileNo : data?.mobileNo; const displayMobile = isAuctionUser ? user?.mobileNo : data?.mobileNo;
const handleAadhaarPress = () => { const handleAadhaarPress = () => {
if (user?.esetuVerified === 'Not Verified') { if (user?.esetuVerified === 'Not Verified' || user?.esetuVerified === null) {
setShowDialog(true); setShowDialog(true);
} }
}; };
@@ -202,7 +210,7 @@ const ProfileTab: React.FC = () => {
udf2: "77.34", udf2: "77.34",
udf3: "string", udf3: "string",
udf4: "string", udf4: "string",
responseUrl: "https://dev.procurelinc.in/" responseUrl: "http://dev.192.168.6.194:8090/"
}; };
const res = await eSetuAdharVarification(payload); const res = await eSetuAdharVarification(payload);
@@ -221,7 +229,7 @@ const ProfileTab: React.FC = () => {
return ( return (
<View style={styles.centerContainer}> <View style={styles.centerContainer}>
<ActivityIndicator size="large" color={colors.primary} /> <ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>Loading profile...</Text> <AppText style={styles.loadingText}>Loading profile...</AppText>
</View> </View>
); );
} }
@@ -230,9 +238,9 @@ const ProfileTab: React.FC = () => {
return ( return (
<View style={styles.centerContainer}> <View style={styles.centerContainer}>
<AlertCircle size={64} color={colors.error} /> <AlertCircle size={64} color={colors.error} />
<Text style={styles.errorText}>{error}</Text> <AppText style={styles.screenErrorText}>{error instanceof Error ? error.message : String(error)}</AppText>
<TouchableOpacity style={styles.retryButton} onPress={refetch}> <TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
<Text style={styles.retryButtonText}>Retry</Text> <AppText style={styles.retryButtonText}>Retry</AppText>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );
@@ -243,7 +251,7 @@ const ProfileTab: React.FC = () => {
return ( return (
<> <>
<ScrollView <ScrollView
style={styles.container} style={[styles.container, { backgroundColor: colors.background }]}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
@@ -254,11 +262,6 @@ const ProfileTab: React.FC = () => {
/> />
} }
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={8}
windowSize={10}
> >
{/* Header with Gradient */} {/* Header with Gradient */}
<ProfileHeader data={data} displayName={displayName || data.fullName} /> <ProfileHeader data={data} displayName={displayName || data.fullName} />
@@ -352,7 +355,7 @@ const ProfileTab: React.FC = () => {
label="Expiry Date" label="Expiry Date"
value={data.expiryDate} value={data.expiryDate}
IconComponent={CalendarClock} IconComponent={CalendarClock}
valueColor={colors.warning} valueColor={colors.text}
/> />
</InfoSection> </InfoSection>
)} )}
@@ -401,6 +404,8 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
data, data,
onSuccess, onSuccess,
}) => { }) => {
const { colors } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const [formData, setFormData] = useState<UpdateProfilePayload>({ const [formData, setFormData] = useState<UpdateProfilePayload>({
firstName: data.firstName || '', firstName: data.firstName || '',
middleName: data.middleName || '', middleName: data.middleName || '',
@@ -484,83 +489,83 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
style={styles.modalOverlay} style={styles.modalOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
> >
<View style={styles.modalContent}> <View style={[styles.modalContent, { backgroundColor: colors.background }]}>
<View style={styles.modalHeader}> <View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Edit Personal Information</Text> <AppText variant="h3" style={styles.modalTitle}>Edit Personal Information</AppText>
<TouchableOpacity onPress={onClose} style={styles.closeButton}> <TouchableOpacity onPress={onClose} style={styles.closeButton}>
<X size={24} color={colors.darkText} /> <X size={24} color={colors.text} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}> <ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>First Name *</Text> <AppText variant="bodySmall" style={styles.inputLabel}>First Name *</AppText>
<TextInput <TextInput
style={[styles.input, errors.firstName && styles.inputError]} style={[styles.input, errors.firstName && styles.inputError]}
value={formData.firstName} value={formData.firstName}
onChangeText={(text) => setFormData({ ...formData, firstName: text })} onChangeText={(text) => setFormData({ ...formData, firstName: text })}
placeholder="Enter first name" placeholder="Enter first name"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
/> />
{errors.firstName && ( {errors.firstName && (
<Text style={styles.errorText}>{errors.firstName}</Text> <AppText variant="caption" style={styles.errorText}>{errors.firstName}</AppText>
)} )}
</View> </View>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Middle Name</Text> <AppText variant="bodySmall" style={styles.inputLabel}>Middle Name</AppText>
<TextInput <TextInput
style={styles.input} style={styles.input}
value={formData.middleName} value={formData.middleName}
onChangeText={(text) => setFormData({ ...formData, middleName: text })} onChangeText={(text) => setFormData({ ...formData, middleName: text })}
placeholder="Enter middle name" placeholder="Enter middle name"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
/> />
</View> </View>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Last Name *</Text> <AppText variant="bodySmall" style={styles.inputLabel}>Last Name *</AppText>
<TextInput <TextInput
style={[styles.input, errors.lastName && styles.inputError]} style={[styles.input, errors.lastName && styles.inputError]}
value={formData.lastName} value={formData.lastName}
onChangeText={(text) => setFormData({ ...formData, lastName: text })} onChangeText={(text) => setFormData({ ...formData, lastName: text })}
placeholder="Enter last name" placeholder="Enter last name"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
/> />
{errors.lastName && ( {errors.lastName && (
<Text style={styles.errorText}>{errors.lastName}</Text> <AppText variant="caption" style={styles.errorText}>{errors.lastName}</AppText>
)} )}
</View> </View>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Email *</Text> <AppText variant="bodySmall" style={styles.inputLabel}>Email *</AppText>
<TextInput <TextInput
style={[styles.input, errors.emailId && styles.inputError]} style={[styles.input, errors.emailId && styles.inputError]}
value={formData.emailId} value={formData.emailId}
onChangeText={(text) => setFormData({ ...formData, emailId: text })} onChangeText={(text) => setFormData({ ...formData, emailId: text })}
placeholder="Enter email" placeholder="Enter email"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
keyboardType="email-address" keyboardType="email-address"
autoCapitalize="none" autoCapitalize="none"
/> />
{errors.emailId && ( {errors.emailId && (
<Text style={styles.errorText}>{errors.emailId}</Text> <AppText variant="caption" style={styles.errorText}>{errors.emailId}</AppText>
)} )}
</View> </View>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Mobile Number *</Text> <AppText variant="bodySmall" style={styles.inputLabel}>Mobile Number *</AppText>
<TextInput <TextInput
style={[styles.input, errors.mobileNo && styles.inputError]} style={[styles.input, errors.mobileNo && styles.inputError]}
value={formData.mobileNo} value={formData.mobileNo}
onChangeText={(text) => setFormData({ ...formData, mobileNo: text })} onChangeText={(text) => setFormData({ ...formData, mobileNo: text })}
placeholder="Enter mobile number" placeholder="Enter mobile number"
placeholderTextColor={colors.grayText} placeholderTextColor={colors.textSecondary}
keyboardType="phone-pad" keyboardType="phone-pad"
maxLength={10} maxLength={10}
/> />
{errors.mobileNo && ( {errors.mobileNo && (
<Text style={styles.errorText}>{errors.mobileNo}</Text> <AppText variant="caption" style={styles.errorText}>{errors.mobileNo}</AppText>
)} )}
</View> </View>
</ScrollView> </ScrollView>
@@ -571,7 +576,7 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
onPress={onClose} onPress={onClose}
disabled={updateProfileMutation.isPending} disabled={updateProfileMutation.isPending}
> >
<Text style={styles.cancelButtonText}>Cancel</Text> <AppText variant="button" style={styles.cancelButtonText}>Cancel</AppText>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
@@ -580,11 +585,11 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
disabled={updateProfileMutation.isPending} disabled={updateProfileMutation.isPending}
> >
{updateProfileMutation.isPending ? ( {updateProfileMutation.isPending ? (
<ActivityIndicator size="small" color={colors.white} /> <ActivityIndicator size="small" color={colors.primary} />
) : ( ) : (
<> <>
<Save size={18} color={colors.white} /> <Save size={18} color={colors.buttonText} />
<Text style={styles.saveButtonText}>Save</Text> <AppText variant="button" style={styles.saveButtonText}>Save</AppText>
</> </>
)} )}
</TouchableOpacity> </TouchableOpacity>
@@ -597,6 +602,9 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
// Profile Header Component - OPTIMIZED // Profile Header Component - OPTIMIZED
const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayName }) => { const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayName }) => {
const { colors } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const getInitials = (name: string): string => { const getInitials = (name: string): string => {
if (!name || typeof name !== 'string' || name.trim().length === 0) { if (!name || typeof name !== 'string' || name.trim().length === 0) {
const fallbackName = `${data.firstName || ''} ${data.lastName || ''}`.trim(); const fallbackName = `${data.firstName || ''} ${data.lastName || ''}`.trim();
@@ -630,46 +638,44 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayN
return ( return (
<View> <View>
<LinearGradient <View
colors={[colors.primary, colors.primaryLight]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.headerGradient} style={styles.headerGradient}
> >
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
<View style={styles.avatar}> <View style={styles.avatar}>
<Text style={styles.avatarText}>{getInitials(displayName)}</Text> <AppText variant="h1" color={colors.text} style={styles.avatarText}>{getInitials(displayName)}</AppText>
</View> </View>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}> <View style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}>
<View style={styles.statusDot} /> <View style={styles.statusDot} />
</View> </View>
</View> </View>
<Text style={styles.headerName}> <AppText variant="h2" style={styles.headerName}>
{displayName || 'Unknown User'} {displayName || 'Unknown User'}
</Text> </AppText>
<View style={styles.roleBadge}> <View style={styles.roleBadge}>
<ShieldCheck size={16} color={colors.white} /> <ShieldCheck size={16} color={colors.text} />
<Text style={styles.roleText}>{data.role}</Text> <AppText variant="caption" style={styles.roleText}>{data.role}</AppText>
</View>
</View> </View>
</LinearGradient>
</View> </View>
); );
}); });
ProfileHeader.displayName = 'ProfileHeader'; ProfileHeader.displayName = 'ProfileHeader';
// Quick Stats Component - OPTIMIZED WITH TOUCHABLE AADHAAR // Quick Stats Component - Glass Cards
const QuickStats: React.FC<QuickStatsProps> = React.memo(({ data, onAadhaarPress }) => { const QuickStats: React.FC<QuickStatsProps> = React.memo(({ data, onAadhaarPress }) => {
const user = useSelector((state: any) => state.token?.token); const user = useSelector((state: any) => state.token?.token);
const { colors } = useTheme();
const stats: StatItem[] = useMemo( const stats: StatItem[] = useMemo(
() => [ () => [
{ {
Icon: CheckCircle, Icon: CheckCircle,
label: 'Aadhaar', label: 'Aadhaar',
value: user?.esetuVerified, value: user?.esetuVerified || 'Pending',
color: user?.esetuVerified === 'Not Verified' ? colors.error : colors.success, color: user?.esetuVerified === 'Not Verified' ? colors.error : colors.success,
onPress: onAadhaarPress, onPress: onAadhaarPress,
}, },
@@ -677,169 +683,103 @@ const QuickStats: React.FC<QuickStatsProps> = React.memo(({ data, onAadhaarPress
Icon: ShieldCheck, Icon: ShieldCheck,
label: '2FA', label: '2FA',
value: data.twoFactorAuthEnabled ? 'Enabled' : 'Disabled', value: data.twoFactorAuthEnabled ? 'Enabled' : 'Disabled',
color: data.twoFactorAuthEnabled ? colors.success : colors.grayText, color: data.twoFactorAuthEnabled ? colors.success : colors.textSecondary,
},
{
Icon: FileText,
label: 'Registration',
value: data.registrationStatus,
color: colors.info,
}, },
// ...
], ],
[data.twoFactorAuthEnabled, data.registrationStatus, user?.esetuVerified, onAadhaarPress] [data.twoFactorAuthEnabled, user?.esetuVerified, onAadhaarPress, colors]
); );
return ( return (
<View style={styles.statsContainer}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 24 }}>
{stats.map((stat) => { {stats.map((stat, index) => {
const CardContent = ( const content = (
<View style={styles.statCard}> <Animated.View
<stat.Icon size={28} color={stat.color} /> entering={FadeInUp.duration(600).delay(index * 100)}
<Text style={styles.statValue} numberOfLines={2}> style={{
{stat.value} flex: 1, backgroundColor: colors.glass, borderColor: colors.overlay, borderWidth: 1,
</Text> borderRadius: 20, padding: 12, alignItems: 'center', marginHorizontal: 4,
<Text style={styles.statLabel}>{stat.label}</Text> minHeight: 100, justifyContent: 'center'
</View> }}
);
if (stat.onPress) {
return (
<TouchableOpacity
key={stat.label}
activeOpacity={0.7}
onPress={stat.onPress}
style={{ flex: 1 }}
> >
{CardContent} <View style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: stat.color + '10', alignItems: 'center', justifyContent: 'center', marginBottom: 8 }}>
</TouchableOpacity> <stat.Icon size={20} color={stat.color} />
</View>
<AppText variant="bodySmall" color={colors.text} numberOfLines={1} style={{ textAlign: 'center' }}>{stat.value}</AppText>
<AppText variant="caption" color={colors.textSecondary}>{stat.label}</AppText>
</Animated.View>
); );
}
return <View key={stat.label} style={{ flex: 1 }}>{CardContent}</View>; return stat.onPress ? (
<TouchableOpacity key={stat.label} style={{ flex: 1 }} onPress={stat.onPress}>{content}</TouchableOpacity>
) : (
<View key={stat.label} style={{ flex: 1 }}>{content}</View>
);
})} })}
</View> </View>
); );
}); });
QuickStats.displayName = 'QuickStats'; // Info Section Component - Glass Card Container
// Info Section Component - OPTIMIZED
const InfoSection: React.FC<InfoSectionProps> = React.memo( const InfoSection: React.FC<InfoSectionProps> = React.memo(
({ title, IconComponent, children, onEdit, showEdit = false }) => { ({ title, IconComponent, children, onEdit, showEdit = false, delay = 0 }) => {
const { colors } = useTheme();
return ( return (
<View style={styles.section}> <Animated.View
<View style={styles.sectionHeader}> entering={FadeInUp.duration(600).delay(delay)}
<View style={styles.sectionHeaderLeft}> style={{
<View style={styles.sectionIconContainer}> backgroundColor: colors.glass,
<IconComponent size={24} color={colors.primary} /> borderColor: colors.overlay,
borderWidth: 1,
borderRadius: 24,
padding: 20,
marginBottom: 16
}}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ padding: 8, backgroundColor: colors.primary + '10', borderRadius: 10, marginRight: 12 }}>
<IconComponent size={20} color={colors.primary} />
</View> </View>
<Text style={styles.sectionTitle}>{title}</Text> <AppText variant="h4" color={colors.text}>{title}</AppText>
</View> </View>
{showEdit && onEdit && ( {showEdit && onEdit && (
<TouchableOpacity style={styles.editButton} onPress={onEdit}> <TouchableOpacity onPress={onEdit} style={{ padding: 4 }}>
<Edit size={20} color={colors.primary} /> <Edit size={18} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
<View style={styles.sectionContent}>{children}</View> <View>{children}</View>
</View> </Animated.View>
); );
} }
); );
InfoSection.displayName = 'InfoSection'; // Info Row Component
// Info Row Component - OPTIMIZED
const InfoRow: React.FC<InfoRowProps> = React.memo( const InfoRow: React.FC<InfoRowProps> = React.memo(
({ label, value, IconComponent, valueColor }) => { ({ label, value, IconComponent, valueColor }) => {
if (!isValidValue(value)) { const { colors } = useTheme();
return null;
} if (!isValidValue(value)) return null;
return ( return (
<View style={styles.infoRow}> <View style={{ flexDirection: 'row', marginBottom: 16, alignItems: 'flex-start' }}>
<View style={styles.infoRowTouchable}> <IconComponent size={16} color={colors.textSecondary} style={{ marginTop: 4, marginRight: 12 }} />
<View style={styles.infoRowContent}> <View style={{ flex: 1 }}>
<View style={styles.infoIconContainer}> <AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 2 }}>{label}</AppText>
<IconComponent size={18} color={colors.primary} /> <AppText variant='bodySmall' color={valueColor || colors.text}>{value}</AppText>
</View>
<View style={styles.infoTextContainer}>
<Text style={styles.infoLabel}>{label}</Text>
<Text
style={[styles.infoValue, valueColor && { color: valueColor }]}
>
{value}
</Text>
</View>
</View>
</View> </View>
</View> </View>
); );
} }
); );
InfoRow.displayName = 'InfoRow';
// Styles // Styles
interface Styles { const createStyles = (colors: any) => StyleSheet.create({
container: ViewStyle;
contentContainer: ViewStyle;
centerContainer: ViewStyle;
loadingText: TextStyle;
errorText: TextStyle;
retryButton: ViewStyle;
retryButtonText: TextStyle;
headerGradient: ViewStyle;
avatarContainer: ViewStyle;
avatar: ViewStyle;
avatarText: TextStyle;
statusBadge: ViewStyle;
statusDot: ViewStyle;
headerName: TextStyle;
roleBadge: ViewStyle;
roleText: TextStyle;
statsContainer: ViewStyle;
statCard: ViewStyle;
statValue: TextStyle;
statLabel: TextStyle;
section: ViewStyle;
sectionHeader: ViewStyle;
sectionHeaderLeft: ViewStyle;
sectionIconContainer: ViewStyle;
sectionTitle: TextStyle;
sectionContent: ViewStyle;
infoRow: ViewStyle;
infoRowTouchable: ViewStyle;
infoRowContent: ViewStyle;
infoIconContainer: ViewStyle;
infoTextContainer: ViewStyle;
infoLabel: TextStyle;
infoValue: TextStyle;
editButton: ViewStyle;
modalOverlay: ViewStyle;
modalContent: ViewStyle;
modalHeader: ViewStyle;
modalTitle: TextStyle;
closeButton: ViewStyle;
formContainer: ViewStyle;
inputGroup: ViewStyle;
inputLabel: TextStyle;
input: TextStyle;
inputError: TextStyle;
errorText: TextStyle;
modalFooter: ViewStyle;
button: ViewStyle;
cancelButton: ViewStyle;
saveButton: ViewStyle;
cancelButtonText: TextStyle;
saveButtonText: TextStyle;
navigationOverlay: ViewStyle;
}
const styles = StyleSheet.create<Styles>({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.lightGray, paddingHorizontal: 20
}, },
contentContainer: { contentContainer: {
paddingBottom: 20, paddingBottom: 20,
@@ -848,16 +788,16 @@ const styles = StyleSheet.create<Styles>({
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.white, backgroundColor: colors.background,
padding: 20, padding: 20,
}, },
loadingText: { loadingText: {
marginTop: 16, marginTop: 16,
fontSize: 16, fontSize: 16,
color: colors.grayText, color: colors.textSecondary,
fontWeight: '500', fontWeight: '500',
}, },
errorText: { screenErrorText: {
marginTop: 16, marginTop: 16,
fontSize: 16, fontSize: 16,
color: colors.error, color: colors.error,
@@ -886,13 +826,6 @@ const styles = StyleSheet.create<Styles>({
paddingTop: Platform.OS === 'ios' ? 60 : 40, paddingTop: Platform.OS === 'ios' ? 60 : 40,
paddingBottom: 30, paddingBottom: 30,
alignItems: 'center', alignItems: 'center',
borderBottomLeftRadius: 32,
borderBottomRightRadius: 32,
elevation: 8,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
}, },
avatarContainer: { avatarContainer: {
position: 'relative', position: 'relative',
@@ -902,11 +835,11 @@ const styles = StyleSheet.create<Styles>({
width: 100, width: 100,
height: 100, height: 100,
borderRadius: 50, borderRadius: 50,
backgroundColor: colors.white, backgroundColor: colors.card,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderWidth: 4, borderWidth: 4,
borderColor: colors.white, borderColor: colors.card,
elevation: 4, elevation: 4,
shadowColor: colors.shadow, shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
@@ -926,7 +859,7 @@ const styles = StyleSheet.create<Styles>({
height: 24, height: 24,
borderRadius: 12, borderRadius: 12,
borderWidth: 3, borderWidth: 3,
borderColor: colors.white, borderColor: colors.card, // Updated
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
@@ -939,7 +872,7 @@ const styles = StyleSheet.create<Styles>({
headerName: { headerName: {
fontSize: 26, fontSize: 26,
fontWeight: '700', fontWeight: '700',
color: colors.white, color: colors.text, // Updated
marginBottom: 8, marginBottom: 8,
textAlign: 'center', textAlign: 'center',
paddingHorizontal: 20, paddingHorizontal: 20,
@@ -947,14 +880,14 @@ const styles = StyleSheet.create<Styles>({
roleBadge: { roleBadge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.25)', backgroundColor: colors.primary + '20', // Updated opacity approach
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 6, paddingVertical: 6,
borderRadius: 20, borderRadius: 20,
gap: 6, gap: 6,
}, },
roleText: { roleText: {
color: colors.white, color: colors.primary, // Updated
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
}, },
@@ -968,7 +901,7 @@ const styles = StyleSheet.create<Styles>({
}, },
statCard: { statCard: {
flex: 1, flex: 1,
backgroundColor: colors.white, backgroundColor: colors.card,
borderRadius: 16, borderRadius: 16,
padding: 16, padding: 16,
alignItems: 'center', alignItems: 'center',
@@ -981,20 +914,20 @@ const styles = StyleSheet.create<Styles>({
statValue: { statValue: {
fontSize: 14, fontSize: 14,
fontWeight: '700', fontWeight: '700',
color: colors.darkText, color: colors.text,
marginTop: 8, marginTop: 8,
textAlign: 'center', textAlign: 'center',
}, },
statLabel: { statLabel: {
fontSize: 12, fontSize: 12,
color: colors.grayText, color: colors.textSecondary,
marginTop: 4, marginTop: 4,
}, },
section: { section: {
marginHorizontal: 16, marginHorizontal: 16,
marginTop: 24, marginTop: 24,
backgroundColor: colors.white, backgroundColor: colors.card,
borderRadius: 20, borderRadius: 20,
padding: 20, padding: 20,
elevation: 2, elevation: 2,
@@ -1021,7 +954,7 @@ const styles = StyleSheet.create<Styles>({
width: 40, width: 40,
height: 40, height: 40,
borderRadius: 12, borderRadius: 12,
backgroundColor: colors.primaryHighlight, backgroundColor: colors.primary + '10', // Updated
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: 12, marginRight: 12,
@@ -1029,7 +962,7 @@ const styles = StyleSheet.create<Styles>({
sectionTitle: { sectionTitle: {
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: colors.darkText, color: colors.text,
flex: 1, flex: 1,
}, },
sectionContent: { sectionContent: {
@@ -1038,7 +971,7 @@ const styles = StyleSheet.create<Styles>({
editButton: { editButton: {
padding: 8, padding: 8,
borderRadius: 8, borderRadius: 8,
backgroundColor: colors.primaryHighlight, backgroundColor: colors.primary + '10', // Updated
}, },
infoRow: { infoRow: {
@@ -1047,7 +980,7 @@ const styles = StyleSheet.create<Styles>({
}, },
infoRowTouchable: { infoRowTouchable: {
padding: 14, padding: 14,
backgroundColor: colors.lightestGray, backgroundColor: colors.background, // Updated
borderRadius: 12, borderRadius: 12,
}, },
infoRowContent: { infoRowContent: {
@@ -1058,7 +991,7 @@ const styles = StyleSheet.create<Styles>({
width: 32, width: 32,
height: 32, height: 32,
borderRadius: 8, borderRadius: 8,
backgroundColor: colors.white, backgroundColor: colors.card, // Updated
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: 12, marginRight: 12,
@@ -1070,13 +1003,13 @@ const styles = StyleSheet.create<Styles>({
}, },
infoLabel: { infoLabel: {
fontSize: 13, fontSize: 13,
color: colors.grayText, color: colors.textSecondary,
fontWeight: '500', fontWeight: '500',
lineHeight: 18, lineHeight: 18,
}, },
infoValue: { infoValue: {
fontSize: 15, fontSize: 15,
color: colors.darkText, color: colors.text,
fontWeight: '600', fontWeight: '600',
lineHeight: 21, lineHeight: 21,
}, },
@@ -1087,11 +1020,11 @@ const styles = StyleSheet.create<Styles>({
justifyContent: 'flex-end', justifyContent: 'flex-end',
}, },
modalContent: { modalContent: {
backgroundColor: colors.white,
borderTopLeftRadius: 24, borderTopLeftRadius: 24,
borderTopRightRadius: 24, borderTopRightRadius: 24,
maxHeight: '90%', maxHeight: '90%',
paddingTop: 20, paddingTop: 20,
borderRadius: 30
}, },
modalHeader: { modalHeader: {
flexDirection: 'row', flexDirection: 'row',
@@ -1105,7 +1038,7 @@ const styles = StyleSheet.create<Styles>({
modalTitle: { modalTitle: {
fontSize: 20, fontSize: 20,
fontWeight: '700', fontWeight: '700',
color: colors.darkText, color: colors.text,
}, },
closeButton: { closeButton: {
padding: 4, padding: 4,
@@ -1120,7 +1053,7 @@ const styles = StyleSheet.create<Styles>({
inputLabel: { inputLabel: {
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
color: colors.darkText, color: colors.text,
marginBottom: 8, marginBottom: 8,
}, },
input: { input: {
@@ -1129,8 +1062,8 @@ const styles = StyleSheet.create<Styles>({
borderRadius: 12, borderRadius: 12,
padding: 14, padding: 14,
fontSize: 15, fontSize: 15,
color: colors.darkText, color: colors.text,
backgroundColor: colors.white, backgroundColor: colors.card, // Updated
}, },
inputError: { inputError: {
borderColor: colors.error, borderColor: colors.error,
@@ -1157,7 +1090,7 @@ const styles = StyleSheet.create<Styles>({
gap: 8, gap: 8,
}, },
cancelButton: { cancelButton: {
backgroundColor: colors.lightestGray, backgroundColor: colors.background, // Updated
}, },
saveButton: { saveButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
@@ -1165,7 +1098,7 @@ const styles = StyleSheet.create<Styles>({
cancelButtonText: { cancelButtonText: {
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '600',
color: colors.darkText, color: colors.text,
}, },
saveButtonText: { saveButtonText: {
fontSize: 16, fontSize: 16,
+107 -81
View File
@@ -3,23 +3,22 @@ import {
SafeAreaView, SafeAreaView,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text,
TouchableOpacity, TouchableOpacity,
View, View,
Platform, Platform,
} from 'react-native'; } from 'react-native';
import React, { use, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useNavigation, useRoute, NavigationProp, RouteProp } from '@react-navigation/native'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton'; import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import MyInput from '../../Component/MyInput'; import MyInput from '../../components/MyInput';
import { showToast } from '../../Constant/ShowDailog'; import { showToast } from '../../Constant/ShowDailog';
import { PARTNER_API_ENDPOINT } from '../../Services/ApiCall/ApiUrls';
import { usePartnerShipSubmit } from '../../Services/ApiCall/Hooks'; import { usePartnerShipSubmit } from '../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Navigation } from 'lucide-react-native';
import { NavigationString } from '../../Constant/AllImports'; import { NavigationString } from '../../Constant/AllImports';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Trash2, UserPlus } from 'lucide-react-native';
type Partner = { type Partner = {
partnerName: string; partnerName: string;
@@ -32,11 +31,12 @@ type Navigation = {
replace: (route: string) => void; replace: (route: string) => void;
}; };
type PartnershipParams = { type PartnershipParams = {
otherId: string | number otherId: string | number;
}; };
const Partnership = () => { const Partnership = () => {
const route = useRoute<RouteProp<{ params: PartnershipParams }, 'params'>>(); const route = useRoute<RouteProp<{ params: PartnershipParams }, 'params'>>();
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation() as Navigation; const navigation = useNavigation() as Navigation;
const [partners, setPartners] = useState<Partner[]>([ const [partners, setPartners] = useState<Partner[]>([
{ {
@@ -49,9 +49,11 @@ const Partnership = () => {
]); ]);
const [focusedField, setFocusedField] = useState<string>(''); const [focusedField, setFocusedField] = useState<string>('');
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const { mutateAsync: partnerShipSubmit } = usePartnerShipSubmit(); const { mutateAsync: partnerShipSubmit } = usePartnerShipSubmit();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
const handleChange = (index: number, name: keyof Partner, value: string) => { const handleChange = (index: number, name: keyof Partner, value: string) => {
const updated = [...partners]; const updated = [...partners];
updated[index][name] = value; updated[index][name] = value;
@@ -90,9 +92,8 @@ const Partnership = () => {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
setLoading(true) setLoading(true);
try { try {
// Convert percentage strings to numbers and validate
const payload = partners.map((p, i) => { const payload = partners.map((p, i) => {
const percentage = Number(p.percentage); const percentage = Number(p.percentage);
@@ -105,13 +106,12 @@ const Partnership = () => {
ptnrAadharNo: p.aadharNumber, ptnrAadharNo: p.aadharNumber,
ptnrMobNo: p.mobileNumber, ptnrMobNo: p.mobileNumber,
ptnrName: p.partnerName, ptnrName: p.partnerName,
ptnrPop: percentage, // Ensure this is a number ptnrPop: percentage,
ptnrPANNo: p.partnerPan, ptnrPANNo: p.partnerPan,
ptnrNo: i, ptnrNo: i,
}; };
}); });
// Validate total percentage equals 100%
const totalPercentage = payload.reduce((sum, p) => sum + p.ptnrPop, 0); const totalPercentage = payload.reduce((sum, p) => sum + p.ptnrPop, 0);
if (totalPercentage !== 100) { if (totalPercentage !== 100) {
showToast(`Total percentage must equal 100% (currently ${totalPercentage}%)`); showToast(`Total percentage must equal 100% (currently ${totalPercentage}%)`);
@@ -123,153 +123,179 @@ const Partnership = () => {
if (response?.statusCode === 200) { if (response?.statusCode === 200) {
showToast(response.message || 'Submitted successfully!'); showToast(response.message || 'Submitted successfully!');
navigation.replace(NavigationString.Home); navigation.replace(NavigationString.Home);
} }
} catch (error: any) { } catch (error: any) {
console.error('❌ Submission failed:', error.message); console.error('❌ Submission failed:', error.message);
showToast(error.message || 'Submission failed. Please check partner details.'); showToast(error.message || 'Submission failed. Please check partner details.');
} finally { } finally {
setLoading(false) setLoading(false);
} }
}; };
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<Text style={[styles.heading, { paddingTop: insets.top }]}>Partnership Details</Text> <View style={[styles.header, { paddingTop: insets.top + spacing.s }]}>
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
Partnership Details
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Add all partners and their share percentages
</AppText>
</View>
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40} keyboardVerticalOffset={40}
> >
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled"> <ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{partners.map((partner, index) => ( {partners.map((partner, index) => (
<View style={{ marginBottom: 20 }} key={index}> <Animated.View
<Text style={styles.partnerTitle}>Partner {index + 1}</Text> key={index}
entering={FadeInUp.delay(index * 80).springify()}
style={[styles.card, shadows.medium]}
>
<View style={styles.cardHeader}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
Partner {index + 1}
</AppText>
{partners.length > 1 && (
<TouchableOpacity onPress={() => handleRemove(index)} activeOpacity={0.7}>
<Trash2 size={18} color={colors.error} />
</TouchableOpacity>
)}
</View>
<MyInput <MyInput
icon="user" label="Partner Name"
placeholder="Name of Partner *" placeholder="Enter Partner Name"
value={partner.partnerName} value={partner.partnerName}
onChangeText={text => handleChange(index, 'partnerName', text)} onChangeText={text => handleChange(index, 'partnerName', text)}
onFocus={() => setFocusedField(`partnerName-${index}`)} onFocus={() => setFocusedField(`partnerName-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `partnerName-${index}`} focused={focusedField === `partnerName-${index}`}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="percent" label="Share Percentage"
placeholder="Partnership Percentage *" placeholder="Enter Percentage"
value={partner.percentage} value={partner.percentage}
onChangeText={text => handleChange(index, 'percentage', text)} onChangeText={text => handleChange(index, 'percentage', text)}
onFocus={() => setFocusedField(`percentage-${index}`)} onFocus={() => setFocusedField(`percentage-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `percentage-${index}`} focused={focusedField === `percentage-${index}`}
keyboardType="numeric" keyboardType="numeric"
maxlength={3} maxLength={3}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="id-card" label="PAN of Partner"
placeholder="PAN of Partner *" placeholder="Enter PAN Number"
value={partner.partnerPan} value={partner.partnerPan}
onChangeText={text => handleChange(index, 'partnerPan', text)} onChangeText={text => handleChange(index, 'partnerPan', text)}
onFocus={() => setFocusedField(`partnerPan-${index}`)} onFocus={() => setFocusedField(`partnerPan-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `partnerPan-${index}`} focused={focusedField === `partnerPan-${index}`}
autoCapitalize="characters" autoCapitalize="characters"
maxlength={10} maxLength={10}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="credit-card" label="Aadhaar Number"
placeholder="Aadhar Number *" placeholder="Enter 12-digit Aadhaar"
value={partner.aadharNumber} value={partner.aadharNumber}
onChangeText={text => handleChange(index, 'aadharNumber', text)} onChangeText={text => handleChange(index, 'aadharNumber', text)}
onFocus={() => setFocusedField(`aadharNumber-${index}`)} onFocus={() => setFocusedField(`aadharNumber-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `aadharNumber-${index}`} focused={focusedField === `aadharNumber-${index}`}
keyboardType="numeric" keyboardType="numeric"
maxlength={12} maxLength={12}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="phone" label="Mobile Number"
placeholder="Mobile Number *" placeholder="Enter Mobile Number"
value={partner.mobileNumber} value={partner.mobileNumber}
onChangeText={text => handleChange(index, 'mobileNumber', text)} onChangeText={text => handleChange(index, 'mobileNumber', text)}
onFocus={() => setFocusedField(`mobileNumber-${index}`)} onFocus={() => setFocusedField(`mobileNumber-${index}`)}
onBlur={() => setFocusedField('')} onBlur={() => setFocusedField('')}
focused={focusedField === `mobileNumber-${index}`} focused={focusedField === `mobileNumber-${index}`}
keyboardType="phone-pad" keyboardType="phone-pad"
maxlength={10} maxLength={10}
/> />
</Animated.View>
{partners.length > 1 && (
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
<Text style={styles.deleteText}>Delete Partner</Text>
</TouchableOpacity>
)}
</View>
))} ))}
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}> <TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
<Text style={styles.addText}>+ Add Partner</Text> <UserPlus size={16} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
Add Partner
</AppText>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>
<PrimaryButton title={loading ? "Loading..." : "Submit" }onPress={handleSubmit} /> <View style={styles.footer}>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
</View>
</View> </View>
); );
}; };
export default Partnership; export default Partnership;
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
heading: { header: {
fontSize: 18, paddingHorizontal: spacing.m,
fontWeight: 'bold', paddingBottom: spacing.m,
marginBottom: 10, borderBottomWidth: 1,
marginTop: 10, borderBottomColor: colors.border,
textAlign: 'center', backgroundColor: colors.surface,
},
partnerTitle: {
fontWeight: '600',
fontSize: 16,
marginBottom: 10,
}, },
scrollContainer: { scrollContainer: {
// backgroundColor: color.background,
flexGrow: 1, flexGrow: 1,
padding: spacing.m,
paddingBottom: 30,
},
card: {
backgroundColor: colors.surface,
borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
paddingBottom: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
}, },
addButton: { addButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
padding: 10, flexDirection: 'row',
marginTop: 10,
borderRadius: 10,
alignItems: 'center', alignItems: 'center',
width: 140, justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 12,
alignSelf: 'flex-end', alignSelf: 'flex-end',
}, },
addText: { footer: {
color: 'white', padding: spacing.m,
fontWeight: '600', borderTopWidth: 1,
}, borderTopColor: colors.border,
deleteButton: { backgroundColor: colors.surface,
backgroundColor: '#FF5C5C',
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 140,
alignSelf: 'flex-end',
},
deleteText: {
color: 'white',
fontWeight: '600',
}, },
}); });
+128 -109
View File
@@ -1,24 +1,16 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, SafeAreaView, ScrollView, BackHandler, ToastAndroid, Button, Alert } from 'react-native'; import { View, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, ScrollView, BackHandler, ToastAndroid, Alert } from 'react-native';
// import AntDesign from 'react-native-vector-icons/AntDesign';
import { ArrowLeft } from 'lucide-react-native'; import { ArrowLeft } from 'lucide-react-native';
import { NavigationProp, RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
// import { sendAadhaarOtp, verifyAadhaarOtp, verifyGstNumber, verifyPanHandlerRes } from '../../utils/helper/ragisterationApiCalls';
// import { getAllMsMeOrg, getCityDropdown, getClassDropdown, getMsmeDropdown, getOrgDropdown, getSocialDropdown, getStateDropdown } from '../../utils/helper/dropdownApis';
import { ApiResponse, DocumentsType, FormDataType, OrgType, UserData } from './types/types'; import { ApiResponse, DocumentsType, FormDataType, OrgType, UserData } from './types/types';
import { pick } from '@react-native-documents/picker'; import { pick } from '@react-native-documents/picker';
// import { REJ_STAGE_TWO } from '../../utils/services/apiUrl';
// import LoaderModel from '../../component/LoaderModal';
import axios from 'axios'; import axios from 'axios';
import StageOne from './ragistrationStages/StageOne'; import StageOne from './ragistrationStages/StageOne';
import StageTwo from './ragistrationStages/StageTwo'; import StageTwo from './ragistrationStages/StageTwo';
import StageZero from './ragistrationStages/StageZero'; import StageZero from './ragistrationStages/StageZero';
import StageThree from './ragistrationStages/StageThree'; import StageThree from './ragistrationStages/StageThree';
import StageFour from './ragistrationStages/StageFour'; import StageFour from './ragistrationStages/StageFour';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton'; import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import StepProgressBar from '../../Component/StepProgressBar'; import StepProgressBar from '../../Component/StepProgressBar';
import { formatDateInput } from '../../Helper/formatDateInput'; import { formatDateInput } from '../../Helper/formatDateInput';
@@ -26,6 +18,9 @@ import { showToast } from '../../Constant/ShowDailog';
import { useCityDropdown, useContractorStageTwoSubmit, usefetchMasterData, useSendAadhaarOtp, useVerifyAadhaarOtp, useVerifyGstNumber, useVerifyPan } from '../../Services/ApiCall/Hooks'; import { useCityDropdown, useContractorStageTwoSubmit, usefetchMasterData, useSendAadhaarOtp, useVerifyAadhaarOtp, useVerifyGstNumber, useVerifyPan } from '../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
type RouteParams = { type RouteParams = {
userData?: { userData?: {
@@ -38,6 +33,7 @@ type RouteParams = {
}; };
const Registration: React.FC = () => { const Registration: React.FC = () => {
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const { params } = useRoute<RouteProp<Record<string, RouteParams>, string>>(); const { params } = useRoute<RouteProp<Record<string, RouteParams>, string>>();
const userData = params as RouteParams; const userData = params as RouteParams;
@@ -102,6 +98,9 @@ const Registration: React.FC = () => {
const { mutateAsync: verifyGstNumber } = useVerifyGstNumber(); const { mutateAsync: verifyGstNumber } = useVerifyGstNumber();
const { mutateAsync: contractorStageTwoSubmit } = useContractorStageTwoSubmit(); const { mutateAsync: contractorStageTwoSubmit } = useContractorStageTwoSubmit();
const { data: masterData } = usefetchMasterData(); const { data: masterData } = usefetchMasterData();
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
useEffect(() => { useEffect(() => {
if (!masterData?.data) return; if (!masterData?.data) return;
const contractorClasses = masterData.data.contractorClasses || []; const contractorClasses = masterData.data.contractorClasses || [];
@@ -138,22 +137,13 @@ useEffect(() => {
const { mutate: cityDropdownMutate } = useCityDropdown(); const { mutate: cityDropdownMutate } = useCityDropdown();
const { data: MasterData}=usefetchMasterData() const { data: MasterData } = usefetchMasterData();
const user = useSelector((state: any) => state.token.token); const user = useSelector((state: any) => state.token.token);
const steps = ['', '', '', '', '']; const steps = ['', '', '', '', ''];
const dropdownStyle = {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: '#fff',
marginBottom: 10,
}
useEffect(() => { useEffect(() => {
const fetchUserData = async (user: any) => { const fetchUserData = async (user: any) => {
try { try {
if (user) { if (user) {
const parsedUser = user; const parsedUser = user;
setFormData(prev => ({ setFormData(prev => ({
@@ -180,25 +170,11 @@ useEffect(() => {
gstin = gstin.toUpperCase(); gstin = gstin.toUpperCase();
const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]$/; const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;
if (!panRegex.test(pan)) { if (!panRegex.test(pan)) {
return { valid: false, message: "Invalid PAN format. It should be like 'ABCDE1234F'." }; return { valid: false, message: "Invalid PAN format. It should be like 'ABCDE1234F'." };
} }
// if (!gstinRegex.test(gstin)) {
// return { valid: false, message: "Invalid GSTIN format. It should be like '22ABCDE1234F1Z5'." };
// }
// const gstinPAN = gstin.slice(2, 12); // Extract PAN part from GSTIN
// if (pan !== gstinPAN) {
// return {
// valid: false,
// message: "PAN does not match the PAN embedded in GSTIN.",
// };
// }
return { valid: true, message: 'PAN and GSTIN are correctly linked.' }; return { valid: true, message: 'PAN and GSTIN are correctly linked.' };
}; };
@@ -207,7 +183,7 @@ useEffect(() => {
}; };
const verifyPanHandler = async () => { const verifyPanHandler = async () => {
setPanVerifying(true); // Start loading setPanVerifying(true);
const payload = { const payload = {
dob: formData?.dateOfIncorp.trim(), dob: formData?.dateOfIncorp.trim(),
name: formData?.orgPanName.trim(), name: formData?.orgPanName.trim(),
@@ -220,7 +196,7 @@ useEffect(() => {
} else { } else {
setPanVerified(false); setPanVerified(false);
} }
setPanVerifying(false); // Stop loading setPanVerifying(false);
}; };
const sendAdharOTPHandler = async (text: string) => { const sendAdharOTPHandler = async (text: string) => {
@@ -333,11 +309,10 @@ useEffect(() => {
fileData.append('pancardfile', documents.panCard as any); fileData.append('pancardfile', documents.panCard as any);
fileData.append('regfile', documents.registrationCertificate as any); fileData.append('regfile', documents.registrationCertificate as any);
const res = await contractorStageTwoSubmit(fileData); const res = await contractorStageTwoSubmit(fileData);
if (res.statusCode === 200) {
if (res.status === 200) {
if (res) { if (res) {
showToast('Form submitted successfully!'); showToast('Form submitted successfully!');
const orgType = selectedOrg; const orgType = selectedOrg;
@@ -357,7 +332,6 @@ useEffect(() => {
} }
} catch (error: any) { } catch (error: any) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
} else { } else {
console.error('Unexpected error', error); console.error('Unexpected error', error);
} }
@@ -369,7 +343,6 @@ useEffect(() => {
}; };
const fetchCity = async (stateId: number) => { const fetchCity = async (stateId: number) => {
cityDropdownMutate(stateId, { cityDropdownMutate(stateId, {
onSuccess: (data) => { onSuccess: (data) => {
@@ -380,13 +353,10 @@ useEffect(() => {
console.error('Error in city fetch:', error); console.error('Error in city fetch:', error);
} }
}); });
}; };
console.log('formdata : ', formData); console.log('formdata : ', formData);
useEffect(() => { useEffect(() => {
if (formData?.state) { if (formData?.state) {
fetchCity(Number(formData.state)); fetchCity(Number(formData.state));
@@ -502,15 +472,31 @@ useEffect(() => {
} }
}; };
// ── Stage Titles ──
const stageTitles = [
'Personal & Contact',
'Registration Details',
'KYC Verification',
'Address Details',
'Upload Documents',
];
return ( return (
<View style={{ flex: 1, backgroundColor: colors.white }}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -30}
>
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
bounces={false}
showsVerticalScrollIndicator={false}
> >
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled" bounces={false}>
<View style={{ paddingBottom: 20, marginTop: insets.top, flex: 1 }}> <View style={{ paddingBottom: 20, marginTop: insets.top, flex: 1 }}>
{/* ═══ Back Button ═══ */}
<View style={styles.headerRow}>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
if (stage > 0) { if (stage > 0) {
@@ -518,12 +504,39 @@ useEffect(() => {
} }
}} }}
style={styles.backBtn} style={styles.backBtn}
activeOpacity={0.7}
> >
{/* Replaced AntDesign with Lucide ArrowLeft */} <ArrowLeft size={22} color={colors.text} />
<ArrowLeft size={25} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
Registration
</AppText>
<View style={{ width: 36 }} />
</View>
{/* ═══ Progress Bar ═══ */}
<StepProgressBar currentStep={stage} steps={steps} /> <StepProgressBar currentStep={stage} steps={steps} />
{/* ═══ Stage Title ═══
<Animated.View
key={`title-${stage}`}
entering={FadeInUp.duration(300)}
style={styles.stageTitleContainer}
>
<AppText variant="h3" color={colors.text} style={styles.stageTitle}>
{stageTitles[stage]}
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Step {stage + 1} of {steps.length}
</AppText>
</Animated.View> */}
{/* ═══ Stage Content ═══ */}
<Animated.View
key={`stage-${stage}`}
entering={FadeInUp.duration(350).delay(100)}
style={[styles.stageCard, shadows.medium]}
>
{stage === 0 && ( {stage === 0 && (
<StageZero <StageZero
formData={formData} formData={formData}
@@ -549,7 +562,7 @@ useEffect(() => {
allMsmeOrgs={allMsmeOrgs} allMsmeOrgs={allMsmeOrgs}
allSocialCategories={allSocialCategories} allSocialCategories={allSocialCategories}
allMSME={allMSME} allMSME={allMSME}
dropdownStyle={dropdownStyle} dropdownStyle={{}}
setSelectedOrg={setSelectedOrg} setSelectedOrg={setSelectedOrg}
selectedOrg={selectedOrg} selectedOrg={selectedOrg}
/> />
@@ -594,8 +607,9 @@ useEffect(() => {
states={states} states={states}
cities={cities} cities={cities}
fetchCity={() => { fetchCity={() => {
fetchCity(Number(formData.state))}} fetchCity(Number(formData.state));
dropdownStyle={dropdownStyle} }}
dropdownStyle={{}}
styles={styles} styles={styles}
/> />
)} )}
@@ -609,10 +623,14 @@ useEffect(() => {
selectedOrg={selectedOrg} selectedOrg={selectedOrg}
/> />
)} )}
</Animated.View>
</View> </View>
{/* ═══ Next / Submit Button ═══ */}
<Animated.View entering={FadeInDown.duration(300).delay(200)}>
<PrimaryButton <PrimaryButton
title={stage === 4 ? (loading ? 'Loading...' : 'Submit') : 'Next'} title={stage === 4 ? (loading ? 'Loading...' : 'Submit') : 'Next'}
style={{ backgroundColor: colors.primary }}
onPress={() => { onPress={() => {
if (validateStage(stage)) { if (validateStage(stage)) {
switch (stage) { switch (stage) {
@@ -657,6 +675,7 @@ useEffect(() => {
} }
}} }}
/> />
</Animated.View>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</View> </View>
@@ -665,96 +684,96 @@ useEffect(() => {
export default Registration; export default Registration;
const styles = StyleSheet.create({ // ── Styles ────────────────────────────────────────────────────────────
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
scrollContainer: { scrollContainer: {
flexGrow: 1, flexGrow: 1,
paddingHorizontal:14 paddingHorizontal: spacing.m,
}, },
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: spacing.s,
},
backBtn: {
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: colors.surface,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
},
stageTitleContainer: {
marginBottom: spacing.m,
paddingHorizontal: spacing.xs,
},
stageTitle: {
fontWeight: '800',
marginBottom: 2,
},
stageCard: {
backgroundColor: colors.surface,
borderRadius: 16,
padding: spacing.m,
},
// These styles will be passed to child stages that still use `styles` prop
label: { label: {
fontSize: 15, fontSize: typography.size.s,
fontWeight: '600', fontWeight: '700',
marginVertical: 10, color: colors.primary,
color: colors.black, marginBottom: spacing.s,
letterSpacing: 0.3,
textTransform: 'uppercase',
}, },
row: { row: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: 10, marginBottom: 10,
}, },
backBtn: {
padding: 5,
width: 50,
marginBottom: 5,
},
sectionTitle: { sectionTitle: {
fontSize: 24, fontSize: typography.size.l,
fontWeight: '700', fontWeight: '700',
color: colors.black, color: colors.text,
marginBottom: 20, marginBottom: spacing.m,
marginTop: 10,
textAlign: 'center', textAlign: 'center',
}, },
uploadGrid: { uploadGrid: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'space-between', justifyContent: 'space-between',
gap: spacing.s,
}, },
uploadCard: { uploadCard: {
width: '48%', width: '48%',
backgroundColor: '#fdfdfd', backgroundColor: colors.surface,
borderRadius: 20, borderRadius: 16,
padding: 20, padding: spacing.m,
alignItems: 'center', alignItems: 'center',
marginBottom: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 6,
elevation: 4,
borderWidth: 1, borderWidth: 1,
borderColor: '#ddd', borderColor: colors.border,
}, },
iconCircle: { iconCircle: {
width: 50, width: 46,
height: 50, height: 46,
borderRadius: 25, borderRadius: 23,
backgroundColor: colors.primary, backgroundColor: colors.primary,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginBottom: 10, marginBottom: spacing.s,
}, },
cardTitle: { cardTitle: {
fontSize: 15, fontSize: typography.size.s,
fontWeight: '600', fontWeight: '600',
color: colors.black, color: colors.text,
marginBottom: 4, marginBottom: 4,
textAlign: 'center', textAlign: 'center',
}, },
cardHint: { cardHint: {
fontSize: 12, fontSize: typography.size.xs,
color: '#888', color: colors.textSecondary,
textAlign: 'center', textAlign: 'center',
}, },
dropdown: {
height: 55,
borderColor: '#e0e0e0',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 14,
marginTop: 8,
marginBottom: 14,
backgroundColor: '#fff',
},
selectedTextStyle: {
fontSize: 14,
color: colors.black,
},
placeholderStyle: {
fontSize: 14,
color: colors.black,
},
itemTextStyle: {
fontSize: 14,
color: colors.black,
},
}); });
+111 -73
View File
@@ -3,24 +3,21 @@ import {
SafeAreaView, SafeAreaView,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text,
TouchableOpacity, TouchableOpacity,
View, View,
Platform, Platform,
} from 'react-native'; } from 'react-native';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import colors from '../../Constant/Color';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton'; import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import MyInput from '../../Component/MyInput'; import MyInput from '../../components/MyInput';
import { showToast } from '../../Constant/ShowDailog'; import { showToast } from '../../Constant/ShowDailog';
import { STAGETHREE } from '../../Services/ApiCall/ApiUrls';
import { useContractorStageThreeSubmit } from '../../Services/ApiCall/Hooks'; import { useContractorStageThreeSubmit } from '../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Trash2, Building2 } from 'lucide-react-native';
type Company = { type Company = {
director: string; director: string;
@@ -32,13 +29,16 @@ type Company = {
type StageThreeRouteParams = { type StageThreeRouteParams = {
orgType: string; orgType: string;
otherId: string | number otherId: string | number;
}; };
const StageThreeScreen = () => { const StageThreeScreen = () => {
const route = useRoute<RouteProp<{ params: StageThreeRouteParams }, 'params'>>(); const route = useRoute<RouteProp<{ params: StageThreeRouteParams }, 'params'>>();
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const insets = useSafeAreaInsets() const { colors, spacing, typography, shadows } = useTheme();
const insets = useSafeAreaInsets();
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
const [company, setCompany] = useState<Company[]>([ const [company, setCompany] = useState<Company[]>([
{ {
director: '', director: '',
@@ -72,7 +72,6 @@ const StageThreeScreen = () => {
]); ]);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
setLoading(true); setLoading(true);
try { try {
@@ -119,140 +118,179 @@ const StageThreeScreen = () => {
); );
}; };
const orgLabel =
route.params?.orgType == '4'
? 'Public'
: route.params?.orgType == '5'
? 'Private'
: route.params?.orgType == '6'
? 'LLP'
: route.params?.orgType;
return ( return (
<View style={{ flex: 1 ,paddingHorizontal:10 }}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<Text style={[styles.heading,{marginTop:insets.top}]}>Stage Three For {route.params?.orgType == '4' ? 'Public' : route.params?.orgType == '5' ? 'Private' : route.params?.orgType == '6' ? 'Limited Liability Partnership (LLP)' : route.params?.orgType} Organization</Text> <View style={[styles.header, { paddingTop: insets.top + spacing.s }]}>
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
{orgLabel} Organization
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
Add directors / shareholders for your organization
</AppText>
</View>
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40} keyboardVerticalOffset={40}
> >
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled"> <ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{company.map((partner, index) => ( {company.map((partner, index) => (
<View style={{ marginBottom: 20 }} key={index}> <Animated.View
<Text style={styles.partnerTitle}>Company {index + 1}</Text> key={index}
entering={FadeInUp.delay(index * 80).springify()}
style={[styles.card, shadows.medium]}
>
<View style={styles.cardHeader}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
Director {index + 1}
</AppText>
{company.length > 1 && (
<TouchableOpacity onPress={() => handleRemove(index)} activeOpacity={0.7}>
<Trash2 size={18} color={colors.error} />
</TouchableOpacity>
)}
</View>
<MyInput <MyInput
icon="user" label="Director Name"
placeholder="Name of Director *" placeholder="Enter Name of Director"
value={partner.director} value={partner.director}
onChangeText={(text: any) => handleChange(index, 'director', text)} onChangeText={(text: any) => handleChange(index, 'director', text)}
onFocus={() => setFocusedField(`director-${index}`)} onFocus={() => setFocusedField(`director-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `director-${index}`} focused={focusedField === `director-${index}`}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="id-card" label="DIN Number"
placeholder="DIN Number in Firm *" placeholder="Enter DIN Number"
value={partner.DINNumber} value={partner.DINNumber}
onChangeText={(text: any) => handleChange(index, 'DINNumber', text)} onChangeText={(text: any) => handleChange(index, 'DINNumber', text)}
onFocus={() => setFocusedField(`DINNumber-${index}`)} onFocus={() => setFocusedField(`DINNumber-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `DINNumber-${index}`} focused={focusedField === `DINNumber-${index}`}
keyboardType="numeric" keyboardType="numeric"
maxlength={8} maxLength={8}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="id-card" label="PAN of Director"
placeholder="PAN of Director *" placeholder="Enter PAN Number"
value={partner.panNumber} value={partner.panNumber}
onChangeText={(text: any) => handleChange(index, 'panNumber', text)} onChangeText={(text: any) => handleChange(index, 'panNumber', text)}
onFocus={() => setFocusedField(`panNumber-${index}`)} onFocus={() => setFocusedField(`panNumber-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `panNumber-${index}`} focused={focusedField === `panNumber-${index}`}
maxlength={12} maxLength={12}
autoCapitalize="characters" autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="credit-card" label="Aadhaar Number"
placeholder="Aadhar Number *" placeholder="Enter 12-digit Aadhaar"
value={partner.aadharNumber} value={partner.aadharNumber}
onChangeText={(text: any) => handleChange(index, 'aadharNumber', text)} onChangeText={(text: any) => handleChange(index, 'aadharNumber', text)}
onFocus={() => setFocusedField(`aadharNumber-${index}`)} onFocus={() => setFocusedField(`aadharNumber-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `aadharNumber-${index}`} focused={focusedField === `aadharNumber-${index}`}
keyboardType="numeric" keyboardType="numeric"
maxlength={12} maxLength={12}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="phone" label="Mobile Number"
placeholder="Mobile Number *" placeholder="Enter Mobile Number"
value={partner.mobileNumber} value={partner.mobileNumber}
onChangeText={(text: any) => handleChange(index, 'mobileNumber', text)} onChangeText={(text: any) => handleChange(index, 'mobileNumber', text)}
onFocus={() => setFocusedField(`mobileNumber-${index}`)} onFocus={() => setFocusedField(`mobileNumber-${index}`)}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === `mobileNumber-${index}`} focused={focusedField === `mobileNumber-${index}`}
keyboardType="phone-pad" keyboardType="phone-pad"
maxlength={10} maxLength={10}
/> />
</Animated.View>
{company.length > 1 && (
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
<Text style={styles.deleteText}>Delete Company</Text>
</TouchableOpacity>
)}
</View>
))} ))}
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}> <TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
<Text style={styles.addText}>+ Add Company</Text> <Building2 size={16} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
Add Director
</AppText>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>
<View style={styles.footer}>
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} /> <PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
</View> </View>
</View>
); );
}; };
export default StageThreeScreen; export default StageThreeScreen;
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
heading: { header: {
fontSize: 18, paddingHorizontal: spacing.m,
fontWeight: 'bold', paddingBottom: spacing.m,
marginBottom: 10, borderBottomWidth: 1,
marginTop: 10, borderBottomColor: colors.border,
textAlign: 'center', backgroundColor: colors.surface,
},
partnerTitle: {
fontWeight: '600',
fontSize: 16,
marginBottom: 10,
}, },
scrollContainer: { scrollContainer: {
flexGrow: 1, flexGrow: 1,
padding: spacing.m,
paddingBottom: 30,
},
card: {
backgroundColor: colors.surface,
borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.s,
paddingBottom: spacing.xs,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
}, },
addButton: { addButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
padding: 10, flexDirection: 'row',
marginTop: 10,
borderRadius: 10,
alignItems: 'center', alignItems: 'center',
width: 140, justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 12,
alignSelf: 'flex-end', alignSelf: 'flex-end',
}, },
addText: { footer: {
color: 'white', padding: spacing.m,
fontWeight: '600', borderTopWidth: 1,
}, borderTopColor: colors.border,
deleteButton: { backgroundColor: colors.surface,
backgroundColor: '#FF5C5C',
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 140,
alignSelf: 'flex-end',
},
deleteText: {
color: 'white',
fontWeight: '600',
}, },
}); });
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native'; import { View, TouchableOpacity } from 'react-native';
import { import {
IdCard, IdCard,
FileText, FileText,
@@ -7,7 +7,12 @@ import {
ShieldCheck, ShieldCheck,
Building2, Building2,
ScrollText, ScrollText,
} from 'lucide-react-native'; // lucide icons CheckCircle,
Upload,
} from 'lucide-react-native';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface DocumentItem { interface DocumentItem {
key: string; key: string;
@@ -30,13 +35,15 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
formData, formData,
selectedOrg, selectedOrg,
}) => { }) => {
const { colors, spacing, shadows } = useTheme();
const documentItems: DocumentItem[] = [ const documentItems: DocumentItem[] = [
{ key: 'panCard', label: 'PAN Card', icon: 'pan' }, { key: 'panCard', label: 'PAN Card', icon: 'pan' },
{ key: 'aadharCard', label: 'Aadhar Card', icon: 'aadhar' }, { key: 'aadharCard', label: 'Aadhar Card', icon: 'aadhar' },
{ key: 'gstnCertificate', label: 'GSTN Certificate', icon: 'gst' }, { key: 'gstnCertificate', label: 'GSTN Certificate', icon: 'gst' },
{ key: 'constractorClass', label: 'Contractor Class File', icon: 'contractor' }, { key: 'constractorClass', label: 'Contractor Class', icon: 'contractor' },
{ key: 'msmefile', label: 'MSME Organization', icon: 'msme' }, { key: 'msmefile', label: 'MSME Organization', icon: 'msme' },
{ key: 'registrationCertificate', label: 'Registration Certificate', icon: 'registration' }, { key: 'registrationCertificate', label: 'Registration Cert.', icon: 'registration' },
]; ];
const shouldRenderItem = (itemKey: string): boolean => { const shouldRenderItem = (itemKey: string): boolean => {
@@ -52,54 +59,84 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
return false; return false;
} }
return true; // pan + aadhar + rest (unless filtered above) return true;
}; };
const renderIcon = (iconType: DocumentItem['icon']) => { const renderIcon = (iconType: DocumentItem['icon']) => {
const commonProps = { size: 26, color: '#fff', strokeWidth: 2 }; const commonProps = { size: 24, color: colors.buttonText, strokeWidth: 2 };
switch (iconType) { switch (iconType) {
case 'pan': case 'pan':
return <IdCard {...commonProps} />; // PAN → ID card feel [web:19] return <IdCard {...commonProps} />;
case 'aadhar': case 'aadhar':
return <IdCard {...commonProps} />; // Same icon, different label return <IdCard {...commonProps} />;
case 'gst': case 'gst':
return <FileBadge {...commonProps} />; // File with badge [web:20] return <FileBadge {...commonProps} />;
case 'contractor': case 'contractor':
return <ShieldCheck {...commonProps} />; // Safety / verification feel return <ShieldCheck {...commonProps} />;
case 'msme': case 'msme':
return <Building2 {...commonProps} />; // Organization / company return <Building2 {...commonProps} />;
case 'registration': case 'registration':
return <ScrollText {...commonProps} />; // Certificate / document scroll return <ScrollText {...commonProps} />;
default: default:
return <FileText {...commonProps} />; return <FileText {...commonProps} />;
} }
}; };
let animIndex = 0;
return ( return (
<> <>
<Text style={styles.sectionTitle}>Upload Required Documents</Text> <AppText variant="h3" color={colors.text} style={{ fontWeight: '700', textAlign: 'center', marginBottom: spacing.m }}>
Upload Documents
</AppText>
<View style={styles.uploadGrid}> <View style={{
flexDirection: 'row',
flexWrap: 'wrap',
}}>
{documentItems.map(item => { {documentItems.map(item => {
if (!shouldRenderItem(item.key)) return null; if (!shouldRenderItem(item.key)) return null;
const isUploaded = !!documents[item.key];
const currentIndex = animIndex++;
return ( return (
<TouchableOpacity <Animated.View
key={item.key} key={item.key}
style={styles.uploadCard} entering={FadeInUp.delay(80 * currentIndex).springify()}
onPress={() => handleDocumentPick(item.key)} style={{ width: '50%', padding: spacing.xs }}
> >
<View style={styles.iconCircle}>{renderIcon(item.icon)}</View> <TouchableOpacity
style={[
styles.uploadCard,
{ width: '100%' },
{
borderColor: isUploaded ? colors.primary : colors.border,
backgroundColor: isUploaded ? `${colors.primary}08` : colors.surface,
},
]}
onPress={() => handleDocumentPick(item.key)}
activeOpacity={0.7}
>
<View style={[styles.iconCircle, isUploaded && { backgroundColor: colors.primary }]}>
{isUploaded ? (
<CheckCircle size={24} color={colors.buttonText} strokeWidth={2.5} />
) : (
renderIcon(item.icon)
)}
</View>
<Text style={styles.cardTitle}>{item.label}</Text> <AppText variant="bodySmall" color={colors.text} style={{ fontWeight: '600', textAlign: 'center', marginBottom: 4 }}>
{item.label}
</AppText>
<Text style={styles.cardHint}> <AppText variant="caption" color={isUploaded ? colors.primary : colors.textSecondary} style={{ textAlign: 'center', fontSize: 10 }}>
{documents[item.key] {isUploaded
? `${documents[item.key]?.name} ✅` ? `${documents[item.key]?.name?.slice(0, 18)}...`
: 'Tap to upload PDF'} : 'Tap to upload PDF'}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
</Animated.View>
); );
})} })}
</View> </View>
@@ -1,11 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Text, View, StyleSheet } from 'react-native'; import { View } from 'react-native';
import DatePicker from 'react-native-date-picker';
import DatePicker from 'react-native-date-picker' import MyInput from '../../../components/MyInput';
import MyInput from '../../../Component/MyInput';
import MyDropdown from '../../../Component/Dropdown/MyDropdown'; import MyDropdown from '../../../Component/Dropdown/MyDropdown';
import colors from '../../../Constant/Color';
import { DropdownOption } from '../types/types'; import { DropdownOption } from '../types/types';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props { interface Props {
formData: { formData: {
@@ -14,7 +14,7 @@ interface Props {
orgType: string; orgType: string;
registrationDetail: string | number; registrationDetail: string | number;
socialCategory: string; socialCategory: string;
expiryDate: string expiryDate: string;
}; };
handleChange: (key: string, value: string) => void; handleChange: (key: string, value: string) => void;
focusedField: string | null; focusedField: string | null;
@@ -26,7 +26,7 @@ interface Props {
allMSME: DropdownOption[]; allMSME: DropdownOption[];
dropdownStyle: object; dropdownStyle: object;
setSelectedOrg: (val: number) => void; setSelectedOrg: (val: number) => void;
selectedOrg: string | number selectedOrg: string | number;
} }
const StageOne: React.FC<Props> = ({ const StageOne: React.FC<Props> = ({
@@ -40,23 +40,24 @@ const StageOne: React.FC<Props> = ({
allSocialCategories, allSocialCategories,
dropdownStyle, dropdownStyle,
setSelectedOrg, setSelectedOrg,
selectedOrg selectedOrg,
}) => { }) => {
console.log("masterdata",allOrganizations,allMsmeOrgs,allSocialCategories,contractorClassItems,selectedOrg !== 8) const { colors, spacing } = useTheme();
const [selectedAllMSME, setSelectedAllMSME] = useState();
const [selectedAllMSME, setSelectedAllMSME] = useState() const [open, setOpen] = useState(false);
const [open, setOpen] = useState(false) const [date, setDate] = useState(new Date());
const [date, setDate] = useState(new Date())
const handleDateConfirm = (selectedDate: Date) => { const handleDateConfirm = (selectedDate: Date) => {
setOpen(false); setOpen(false);
setDate(selectedDate); setDate(selectedDate);
handleChange('expiryDate', selectedDate.toISOString().split('T')[0]); // Format as YYYY-MM-DD handleChange('expiryDate', selectedDate.toISOString().split('T')[0]);
}; };
return ( return (
<View> <View>
<Text style={styles.label}>Registration & Type Details</Text> <AppText variant="caption" color={colors.primary} style={{ fontWeight: '700', letterSpacing: 0.3, textTransform: 'uppercase', marginBottom: spacing.s }}>
Registration & Type Details
</AppText>
<MyDropdown <MyDropdown
label="Type of Organization" label="Type of Organization"
@@ -69,24 +70,26 @@ const StageOne: React.FC<Props> = ({
setSelectedOrg(item.value); setSelectedOrg(item.value);
handleChange('orgType', item.value); handleChange('orgType', item.value);
}} }}
style={dropdownStyle}
onFocus={() => setFocusedField('orgType')} onFocus={() => setFocusedField('orgType')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'orgType'} focusedField={focusedField === 'orgType'}
/> />
{
selectedOrg !== 8 && {selectedOrg !== 8 && (
<MyInput <MyInput
icon="file" label="Registration Number"
placeholder="Registration Number *" placeholder="Enter Registration Number"
value={formData.registrationNumber} value={formData.registrationNumber}
onChangeText={text => handleChange('registrationNumber', text)} onChangeText={text => handleChange('registrationNumber', text)}
onFocus={() => setFocusedField('registrationNumber')} onFocus={() => setFocusedField('registrationNumber')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'registrationNumber'} focused={focusedField === 'registrationNumber'}
autoCapitalize='characters' /> autoCapitalize="characters"
} containerStyle={{ marginBottom: spacing.s }}
{selectedOrg && />
)}
{selectedOrg && (
<MyDropdown <MyDropdown
label="Contractor Class" label="Contractor Class"
data={contractorClassItems} data={contractorClassItems}
@@ -94,7 +97,6 @@ const StageOne: React.FC<Props> = ({
valueField="value" valueField="value"
placeholder="Select Class" placeholder="Select Class"
value={selectedOrg == 8 ? contractorClassItems[0]?.value : formData.contractorClass} value={selectedOrg == 8 ? contractorClassItems[0]?.value : formData.contractorClass}
style={dropdownStyle}
disable={selectedOrg == 8 ? true : false} disable={selectedOrg == 8 ? true : false}
onChange={item => { onChange={item => {
handleChange('contractorClass', item.value); handleChange('contractorClass', item.value);
@@ -102,9 +104,10 @@ const StageOne: React.FC<Props> = ({
onFocus={() => setFocusedField('contractorClass')} onFocus={() => setFocusedField('contractorClass')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'contractorClass'} focusedField={focusedField === 'contractorClass'}
/>} />
)}
{selectedOrg !== 8 && {selectedOrg !== 8 && (
<MyDropdown <MyDropdown
label="Registration Detail (MSME/NSIC etc.)" label="Registration Detail (MSME/NSIC etc.)"
data={allMsmeOrgs} data={allMsmeOrgs}
@@ -113,32 +116,15 @@ const StageOne: React.FC<Props> = ({
placeholder="Select Registration Type" placeholder="Select Registration Type"
value={formData.registrationDetail} value={formData.registrationDetail}
onChange={item => { onChange={item => {
setSelectedAllMSME(item.value) setSelectedAllMSME(item.value);
console.log('first', item) handleChange('registrationDetail', item.value);
handleChange('registrationDetail', item.value)
}} }}
style={dropdownStyle}
onFocus={() => setFocusedField('registrationDetail')} onFocus={() => setFocusedField('registrationDetail')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'registrationDetail'} focusedField={focusedField === 'registrationDetail'}
/>} />
{/* <MyDropdown )}
label="MSME Details"
data={allMSME}
labelField="label"
valueField="value"
placeholder="Select MSME Type"
value={''}
onChange={item => {
setSelectedAllMSME(item.value)
handleChange('allMsme', item.value)
}}
style={dropdownStyle}
onFocus={() => setFocusedField('allMsme')}
onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'allMsme'}
/> */}
<MyDropdown <MyDropdown
label="Social Category" label="Social Category"
data={allSocialCategories} data={allSocialCategories}
@@ -147,25 +133,21 @@ const StageOne: React.FC<Props> = ({
placeholder="Select Social Category" placeholder="Select Social Category"
value={formData.socialCategory} value={formData.socialCategory}
onChange={item => handleChange('socialCategory', item.value)} onChange={item => handleChange('socialCategory', item.value)}
style={dropdownStyle}
onFocus={() => setFocusedField('socialCategory')} onFocus={() => setFocusedField('socialCategory')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'socialCategory'} focusedField={focusedField === 'socialCategory'}
/> />
{
selectedAllMSME !== 3 && selectedOrg !== 8 &&
<>
{selectedAllMSME !== 3 && selectedOrg !== 8 && (
<>
<MyInput <MyInput
icon="calendar-alt" label="Expiry Date"
placeholder="Expiry Date *" placeholder="Select Expiry Date"
value={formData.expiryDate} value={formData.expiryDate}
onChangeText={text => handleChange('expiryDate', text)} onChangeText={text => handleChange('expiryDate', text)}
onFocus={() => setFocusedField('expiryDate')} onFocus={() => setFocusedField('expiryDate')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'expiryDate'} focused={focusedField === 'expiryDate'}
rightIcon='wallet'
editable={true} editable={true}
rightIconPress={() => setOpen(true)} rightIconPress={() => setOpen(true)}
/> />
@@ -175,22 +157,12 @@ const StageOne: React.FC<Props> = ({
date={date} date={date}
onConfirm={handleDateConfirm} onConfirm={handleDateConfirm}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
mode="date" // Ensure you're using date mode mode="date"
/> />
</> </>
} )}
</View> </View>
); );
}; };
const styles = StyleSheet.create({
label: {
fontSize: 16,
fontWeight: '600',
color: colors.black,
marginBottom: 10,
},
});
export default StageOne; export default StageOne;
@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { Text } from 'react-native'; import { View } from 'react-native';
import { DropdownOption } from '../types/types'; import { DropdownOption } from '../types/types';
import MyInput from '../../../Component/MyInput'; import MyInput from '../../../components/MyInput';
import MyDropdown from '../../../Component/Dropdown/MyDropdown'; import MyDropdown from '../../../Component/Dropdown/MyDropdown';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface AddressDetailsSectionProps { interface AddressDetailsSectionProps {
formData: { formData: {
@@ -21,7 +21,7 @@ interface AddressDetailsSectionProps {
cities: DropdownOption[]; cities: DropdownOption[];
fetchCity: (stateValue: number) => void; fetchCity: (stateValue: number) => void;
dropdownStyle?: object; dropdownStyle?: object;
styles?: any; // Optional styles prop for custom styling styles?: any;
} }
const StageThree: React.FC<AddressDetailsSectionProps> = ({ const StageThree: React.FC<AddressDetailsSectionProps> = ({
@@ -32,54 +32,51 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
states, states,
cities, cities,
fetchCity, fetchCity,
styles styles,
}) => { }) => {
const { colors, spacing } = useTheme();
const dropdownStyle = {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: '#fff',
marginBottom: 10,
};
return ( return (
<> <>
<Text style={styles.label}>Address Details</Text> <AppText variant="caption" color={colors.primary} style={styles.label}>
Address Details
</AppText>
<MyInput <MyInput
icon="home" label="Address"
placeholder="Enter Address *" placeholder="Enter Full Address"
value={formData.address} value={formData.address}
onChangeText={text => handleChange('address', text)} onChangeText={text => handleChange('address', text)}
onFocus={() => setFocusedField('address')} onFocus={() => setFocusedField('address')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'address'} focused={focusedField === 'address'}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
maxlength={10} label="Contact Number"
icon="phone" maxLength={10}
placeholder="Contact Number *" placeholder="Enter Contact Number"
value={formData.agentContact2} value={formData.agentContact2}
onChangeText={text => handleChange('agentContact2', text)} onChangeText={text => handleChange('agentContact2', text)}
onFocus={() => setFocusedField('agentContact2')} onFocus={() => setFocusedField('agentContact2')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'agentContact2'} focused={focusedField === 'agentContact2'}
keyboardType="numeric" keyboardType="numeric"
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="lock" label="Pin Code"
placeholder="Pin Code *" placeholder="Enter 6-digit Pin Code"
keyboardType="numeric" keyboardType="numeric"
maxlength={6} maxLength={6}
value={formData.pinCode} value={formData.pinCode}
onChangeText={text => handleChange('pinCode', text)} onChangeText={text => handleChange('pinCode', text)}
onFocus={() => setFocusedField('pinCode')} onFocus={() => setFocusedField('pinCode')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'pinCode'} focused={focusedField === 'pinCode'}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyDropdown <MyDropdown
@@ -94,7 +91,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
handleChange('city', ''); handleChange('city', '');
fetchCity(item.value); fetchCity(item.value);
}} }}
style={dropdownStyle}
onFocus={() => setFocusedField('state')} onFocus={() => setFocusedField('state')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'state'} focusedField={focusedField === 'state'}
@@ -108,7 +104,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
placeholder="Select City" placeholder="Select City"
value={formData.city} value={formData.city}
onChange={item => handleChange('city', item.value)} onChange={item => handleChange('city', item.value)}
style={dropdownStyle}
onFocus={() => setFocusedField('city')} onFocus={() => setFocusedField('city')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focusedField={focusedField === 'city'} focusedField={focusedField === 'city'}
@@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import { import {
Text,
TouchableOpacity, TouchableOpacity,
ActivityIndicator, ActivityIndicator,
View, View,
Alert,
} from 'react-native'; } from 'react-native';
import MyInput from '../../../Component/MyInput'; import MyInput from '../../../components/MyInput';
import { useTheme } from '../../../Hooks/useTheme';
// Adjust the path to your custom input import AppText from '../../../components/AppText';
interface Props { interface Props {
formData: { formData: {
@@ -21,12 +21,10 @@ interface Props {
handleChange: (key: string, value: string) => void; handleChange: (key: string, value: string) => void;
focusedField: string | null; focusedField: string | null;
setFocusedField: (field: string | null | any) => void; setFocusedField: (field: string | null | any) => void;
// PAN
panVerified: boolean; panVerified: boolean;
setPanVerified: (value: boolean) => void; setPanVerified: (value: boolean) => void;
verifyPanHandler: () => void; verifyPanHandler: () => void;
panVerifying: boolean; panVerifying: boolean;
// Aadhaar
aadhaarVerified: boolean; aadhaarVerified: boolean;
setAadhaarVerified: (value: boolean) => void; setAadhaarVerified: (value: boolean) => void;
sendAdharOTPHandler: (aadhaarNumber: string) => void; sendAdharOTPHandler: (aadhaarNumber: string) => void;
@@ -34,13 +32,12 @@ interface Props {
setadharotpsend: (value: boolean) => void; setadharotpsend: (value: boolean) => void;
verifyAdharOtpHandler: () => void; verifyAdharOtpHandler: () => void;
aadhaarVerifying: boolean; aadhaarVerifying: boolean;
// GST
verifyGSTHandler: () => void; verifyGSTHandler: () => void;
gstverifying: boolean; gstverifying: boolean;
color: any; color: any;
styles: any; styles: any;
formatDateInput: (text: string) => string; formatDateInput: (text: string) => string;
selectedOrg:string|number selectedOrg: string | number;
} }
const StageTwo: React.FC<Props> = ({ const StageTwo: React.FC<Props> = ({
@@ -64,61 +61,79 @@ const StageTwo: React.FC<Props> = ({
color, color,
styles, styles,
formatDateInput, formatDateInput,
selectedOrg selectedOrg,
}) => { }) => {
const { colors, spacing, shadows } = useTheme();
const verifyButtonStyle = {
backgroundColor: colors.primary,
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
};
return ( return (
<> <>
{/* ---------- PAN Details ---------- */} {/* ── PAN Details ── */}
<Text style={styles.label}>PAN Details</Text> <AppText variant="caption" color={colors.primary} style={styles.label}>
PAN Details
</AppText>
<MyInput <MyInput
icon="file" label={selectedOrg == 8 ? "PAN Number" : "Organization PAN Number"}
placeholder={selectedOrg == 8 ? "Pan Number *" : "Organization PAN Number *"} placeholder={selectedOrg == 8 ? "Enter PAN Number" : "Enter Organization PAN"}
value={formData.orgPanNumber} value={formData.orgPanNumber}
onChangeText={(text) => { onChangeText={(text) => {
setPanVerified(false); setPanVerified(false);
handleChange('orgPanNumber', text); handleChange('orgPanNumber', text);
}} }}
isVarify={panVerified} isVerify={panVerified}
onFocus={() => setFocusedField('pan')} onFocus={() => setFocusedField('pan')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'pan'} focused={focusedField === 'pan'}
editable={!panVerified} editable={!panVerified}
autoCapitalize="characters" autoCapitalize="characters"
maxlength={10} maxLength={10}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="calendar" label={selectedOrg == 8 ? "Date of Birth" : "Date of Incorporation"}
placeholder={selectedOrg == 8 ? "Date of Birth *" :"Date of Incorporation *"} placeholder="DD/MM/YYYY"
value={formData.dateOfIncorp} value={formData.dateOfIncorp}
onChangeText={(text) => { onChangeText={(text) => {
const formatted = formatDateInput(text); const formatted = formatDateInput(text);
setPanVerified(false); setPanVerified(false);
handleChange('dateOfIncorp', formatted); handleChange('dateOfIncorp', formatted);
}} }}
isVarify={panVerified} isVerify={panVerified}
onFocus={() => setFocusedField('dob')} onFocus={() => setFocusedField('dob')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'dob'} focused={focusedField === 'dob'}
editable={!panVerified} editable={!panVerified}
keyboardType='numeric' keyboardType="numeric"
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="file-signature" label="PAN Name"
placeholder="PAN Name *" placeholder="Enter Name as on PAN"
value={formData.orgPanName} value={formData.orgPanName}
onChangeText={(text) => { onChangeText={(text) => {
setPanVerified(false); setPanVerified(false);
handleChange('orgPanName', text); handleChange('orgPanName', text);
}} }}
isVarify={panVerified} isVerify={panVerified}
onFocus={() => setFocusedField('panName')} onFocus={() => setFocusedField('panName')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'panName'} focused={focusedField === 'panName'}
editable={!panVerified} editable={!panVerified}
autoCapitalize="characters" autoCapitalize="characters"
containerStyle={{ marginBottom: spacing.s }}
/> />
{formData.orgPanNumber && {formData.orgPanNumber &&
@@ -126,32 +141,69 @@ const StageTwo: React.FC<Props> = ({
formData.orgPanName && formData.orgPanName &&
!panVerified && ( !panVerified && (
<TouchableOpacity <TouchableOpacity
style={{ style={verifyButtonStyle}
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
disabled={panVerifying} disabled={panVerifying}
onPress={verifyPanHandler} onPress={verifyPanHandler}
activeOpacity={0.8}
> >
<Text style={{ color: 'white', fontWeight: '600' }}> <AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
{panVerifying ? 'Verifying' : 'Verify PAN'} {panVerifying ? 'Verifying...' : 'Verify PAN'}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
)} )}
{/* ---------- Aadhaar Details ---------- */} {panVerified && (
<Text style={styles.label}>Aadhaar Details</Text> <TouchableOpacity
style={{
borderWidth: 1.5,
borderColor: '#E67E22',
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
backgroundColor: 'transparent',
}}
onPress={() => {
Alert.alert(
'Change PAN',
'Are you sure you want to change your PAN details? You will need to re-verify.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Change',
style: 'destructive',
onPress: () => {
setPanVerified(false);
handleChange('orgPanNumber', '');
handleChange('dateOfIncorp', '');
handleChange('orgPanName', '');
},
},
],
);
}}
activeOpacity={0.8}
>
<AppText variant="bodySmall" color={'#E67E22'} style={{ fontWeight: '700' }}>
Change PAN
</AppText>
</TouchableOpacity>
)}
{/* ── Aadhaar Details ── */}
<View style={{ marginTop: spacing.m, borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.m }}>
<AppText variant="caption" color={colors.primary} style={styles.label}>
Aadhaar Details
</AppText>
<MyInput <MyInput
icon="address-card" label="Aadhaar Number"
placeholder="Aadhaar Number *" placeholder="Enter 12-digit Aadhaar Number"
keyboardType="numeric" keyboardType="numeric"
maxlength={12} maxLength={12}
value={formData.aadhaarNumber} value={formData.aadhaarNumber}
onChangeText={(text) => { onChangeText={(text) => {
handleChange('aadhaarNumber', text); handleChange('aadhaarNumber', text);
@@ -159,31 +211,27 @@ const StageTwo: React.FC<Props> = ({
setadharotpsend(false); setadharotpsend(false);
handleChange('aadhaarOtp', ''); handleChange('aadhaarOtp', '');
}} }}
isVarify={aadhaarVerified} isVerify={aadhaarVerified}
onFocus={() => setFocusedField('adhar')} onFocus={() => setFocusedField('adhar')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'adhar'} focused={focusedField === 'adhar'}
editable={!aadhaarVerified} editable={!aadhaarVerified}
containerStyle={{ marginBottom: spacing.s }}
/> />
{formData.aadhaarNumber.length === 12 && !adharotpsend && ( {formData.aadhaarNumber.length === 12 && !adharotpsend && (
<TouchableOpacity <TouchableOpacity
style={{ style={verifyButtonStyle}
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
onPress={() => sendAdharOTPHandler(formData.aadhaarNumber)} onPress={() => sendAdharOTPHandler(formData.aadhaarNumber)}
disabled={aadhaarVerifying} disabled={aadhaarVerifying}
activeOpacity={0.8}
> >
{aadhaarVerifying ? ( {aadhaarVerifying ? (
<ActivityIndicator color="#fff" /> <ActivityIndicator color={colors.buttonText} size="small" />
) : ( ) : (
<Text style={{ color: 'white', fontWeight: '600' }}>Send OTP</Text> <AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
Send OTP
</AppText>
)} )}
</TouchableOpacity> </TouchableOpacity>
)} )}
@@ -191,43 +239,78 @@ const StageTwo: React.FC<Props> = ({
{adharotpsend && !aadhaarVerified && ( {adharotpsend && !aadhaarVerified && (
<> <>
<MyInput <MyInput
icon="lock" label="OTP"
placeholder="Enter OTP *" placeholder="Enter 6-digit OTP"
keyboardType="numeric" keyboardType="numeric"
maxlength={6} maxLength={6}
value={formData.aadhaarOtp} value={formData.aadhaarOtp}
onChangeText={(text) => handleChange('aadhaarOtp', text)} onChangeText={(text) => handleChange('aadhaarOtp', text)}
onFocus={() => setFocusedField('adharOtp')} onFocus={() => setFocusedField('adharOtp')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'adharOtp'} focused={focusedField === 'adharOtp'}
containerStyle={{ marginBottom: spacing.s }}
/> />
<TouchableOpacity <TouchableOpacity
style={{ style={verifyButtonStyle}
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
onPress={verifyAdharOtpHandler} onPress={verifyAdharOtpHandler}
disabled={aadhaarVerifying} disabled={aadhaarVerifying}
activeOpacity={0.8}
> >
<Text style={{ color: 'white', fontWeight: '600' }}> <AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
{aadhaarVerifying ? 'Verifying...' : 'Verify'} {aadhaarVerifying ? 'Verifying...' : 'Verify OTP'}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
</> </>
)} )}
{/* ---------- GST Section ---------- */} {aadhaarVerified && (
<TouchableOpacity
style={{
borderWidth: 1.5,
borderColor: '#E67E22',
paddingVertical: 10,
paddingHorizontal: 16,
marginTop: spacing.s,
borderRadius: 10,
alignItems: 'center' as const,
alignSelf: 'flex-end' as const,
minWidth: 120,
backgroundColor: 'transparent',
}}
onPress={() => {
Alert.alert(
'Change Aadhaar',
'Are you sure you want to change your Aadhaar details? You will need to re-verify.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Change',
style: 'destructive',
onPress: () => {
setAadhaarVerified(false);
setadharotpsend(false);
handleChange('aadhaarNumber', '');
handleChange('aadhaarOtp', '');
},
},
],
);
}}
activeOpacity={0.8}
>
<AppText variant="bodySmall" color={'#E67E22'} style={{ fontWeight: '700' }}>
Change Aadhaar
</AppText>
</TouchableOpacity>
)}
</View>
{selectedOrg !== 11 && {/* ── GST Section ── */}
<> {selectedOrg !== 11 && (
<View style={{ marginTop: spacing.m, borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.m }}>
<MyInput <MyInput
icon="file-invoice" label="GSTN Number"
placeholder="GSTN No" placeholder="Enter GSTN Number"
value={formData.gstnNo} value={formData.gstnNo}
onChangeText={(text) => handleChange('gstnNo', text)} onChangeText={(text) => handleChange('gstnNo', text)}
onFocus={() => setFocusedField('gstNumber')} onFocus={() => setFocusedField('gstNumber')}
@@ -235,30 +318,10 @@ const StageTwo: React.FC<Props> = ({
focused={focusedField === 'gstNumber'} focused={focusedField === 'gstNumber'}
autoCapitalize="characters" autoCapitalize="characters"
/> />
</View>
{/* {formData?.gstnNo?.length === 15 && ( )}
<TouchableOpacity
style={{
backgroundColor: color.primary,
padding: 10,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
width: 120,
alignSelf: 'flex-end',
}}
onPress={verifyGSTHandler}
>
<Text style={{ color: 'white', fontWeight: '600' }}>
{gstverifying ? 'Verifying...' : 'Verify GST'}
</Text>
</TouchableOpacity>
)} */}
</>}
</> </>
); );
}; };
export default StageTwo; export default StageTwo;
@@ -1,12 +1,12 @@
import React, { useEffect } from 'react'; import React from 'react';
import { Text, View, StyleSheet } from 'react-native'; import { View } from 'react-native';
import MyInput from '../../../Component/MyInput'; import MyInput from '../../../components/MyInput';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
interface Props { interface Props {
formData: { formData: {
agentContact2: string; agentContact2: string;
organizationName: string; organizationName: string;
officialWebsite: string; officialWebsite: string;
@@ -14,7 +14,7 @@ interface Props {
email: string; email: string;
firstName: string; firstName: string;
middleName: string; middleName: string;
lastName: string lastName: string;
}; };
handleChange: (key: string, value: string) => void; handleChange: (key: string, value: string) => void;
focusedField: string | null; focusedField: string | null;
@@ -29,27 +29,29 @@ const StageZero: React.FC<Props> = ({
setFocusedField, setFocusedField,
styles, styles,
}) => { }) => {
const { colors, spacing } = useTheme();
return ( return (
<> <>
<Text style={styles.label}>Personal & Contact Details</Text> <AppText variant="caption" color={colors.primary} style={styles.label}>
{/* <View style={styles.row}> */} Personal & Contact Details
</AppText>
<MyInput <MyInput
icon="user" label="First Name"
placeholder="First Name *" placeholder="First Name"
value={formData.firstName} value={formData.firstName}
onChangeText={(text: string) => handleChange('firstName', text)} onChangeText={(text: string) => handleChange('firstName', text)}
onFocus={() => setFocusedField('firstname')} onFocus={() => setFocusedField('firstname')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'firstname'} focused={focusedField === 'firstname'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
<View style={{ width: 10 }} />
{
formData.middleName ?
{formData.middleName ? (
<MyInput <MyInput
icon="user" label="Middle Name"
placeholder="Middle Name" placeholder="Middle Name"
value={formData.middleName} value={formData.middleName}
onChangeText={(text: string) => handleChange('middleName', text)} onChangeText={(text: string) => handleChange('middleName', text)}
@@ -57,24 +59,25 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'middleName'} focused={focusedField === 'middleName'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
: null} ) : null}
<MyInput <MyInput
icon="user" label="Last Name"
placeholder="Last Name *" placeholder="Last Name"
value={formData.lastName} value={formData.lastName}
onChangeText={(text: string) => handleChange('lastName', text)} onChangeText={(text: string) => handleChange('lastName', text)}
onFocus={() => setFocusedField('lastName')} onFocus={() => setFocusedField('lastName')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'lastName'} focused={focusedField === 'lastName'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="envelope" label="Email"
placeholder="Email *" placeholder="Email Address"
keyboardType="email-address" keyboardType="email-address"
value={formData.email} value={formData.email}
onChangeText={(text: string) => handleChange('email', text)} onChangeText={(text: string) => handleChange('email', text)}
@@ -82,11 +85,12 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'email'} focused={focusedField === 'email'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="phone" label="Mobile Number"
placeholder="Mobile Number *" placeholder="Mobile Number"
keyboardType="phone-pad" keyboardType="phone-pad"
value={formData.mobileNumber} value={formData.mobileNumber}
onChangeText={(text: string) => handleChange('mobileNumber', text)} onChangeText={(text: string) => handleChange('mobileNumber', text)}
@@ -94,22 +98,24 @@ const StageZero: React.FC<Props> = ({
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'mobileNumber'} focused={focusedField === 'mobileNumber'}
editable={false} editable={false}
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="building" label="Organization Name"
placeholder="Organization Name *" placeholder="Enter Organization Name"
value={formData.organizationName} value={formData.organizationName}
onChangeText={(text: string) => handleChange('organizationName', text)} onChangeText={(text: string) => handleChange('organizationName', text)}
onFocus={() => setFocusedField('organizationName')} onFocus={() => setFocusedField('organizationName')}
onBlur={() => setFocusedField(null)} onBlur={() => setFocusedField(null)}
focused={focusedField === 'organizationName'} focused={focusedField === 'organizationName'}
autoCapitalize="none" autoCapitalize="none"
containerStyle={{ marginBottom: spacing.s }}
/> />
<MyInput <MyInput
icon="globe" label="Official Website"
placeholder="Official Website (optional)" placeholder="Website (optional)"
value={formData.officialWebsite} value={formData.officialWebsite}
onChangeText={(text: string) => handleChange('officialWebsite', text)} onChangeText={(text: string) => handleChange('officialWebsite', text)}
onFocus={() => setFocusedField('officialWebsite')} onFocus={() => setFocusedField('officialWebsite')}
+272 -293
View File
@@ -1,9 +1,7 @@
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import {
View, View,
Text,
TextInput,
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
Alert, Alert,
@@ -13,16 +11,18 @@ import {
ScrollView, ScrollView,
ToastAndroid, ToastAndroid,
} from 'react-native'; } from 'react-native';
import colors from '../../Constant/Color'; import { User, Phone, Mail, Lock, CheckSquare, Square, Shield, UserPlus, Contact, Eye, EyeOff } from 'lucide-react-native';
import { User, Phone, Mail, Lock, Eye, EyeOff, ArrowLeft, CheckSquare, Square } from 'lucide-react-native';
import { useGetDesignation, useRegisterContractor } from '../../Services/ApiCall/Hooks'; import { useGetDesignation, useRegisterContractor } from '../../Services/ApiCall/Hooks';
import MyDropdown from '../../Component/Dropdown/MyDropdown'; import MyDropdown from '../../Component/Dropdown/MyDropdown';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton'; import PrimaryButton from '../../components/PrimaryButton';
import MyInput from '../../components/MyInput';
import InsideHeader from '../../Component/Header/InsideHeader';
import TermsAndConditionsModal from '../../Component/TermsAndConditionsModal/TermsAndConditionsModal'; import TermsAndConditionsModal from '../../Component/TermsAndConditionsModal/TermsAndConditionsModal';
import AppText from '../../components/AppText';
import { Formik } from 'formik'; import { Formik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import Animated, { FadeInUp } from 'react-native-reanimated'; import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
import { InsideHeader } from '../../Component/Index'; import { useTheme } from '../../Hooks/useTheme';
export type ContractorFormData = { export type ContractorFormData = {
firstName: string; firstName: string;
@@ -40,7 +40,7 @@ export type ContractorFormData = {
type DesignationOption = { type DesignationOption = {
label: string; label: string;
value: string; // string id from API value: string;
}; };
const SignupSchema = Yup.object().shape({ const SignupSchema = Yup.object().shape({
@@ -48,7 +48,7 @@ const SignupSchema = Yup.object().shape({
firstName: Yup.string().required('First name is required.'), firstName: Yup.string().required('First name is required.'),
middleName: Yup.string().optional(), middleName: Yup.string().optional(),
lastName: Yup.string().required('Last name is required.'), lastName: Yup.string().required('Last name is required.'),
designation: Yup.string().required('Please select a designation.'), // string designation: Yup.string().required('Please select a designation.'),
mobileNumber: Yup.string() mobileNumber: Yup.string()
.matches(/^[6-9]\d{9}$/, 'Invalid mobile number.') .matches(/^[6-9]\d{9}$/, 'Invalid mobile number.')
.required('Mobile number is required.'), .required('Mobile number is required.'),
@@ -65,6 +65,7 @@ const SignupSchema = Yup.object().shape({
}); });
const Signup = () => { const Signup = () => {
const { colors, spacing, typography, shadows } = useTheme();
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const [designation, setDesignation] = useState<DesignationOption[]>([]); const [designation, setDesignation] = useState<DesignationOption[]>([]);
const { data, isLoading: isDesignationLoading } = useGetDesignation(); const { data, isLoading: isDesignationLoading } = useGetDesignation();
@@ -74,6 +75,7 @@ const Signup = () => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
useEffect(() => { useEffect(() => {
const list = (data as any)?.data; const list = (data as any)?.data;
@@ -81,89 +83,15 @@ const Signup = () => {
const formattedDesignation: DesignationOption[] = list.map((item: any) => ({ const formattedDesignation: DesignationOption[] = list.map((item: any) => ({
label: item.designationName, label: item.designationName,
value: String(item.designationid), // e.g. "3" value: String(item.designationid),
})); }));
setDesignation(formattedDesignation); setDesignation(formattedDesignation);
}, [data]); }, [data]);
const dropdownStyle = {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: colors.white,
marginBottom: 10,
};
const renderInputWithIcon = (
icon: 'user' | 'phone' | 'mail',
placeholder: string,
value: string,
onChangeText: (text: string) => void,
onBlur: () => void,
error?: string,
touched?: boolean,
props: object = {},
) => {
const IconComponent = icon === 'user' ? User : icon === 'phone' ? Phone : Mail;
return (
<>
<View style={styles.inputWrapper}>
<IconComponent size={18} color={colors.grayText} style={styles.icon} />
<TextInput
placeholder={placeholder}
style={styles.input}
placeholderTextColor={colors.placeholder}
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
{...props}
/>
</View>
{touched && error && <Text style={styles.errorText}>{error}</Text>}
</>
);
};
const renderPasswordInput = (
placeholder: string,
value: string,
onChangeText: (text: string) => void,
onBlur: () => void,
show: boolean,
setShow: (show: boolean) => void,
error?: string,
touched?: boolean,
) => (
<>
<View style={styles.inputWrapper}>
<Lock size={18} color={colors.grayText} style={styles.icon} />
<TextInput
placeholder={placeholder}
style={styles.input}
placeholderTextColor={colors.placeholder}
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
secureTextEntry={!show}
/>
<TouchableOpacity onPress={() => setShow(!show)}>
{show ? <Eye size={18} color={colors.grayText} /> : <EyeOff size={18} color={colors.grayText} />}
</TouchableOpacity>
</View>
{touched && error && <Text style={styles.errorText}>{error}</Text>}
</>
);
const handleSubmitForm = (values: any) => { const handleSubmitForm = (values: any) => {
console.log('values', values);
if (!isCheck) { if (!isCheck) {
Alert.alert('Check terms and condition'); Alert.alert('Terms Required', 'Please accept the terms and conditions to proceed.');
return; return;
} }
@@ -175,7 +103,7 @@ const Signup = () => {
mobNo: values.mobileNumber.trim(), mobNo: values.mobileNumber.trim(),
emailAdd: values.email.trim(), emailAdd: values.email.trim(),
contractorType: values.bidderType, contractorType: values.bidderType,
designation: values.designation ? Number(values.designation) : 0, // convert string -> number designation: values.designation ? Number(values.designation) : 0,
password: values.password.trim(), password: values.password.trim(),
otherDesignation: null, otherDesignation: null,
conPassword: values.confirmPassword.trim(), conPassword: values.confirmPassword.trim(),
@@ -197,21 +125,21 @@ const Signup = () => {
}; };
return ( return (
<View style={styles.container}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<SafeAreaView style={styles.container}> <SafeAreaView style={{ flex: 1 }}>
<InsideHeader title="Sign Up" showArrow /> <InsideHeader title="Create Account" showArrow />
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.container} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
> >
<Formik <Formik
initialValues={{ initialValues={{
bidderType: '', bidderType: 'Indian',
firstName: '', firstName: '',
middleName: '', middleName: '',
lastName: '', lastName: '',
designation: '', // string, not 0 designation: '',
mobileNumber: '', mobileNumber: '',
email: '', email: '',
password: '', password: '',
@@ -232,174 +160,239 @@ const Signup = () => {
<ScrollView <ScrollView
contentContainerStyle={styles.scrollContainer} contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
bounces={false} showsVerticalScrollIndicator={false}
> >
<Animated.View entering={FadeInUp.duration(450).delay(80)}> {/* ═══ 1. Bidder Type Toggle ═══ */}
<Text style={styles.label}>Bidder Type</Text> <Animated.View entering={FadeInUp.duration(400).delay(100)} style={{ marginBottom: spacing.m }}>
<View style={styles.toggleContainer}> <View style={styles.sectionLabelRow}>
<UserPlus size={14} color={colors.primary} />
<AppText variant="caption" color={colors.primary} style={styles.sectionLabel}>
Bidder Type
</AppText>
</View>
<View style={[styles.toggleContainer, shadows.light]}>
{['Indian', 'Foreign'].map(type => ( {['Indian', 'Foreign'].map(type => (
<TouchableOpacity <TouchableOpacity
key={type} key={type}
style={[ style={[
styles.toggleButton, styles.toggleButton,
values.bidderType === type && styles.selectedToggle, values.bidderType === type && [styles.toggleButtonActive, { backgroundColor: colors.primary }],
]} ]}
onPress={() => setFieldValue('bidderType', type)} onPress={() => setFieldValue('bidderType', type)}
activeOpacity={0.8}
> >
<Text <AppText
style={[ variant="bodySmall"
styles.toggleText, color={values.bidderType === type ? colors.buttonText : colors.textSecondary}
values.bidderType === type && styles.selectedToggleText, style={{ fontWeight: values.bidderType === type ? '700' : '500' }}
]}
> >
{type} {type} Contractor
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
{touched.bidderType && errors.bidderType && ( {touched.bidderType && errors.bidderType && (
<Text style={styles.errorText}>{errors.bidderType}</Text> <AppText variant="caption" color={colors.error} style={styles.errorText}>
{errors.bidderType}
</AppText>
)} )}
</Animated.View> </Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(120)}>
<View style={styles.row}> {/* ═══ 2. Personal Information ═══ */}
<View style={styles.halfInput}> <Animated.View entering={FadeInUp.duration(450).delay(200)} style={[styles.card, shadows.medium]}>
{renderInputWithIcon( <View style={styles.cardHeader}>
'user', <User size={16} color={colors.primary} />
'First Name *', <AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
values.firstName, Personal Information
handleChange('firstName'), </AppText>
() => handleBlur('firstName'),
errors.firstName,
touched.firstName,
)}
</View>
<View style={styles.halfInput}>
{renderInputWithIcon(
'user',
'Middle Name',
values.middleName,
handleChange('middleName'),
() => handleBlur('middleName'),
errors.middleName,
touched.middleName,
)}
</View>
</View> </View>
{renderInputWithIcon( <MyInput
'user', label="First Name"
'Last Name *', placeholder="Enter First Name"
values.lastName, value={values.firstName}
handleChange('lastName'), onChangeText={handleChange('firstName')}
() => handleBlur('lastName'), onBlur={handleBlur('firstName')}
errors.lastName, error={touched.firstName && errors.firstName ? errors.firstName : undefined}
touched.lastName, rightIcon={<User size={18} color={colors.textSecondary} />}
)} containerStyle={{ marginBottom: spacing.s }}
</Animated.View> />
<MyInput
label="Middle Name"
placeholder="Enter Middle Name (Optional)"
value={values.middleName}
onChangeText={handleChange('middleName')}
onBlur={handleBlur('middleName')}
error={touched.middleName && errors.middleName ? errors.middleName : undefined}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Last Name"
placeholder="Enter Last Name"
value={values.lastName}
onChangeText={handleChange('lastName')}
onBlur={handleBlur('lastName')}
error={touched.lastName && errors.lastName ? errors.lastName : undefined}
containerStyle={{ marginBottom: spacing.s }}
/>
<Animated.View entering={FadeInUp.duration(450).delay(160)}>
<Text style={styles.label}>Designation</Text>
<MyDropdown <MyDropdown
label="Select Designation" label="Designation"
labelField="label" labelField="label"
valueField="value" valueField="value"
data={designation} data={designation}
value={values.designation} value={values.designation}
onChange={(item: any) => { onChange={(item: any) => { setFieldValue('designation', item.value); }}
setFieldValue('designation', item.value); placeholder={isDesignationLoading ? 'Loading...' : 'Select Designation'}
}}
placeholder={isDesignationLoading ? 'Loading...' : 'Choose Designation'}
style={dropdownStyle}
/> />
{touched.designation && errors.designation && ( {touched.designation && errors.designation && (
<Text style={styles.errorText}>{errors.designation}</Text> <AppText variant="caption" color={colors.error} style={styles.errorText}>
{errors.designation}
</AppText>
)} )}
</Animated.View> </Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(200)}>
{renderInputWithIcon(
'phone',
'Mobile Number *',
values.mobileNumber,
handleChange('mobileNumber'),
() => handleBlur('mobileNumber'),
errors.mobileNumber,
touched.mobileNumber,
{
keyboardType: 'phone-pad',
maxLength: 10,
},
)}
{renderInputWithIcon( {/* ═══ 3. Contact Details ═══ */}
'mail', <Animated.View entering={FadeInUp.duration(450).delay(300)} style={[styles.card, shadows.medium]}>
'Email *', <View style={styles.cardHeader}>
values.email, <Contact size={16} color={colors.primary} />
handleChange('email'), <AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
() => handleBlur('email'), Contact Details
errors.email, </AppText>
touched.email,
{
keyboardType: 'email-address',
autoCapitalize: 'none',
},
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(240)}>
{renderPasswordInput(
'Password *',
values.password,
handleChange('password'),
() => handleBlur('password'),
showPassword,
setShowPassword,
errors.password,
touched.password,
)}
{renderPasswordInput(
'Confirm Password *',
values.confirmPassword,
handleChange('confirmPassword'),
() => handleBlur('confirmPassword'),
showConfirmPassword,
setShowConfirmPassword,
errors.confirmPassword,
touched.confirmPassword,
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(280)}>
<View style={styles.termsRow}>
<TouchableOpacity onPress={() => setCheckModal(true)}>
{isCheck ? (
<CheckSquare size={20} color={colors.primary} />
) : (
<Square size={20} color={colors.black} />
)}
</TouchableOpacity>
<Text style={styles.termsText}>
By continuing, I agree to the Terms and Conditions
</Text>
</View> </View>
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(320)} style={styles.buttonWrapper}> <MyInput
<PrimaryButton label="Mobile Number"
title={isRegistering ? 'Submitting...' : 'Submit'} placeholder="Enter Mobile Number"
disabled={isRegistering} value={values.mobileNumber}
onPress={() => handleSubmit()} onChangeText={handleChange('mobileNumber')}
onBlur={handleBlur('mobileNumber')}
error={touched.mobileNumber && errors.mobileNumber ? errors.mobileNumber : undefined}
keyboardType="phone-pad"
maxLength={10}
rightIcon={<Phone size={18} color={colors.textSecondary} />}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Email Address"
placeholder="Enter Email Address"
value={values.email}
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
error={touched.email && errors.email ? errors.email : undefined}
keyboardType="email-address"
autoCapitalize="none"
rightIcon={<Mail size={18} color={colors.textSecondary} />}
containerStyle={{ marginBottom: spacing.s }}
/> />
</Animated.View> </Animated.View>
{/* ═══ 4. Security ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(400)} style={[styles.card, shadows.medium]}>
<View style={styles.cardHeader}>
<Shield size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
Security
</AppText>
</View>
<MyInput
label="Password"
placeholder="••••••••"
value={values.password}
onChangeText={handleChange('password')}
onBlur={handleBlur('password')}
secureTextEntry={!showPassword}
error={touched.password && errors.password ? errors.password : undefined}
rightIcon={
showPassword
? <Eye size={18} color={colors.textSecondary} />
: <EyeOff size={18} color={colors.textSecondary} />
}
rightIconPress={() => setShowPassword(prev => !prev)}
containerStyle={{ marginBottom: spacing.s }}
/>
<MyInput
label="Confirm Password"
placeholder="••••••••"
value={values.confirmPassword}
onChangeText={handleChange('confirmPassword')}
onBlur={handleBlur('confirmPassword')}
secureTextEntry={!showConfirmPassword}
error={touched.confirmPassword && errors.confirmPassword ? errors.confirmPassword : undefined}
rightIcon={
showConfirmPassword
? <Eye size={18} color={colors.textSecondary} />
: <EyeOff size={18} color={colors.textSecondary} />
}
rightIconPress={() => setShowConfirmPassword(prev => !prev)}
containerStyle={{ marginBottom: spacing.s }}
/>
</Animated.View>
{/* ═══ Terms & Conditions ═══ */}
<Animated.View entering={FadeInUp.duration(450).delay(500)}>
<TouchableOpacity
style={[styles.termsCard, shadows.light]}
onPress={() => setCheckModal(true)}
activeOpacity={0.8}
>
{isCheck ? (
<CheckSquare size={22} color={colors.primary} strokeWidth={2.5} />
) : (
<Square size={22} color={colors.textSecondary} strokeWidth={2} />
)}
<AppText variant="bodySmall" color={colors.textSecondary} style={{ flex: 1 }}>
I agree to the{' '}
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700', textDecorationLine: 'underline' }}>
Terms and Conditions
</AppText>
</AppText>
</TouchableOpacity>
</Animated.View>
{/* ═══ Submit Button ═══ */}
<Animated.View entering={FadeInDown.duration(400).delay(600)} style={styles.buttonWrapper}>
<PrimaryButton
title="Create Account"
loading={isRegistering}
onPress={handleSubmit}
variant="primary"
/>
</Animated.View>
{/* ═══ Login Link ═══ */}
<Animated.View entering={FadeInUp.duration(400).delay(650)}>
<TouchableOpacity
onPress={() => navigation.pop()}
style={styles.loginRow}
activeOpacity={0.7}
>
<AppText variant="bodySmall" color={colors.textSecondary}>
Already have an account?{' '}
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
Sign In
</AppText>
</AppText>
</TouchableOpacity>
</Animated.View>
{checkModal && ( {checkModal && (
<TermsAndConditionsModal <TermsAndConditionsModal
visible={checkModal} visible={checkModal}
onClose={() => setCheckModal(false)} onClose={() => setCheckModal(false)}
onChecked={() => setIsCheck(true)} onChecked={() => { setIsCheck(true); setCheckModal(false); }}
/> />
)} )}
</ScrollView> </ScrollView>
@@ -413,99 +406,85 @@ const Signup = () => {
export default Signup; export default Signup;
const styles = StyleSheet.create({
container: { // ── Styles ────────────────────────────────────────────────────────────
flex: 1, const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
backgroundColor: colors.white,
},
scrollContainer: { scrollContainer: {
flexGrow: 1, flexGrow: 1,
paddingHorizontal: 16, padding: spacing.m,
paddingTop: 12, paddingBottom: 30,
paddingBottom: 24,
justifyContent: 'flex-start',
}, },
label: { sectionLabelRow: {
fontSize: 15, flexDirection: 'row',
fontWeight: '600', alignItems: 'center',
marginTop: 6, gap: 6,
marginBottom: 4, marginBottom: 6,
color: colors.darkText, },
sectionLabel: {
fontWeight: '700',
fontSize: typography.size.xs,
letterSpacing: 0.5,
textTransform: 'uppercase',
}, },
toggleContainer: { toggleContainer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', borderRadius: 12,
marginBottom: 4, padding: 3,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
height: 46,
}, },
toggleButton: { toggleButton: {
flex: 1, flex: 1,
paddingVertical: 10, justifyContent: 'center',
borderRadius: 10,
borderWidth: 1,
borderColor: colors.primary,
alignItems: 'center', alignItems: 'center',
marginHorizontal: 4, borderRadius: 9,
backgroundColor: colors.white,
}, },
selectedToggle: { toggleButtonActive: {
backgroundColor: colors.primary, elevation: 2,
borderColor: colors.primary,
}, },
toggleText: { card: {
fontSize: 14, backgroundColor: colors.surface,
color: colors.grayText, borderRadius: 14,
padding: 14,
marginBottom: spacing.m,
}, },
selectedToggleText: { cardHeader: {
color: colors.white,
fontWeight: '600',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 6,
},
halfInput: {
width: '48%',
},
inputWrapper: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.white, gap: 8,
borderRadius: 12, marginBottom: spacing.s,
borderWidth: 1, paddingBottom: spacing.xs,
borderColor: colors.border, borderBottomWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 10, borderBottomColor: colors.border,
marginTop: 8,
height: 52,
}, },
icon: { cardTitle: {
marginRight: 10, fontWeight: '700',
}, fontSize: typography.size.s,
input: { letterSpacing: 0.3,
flex: 1,
fontSize: 15,
color: colors.darkText,
paddingVertical: 0,
}, },
errorText: { errorText: {
color: colors.error, marginTop: 4,
fontSize: 12, fontSize: typography.size.xs,
marginTop: 3, fontWeight: '500',
marginLeft: 4,
}, },
termsRow: { termsCard: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginTop: 14, gap: 10,
marginHorizontal: 2, backgroundColor: colors.surface,
}, borderRadius: 12,
termsText: { padding: 14,
flex: 1, marginBottom: spacing.m,
fontSize: 13, borderWidth: 1,
color: colors.darkText, borderColor: colors.border,
marginLeft: 8,
}, },
buttonWrapper: { buttonWrapper: {
marginTop: 18, marginBottom: spacing.s,
},
loginRow: {
alignItems: 'center',
marginBottom: spacing.m,
}, },
}); });
+29 -53
View File
@@ -1,9 +1,8 @@
import React, { useEffect, useRef, useState, useCallback } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native'; import { Animated, StyleSheet, View } from 'react-native';
import { useNavigation, useIsFocused } from '@react-navigation/native'; import { useNavigation, useIsFocused } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useStore } from 'react-redux'; // For hydration check
import { showToast } from '../../Constant/ShowDailog'; import { showToast } from '../../Constant/ShowDailog';
import { useRagistrationState } from '../../Services/ApiCall/Hooks'; import { useRagistrationState } from '../../Services/ApiCall/Hooks';
@@ -24,37 +23,13 @@ const Splash: React.FC = () => {
const isFocused = useIsFocused(); const isFocused = useIsFocused();
const fadeAnim = useRef(new Animated.Value(0)).current; const fadeAnim = useRef(new Animated.Value(0)).current;
const reduxToken = useSelector((state: any) => state.token?.token); const reduxToken = useSelector((state: any) => state.token?.token);
const store = useStore(); const isPersistRehydrated = useSelector((state: any) => state?._persist?.rehydrated);
const { mutateAsync: registrationStateMutate } = useRagistrationState(); const { mutateAsync: registrationStateMutate } = useRagistrationState();
const [isHydrated, setIsHydrated] = useState(false); const hasSessionToken = Boolean(reduxToken && typeof reduxToken === 'object' && reduxToken.token);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { const routeByOrgType = useCallback((params: { otherId: any; orgType: any }) => {
let unsubscribe: any; const orgType = String(params.orgType ?? '');
const checkHydration = () => {
const token = (store.getState() as any).token?.token;
if (token !== undefined) {
setIsHydrated(true);
unsubscribe?.();
}
};
unsubscribe = store.subscribe(checkHydration);
checkHydration(); // Check immediately
return () => unsubscribe?.();
}, []);
const routeByOrgType = (params: { otherId: any; orgType: any }) => {
// const orgType = params.orgType;
const orgType = "7" as string; // For testing purpose
if (orgType === '4' || orgType === '5' || orgType === '6') { if (orgType === '4' || orgType === '5' || orgType === '6') {
navigation.replace(NavigationString.StageThreeScreen, params); navigation.replace(NavigationString.StageThreeScreen, params);
return; return;
} }
@@ -65,18 +40,26 @@ const Splash: React.FC = () => {
} }
navigation.replace('Dashboard'); navigation.replace('Dashboard');
}; }, [navigation]);
const handleGetData = async () => { const handleGetData = useCallback(async () => {
try { try {
const res = await registrationStateMutate(); const res = await registrationStateMutate();
console.log("splash", res);
if ( res?.data?.message==="No Response From Server"&& res?.data?.success===false){ if (res?.status === false) {
navigation.reset({ navigation.reset({
index: 0, index: 0,
routes: [{ name: NavigationString.Login }], routes: [{ name: NavigationString.Login }],
}); });
return return;
}
if (res?.data?.message === 'No Response From Server' && res?.data?.success === false) {
navigation.reset({
index: 0,
routes: [{ name: NavigationString.Login }],
});
return;
} }
if (res?.statusCode === 404 || res?.statusCode === 401) { if (res?.statusCode === 404 || res?.statusCode === 401) {
navigation.replace('Registration'); navigation.replace('Registration');
@@ -86,7 +69,6 @@ const Splash: React.FC = () => {
if (res?.statusCode === 200 && res?.data) { if (res?.statusCode === 200 && res?.data) {
const { registrationStatus, contOtherId, contOrganization } = res.data; const { registrationStatus, contOtherId, contOrganization } = res.data;
const params = { otherId: contOtherId, orgType: contOrganization }; const params = { otherId: contOtherId, orgType: contOrganization };
switch (registrationStatus) { switch (registrationStatus) {
case null: case null:
case undefined: case undefined:
@@ -115,18 +97,19 @@ const Splash: React.FC = () => {
if (error?.response?.status === 404) { if (error?.response?.status === 404) {
navigation.replace('Registration'); navigation.replace('Registration');
} else { } else {
await AsyncStorage.removeItem('token'); await AsyncStorage.removeItem('persist:root');
navigation.replace('Signin'); navigation.replace(NavigationString.Login);
} }
} }
}; }, [navigation, registrationStateMutate, routeByOrgType]);
useEffect(() => { useEffect(() => {
if (!isFocused || !isHydrated) return; if (!isFocused || !isPersistRehydrated) {
return;
}
const runSplash = async () => { const runSplash = async () => {
try { try {
setIsLoading(true);
Animated.timing(fadeAnim, { Animated.timing(fadeAnim, {
toValue: 1, toValue: 1,
duration: 500, duration: 500,
@@ -134,7 +117,7 @@ const Splash: React.FC = () => {
}).start(); }).start();
if (!reduxToken.token) { if (!hasSessionToken) {
navigation.reset({ navigation.reset({
index: 0, index: 0,
routes: [{ name: NavigationString.Login }], routes: [{ name: NavigationString.Login }],
@@ -142,7 +125,7 @@ const Splash: React.FC = () => {
return; return;
} }
if (reduxToken.userType == 'tender') { if (reduxToken.userType === 'tender') {
await handleGetData(); await handleGetData();
} else { } else {
navigation.reset({ navigation.reset({
@@ -154,18 +137,16 @@ const Splash: React.FC = () => {
} catch (error) { } catch (error) {
navigation.reset({ navigation.reset({
index: 0, index: 0,
routes: [{ name: NavigationString.Login || 'Signin' }], routes: [{ name: NavigationString.Login }],
}); });
} finally {
setIsLoading(false);
} }
}; };
const timeoutId = setTimeout(runSplash, 200); const timeoutId = setTimeout(runSplash, 200);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [isFocused, isHydrated]); }, [fadeAnim, handleGetData, hasSessionToken, isFocused, isPersistRehydrated, navigation, reduxToken?.userType]);
if (!isHydrated) { if (!isPersistRehydrated) {
return ( return (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<Animated.Image <Animated.Image
@@ -185,11 +166,6 @@ const Splash: React.FC = () => {
style={[styles.logo, { opacity: fadeAnim }]} style={[styles.logo, { opacity: fadeAnim }]}
resizeMode="contain" resizeMode="contain"
/> />
{isLoading && (
<View style={{ marginTop: 20 }}>
{/* Optional spinner */}
</View>
)}
</View> </View>
</View> </View>
); );
@@ -353,6 +353,7 @@ const EnvelopeCExcelPreview = (props: any) => {
deviceType: 'MOBILE', deviceType: 'MOBILE',
fileName: 'envelopeC_encrypted.xlsx', fileName: 'envelopeC_encrypted.xlsx',
saveEnvelopesOfficersDetailsResDTO: officerEncryptedKeys, saveEnvelopesOfficersDetailsResDTO: officerEncryptedKeys,
randomKeyGenerated: ENCRYPTION_KEY,
}; };
+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;
+32 -10
View File
@@ -1,18 +1,19 @@
import React, { useCallback, useState, useEffect } from 'react'; import React, { useCallback, useState, useEffect } from 'react';
import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native'; import { View, FlatList, ActivityIndicator, RefreshControl, Text, StyleSheet } from 'react-native';
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useFetchLiveAuction, useFetchLiveTender } from '../../Services/ApiCall/Hooks'; import { useFetchLiveAuction, useFetchLiveTender } from '../../Services/ApiCall/Hooks';
import { SearchInput } from '../../Component/Serach/SeacrhInput'; import { SearchInput } from '../../Component/Serach/SeacrhInput';
import { Skelton } from '../../Component/Index'; import { Skelton } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender'; import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
import LiveTenderTile from './LiveTenderTile'; import LiveTenderTile from './LiveTenderTile';
import { useTheme } from '../../Hooks/useTheme'; // Using Theme
const LiveTender = () => { const LiveTender = () => {
const navigation: any = useNavigation(); const navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch()
const { colors } = useTheme(); // Hook
const user = useSelector((state: any) => state?.token.token); const user = useSelector((state: any) => state?.token.token);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchLiveTender(); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchLiveTender();
@@ -68,11 +69,20 @@ const LiveTender = () => {
); );
if (isLoading) { if (isLoading) {
return <Skelton />; return <View style={{ flex: 1, backgroundColor: colors.background }}>
<Skelton />
</View>;
} }
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<SearchInput onChange={() => { }} placeholder="Search...." /> {/* Liquid Background Blobs */}
<View style={styles.blobContainer}>
<View style={[styles.blob, { top: -50, right: -50, backgroundColor: colors.primary, opacity: 0.05 }]} />
<View style={[styles.blob, { top: 200, left: -100, backgroundColor: colors.secondary, opacity: 0.05 }]} />
<View style={[styles.blob, { bottom: -50, right: -20, backgroundColor: colors.primary, opacity: 0.05 }]} />
</View>
<SearchInput onChange={() => { }} placeholder="Search Tenders..." filter={false} />
{showRefreshBanner && ( {showRefreshBanner && (
<Animated.View <Animated.View
@@ -87,7 +97,7 @@ const LiveTender = () => {
paddingHorizontal: 14, paddingHorizontal: 14,
marginTop: 6, marginTop: 6,
borderRadius: 20, borderRadius: 20,
backgroundColor: '#FFFFFF', backgroundColor: colors.surface,
shadowColor: '#000', shadowColor: '#000',
shadowOpacity: 0.06, shadowOpacity: 0.06,
shadowRadius: 8, shadowRadius: 8,
@@ -102,7 +112,7 @@ const LiveTender = () => {
<Text <Text
style={{ style={{
fontSize: 13, fontSize: 13,
color: '#374151', color: colors.text,
fontWeight: '500', fontWeight: '500',
}} }}
> >
@@ -124,7 +134,7 @@ const LiveTender = () => {
<RefreshControl <RefreshControl
refreshing={isRefetching} refreshing={isRefetching}
onRefresh={refetch} onRefresh={refetch}
tintColor="transparent" tintColor={colors.primary}
colors={[colors.primary]} colors={[colors.primary]}
/> />
} }
@@ -133,11 +143,23 @@ const LiveTender = () => {
windowSize={7} windowSize={7}
removeClippedSubviews removeClippedSubviews
updateCellsBatchingPeriod={50} updateCellsBatchingPeriod={50}
onRefresh={refetch}
refreshing={isRefetching}
/> />
</View> </View>
); );
}; };
const styles = StyleSheet.create({
blobContainer: {
...StyleSheet.absoluteFillObject,
zIndex: -1,
overflow: 'hidden',
},
blob: {
position: 'absolute',
width: 300,
height: 300,
borderRadius: 150,
},
});
export default LiveTender; export default LiveTender;
+216 -192
View File
@@ -1,20 +1,15 @@
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
import { import {
Alert, Alert,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text,
TouchableOpacity, TouchableOpacity,
View, View,
StatusBar,
SafeAreaView,
} from 'react-native'; } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native'; import { useRoute, useNavigation } from '@react-navigation/native';
import { import {
useFetchLiveTenderById, useFetchLiveTenderById,
} from '../../Services/ApiCall/Hooks'; } from '../../Services/ApiCall/Hooks';
import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import ReactNativeBlobUtil from 'react-native-blob-util'; import ReactNativeBlobUtil from 'react-native-blob-util';
import { GET_DOCID } from '../../Services/ApiCall/ApiUrls'; import { GET_DOCID } from '../../Services/ApiCall/ApiUrls';
import { import {
@@ -27,20 +22,23 @@ import {
} from 'lucide-react-native'; } from 'lucide-react-native';
import moment from 'moment'; import moment from 'moment';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { showSuccessAlert, showToast } from '../../Constant/ShowDailog'; import { showToast } from '../../Constant/ShowDailog';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
const InfoRow = ({ label, value }: any) => (
const InfoRow = ({ label, value, styles }: any) => (
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.label}>{label}</Text> <AppText variant="caption" style={styles.label}>{label}</AppText>
<Text style={styles.value}>{value || '-'}</Text> <AppText variant="bodySmall" style={styles.value}>{value || '-'}</AppText>
</View> </View>
); );
const Section = ({ title, icon: Icon, children }: any) => ( const Section = ({ title, icon: Icon, children, styles, colors }: any) => (
<View style={styles.sectionContainer}> <View style={styles.sectionContainer}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<Icon size={18} color={colors.primary} /> <Icon size={18} color={colors.primary} />
<Text style={styles.sectionTitle}>{title}</Text> <AppText variant="body" style={styles.sectionTitle}>{title}</AppText>
</View> </View>
{children} {children}
</View> </View>
@@ -48,11 +46,156 @@ const Section = ({ title, icon: Icon, children }: any) => (
const LiveTenderDetail = () => { const LiveTenderDetail = () => {
const route = useRoute<any>(); const route = useRoute<any>();
const { colors, spacing, shadows, isDarkMode } = useTheme();
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const tender = route?.params; const tender = route?.params;
const { data } = useFetchLiveTenderById(tender?.tenderId); const { data } = useFetchLiveTenderById(tender?.tenderId);
const [downloadingId, setDownloadingId] = useState<string | number | null>(null); const [downloadingId, setDownloadingId] = useState<string | number | null>(null);
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const styles = useMemo(() => StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: colors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 10,
borderBottomWidth: 1,
borderBottomColor: colors.border,
backgroundColor: colors.surface,
paddingVertical: 12,
},
backBtn: {
padding: 8,
marginRight: 8,
},
headerTitle: {
fontSize: 18,
fontWeight: '800',
color: colors.text,
},
headerSub: {
fontSize: 13,
color: colors.textSecondary,
},
liveBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.background,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
liveText: {
fontSize: 11,
fontWeight: '800',
color: colors.textSecondary,
},
content: {
padding: 16,
paddingBottom: 32,
},
sectionContainer: {
backgroundColor: colors.surface,
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: colors.border,
...shadows.light,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 12,
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: colors.text,
textTransform: 'uppercase',
letterSpacing: 0.4,
},
card: {
backgroundColor: colors.surface,
borderRadius: 12,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
paddingVertical: 8,
borderBottomWidth: 0.5,
borderBottomColor: colors.border,
},
label: {
fontSize: 13,
color: colors.textSecondary,
flex: 1,
paddingRight: 8,
fontWeight: '600',
},
value: {
fontSize: 13,
color: colors.text,
flex: 1,
textAlign: 'right',
fontWeight: '500',
},
documentRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
gap: 10,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
docInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1.4,
},
fileIcon: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: colors.primary + '20', // Opacity
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
docName: {
flex: 1,
color: colors.text,
fontSize: 13,
fontWeight: '600',
},
downloadBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
downloadText: {
fontSize: 13,
fontWeight: '700',
color: colors.primary,
},
}), [colors, shadows]);
if (!data?.data?.[0]) return null; if (!data?.data?.[0]) return null;
const tenderData = data.data[0]; const tenderData = data.data[0];
@@ -77,7 +220,7 @@ const LiveTenderDetail = () => {
}).fetch('GET', downloadUrl, { }).fetch('GET', downloadUrl, {
Accept: 'application/pdf', Accept: 'application/pdf',
}); });
const stat = await ReactNativeBlobUtil.fs.stat(path); // const stat = await ReactNativeBlobUtil.fs.stat(path);
showToast('Download complete'); showToast('Download complete');
setDownloadingId(null); setDownloadingId(null);
} catch (error: any) { } catch (error: any) {
@@ -98,193 +241,214 @@ const LiveTenderDetail = () => {
}; };
return ( return (
<View style={styles.safeArea}> <View style={[styles.safeArea, { backgroundColor: colors.background }]}>
<StatusBar barStyle="dark-content" />
<View style={[styles.header, { marginTop: insets.top }]}> <View style={[styles.header, { marginTop: insets.top }]}>
<TouchableOpacity <TouchableOpacity
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
style={styles.backBtn} style={styles.backBtn}
> >
<ArrowLeft size={24} color="#1e293b" /> <ArrowLeft size={24} color={colors.text} />
</TouchableOpacity> </TouchableOpacity>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.headerTitle}>Tender Overview</Text> <AppText variant="body" style={styles.headerTitle}>Tender Overview</AppText>
<Text style={styles.headerSub}> <AppText variant="caption" style={styles.headerSub}>
Ref: {tenderData?.tenderNumber || '-'} Ref: {tenderData?.tenderNumber || '-'}
</Text> </AppText>
</View> </View>
<View style={styles.liveBox}> <View style={styles.liveBox}>
<View <View
style={[ style={[
styles.dot, styles.dot,
{ backgroundColor: isLive ? '#22c55e' : '#94a3b8' }, { backgroundColor: isLive ? colors.success : colors.textSecondary },
]} ]}
/> />
<Text style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</Text> <AppText variant="bodySmall" style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</AppText>
</View> </View>
</View> </View>
<ScrollView contentContainerStyle={styles.content}> <ScrollView contentContainerStyle={styles.content}>
<Section title="Basic Details" icon={Layers}> <Section title="Basic Details" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}> <View style={styles.card}>
<InfoRow label="Tender ID" value={tenderData?.tenderId} /> <InfoRow label="Tender ID" value={tenderData?.tenderId} styles={styles} />
<InfoRow label="Tender Number" value={tenderData?.tenderNumber} /> <InfoRow label="Tender Number" value={tenderData?.tenderNumber} styles={styles} />
<InfoRow label="NIT Number" value={tenderData?.nitNumber} /> <InfoRow label="NIT Number" value={tenderData?.nitNumber} styles={styles} />
<InfoRow label="NIT Date" value={tenderData?.nitDate} /> <InfoRow label="NIT Date" value={tenderData?.nitDate} styles={styles} />
<InfoRow label="Name of Work" value={tenderData?.nameOfWork} /> <InfoRow label="Name of Work" value={tenderData?.nameOfWork} styles={styles} />
<InfoRow label="Description" value={tenderData?.descriptionOfWork} /> <InfoRow label="Description" value={tenderData?.descriptionOfWork} styles={styles} />
<InfoRow label="Tender Stage" value={tenderData?.tenderStage} /> <InfoRow label="Tender Stage" value={tenderData?.tenderStage} styles={styles} />
<InfoRow label="Tender Status" value={tenderData?.tenderStatus} /> <InfoRow label="Tender Status" value={tenderData?.tenderStatus} styles={styles} />
</View> </View>
</Section> </Section>
<Section title="Category & Type" icon={Briefcase}> <Section title="Category & Type" icon={Briefcase} styles={styles} colors={colors}>
<View style={styles.card}> <View style={styles.card}>
<InfoRow label="Tender Category" value={tenderData?.tenderCategory} /> <InfoRow label="Tender Category" value={tenderData?.tenderCategory} styles={styles} />
<InfoRow label="Type of Tender" value={tenderData?.typeOfTender} /> <InfoRow label="Type of Tender" value={tenderData?.typeOfTender} styles={styles} />
<InfoRow label="Selective Tender" value={tenderData?.selectiveTender} /> <InfoRow label="Selective Tender" value={tenderData?.selectiveTender} styles={styles} />
<InfoRow label="Area of Work" value={tenderData?.areaOfWork} /> <InfoRow label="Area of Work" value={tenderData?.areaOfWork} styles={styles} />
</View> </View>
</Section> </Section>
<Section title="Financial Details" icon={FileText}> <Section title="Financial Details" icon={FileText} styles={styles} colors={colors}>
<View style={styles.card}> <View style={styles.card}>
<InfoRow <InfoRow
label="Estimated Value" label="Estimated Value"
value={tenderData?.estimateValueInFig ? `₹ ${tenderData.estimateValueInFig}` : '-'} value={tenderData?.estimateValueInFig ? `₹ ${tenderData.estimateValueInFig}` : '-'}
styles={styles}
/> />
<InfoRow <InfoRow
label="Estimated Value (Words)" label="Estimated Value (Words)"
value={tenderData?.estimateValueInWord} value={tenderData?.estimateValueInWord}
styles={styles}
/> />
<InfoRow <InfoRow
label="Tender Fee" label="Tender Fee"
value={tenderData?.tenderFeeInFig ? `₹ ${tenderData.tenderFeeInFig}` : '-'} value={tenderData?.tenderFeeInFig ? `₹ ${tenderData.tenderFeeInFig}` : '-'}
styles={styles}
/> />
<InfoRow <InfoRow
label="EMD" label="EMD"
value={tenderData?.emdInFig ? `₹ ${tenderData.emdInFig}` : '-'} value={tenderData?.emdInFig ? `₹ ${tenderData.emdInFig}` : '-'}
styles={styles}
/> />
<InfoRow label="GST %" value={tenderData?.gst} /> <InfoRow label="GST %" value={tenderData?.gst} styles={styles} />
<InfoRow label="GST Amount" value={tenderData?.gstAmmount} /> <InfoRow label="GST Amount" value={tenderData?.gstAmmount} styles={styles} />
</View> </View>
</Section> </Section>
<Section title="Work & Validity" icon={Briefcase}> <Section title="Work & Validity" icon={Briefcase} styles={styles} colors={colors}>
<View style={styles.card}> <View style={styles.card}>
<InfoRow <InfoRow
label="Bid Validity Type" label="Bid Validity Type"
value={tenderData?.bidValidateType} value={tenderData?.bidValidateType}
styles={styles}
/> />
<InfoRow <InfoRow
label="Bid Validity Period" label="Bid Validity Period"
value={tenderData?.bidValidatePeriod} value={tenderData?.bidValidatePeriod}
styles={styles}
/> />
<InfoRow <InfoRow
label="Work Completion Type" label="Work Completion Type"
value={tenderData?.workCompletionType} value={tenderData?.workCompletionType}
styles={styles}
/> />
<InfoRow <InfoRow
label="Work Completion Period" label="Work Completion Period"
value={tenderData?.workCompletionPeriod} value={tenderData?.workCompletionPeriod}
styles={styles}
/> />
<InfoRow label="Rainy Season" value={tenderData?.rainySeason} /> <InfoRow label="Rainy Season" value={tenderData?.rainySeason} styles={styles} />
</View> </View>
</Section> </Section>
<Section title="Key Dates" icon={Calendar}> <Section title="Key Dates" icon={Calendar} styles={styles} colors={colors}>
<View style={styles.card}> <View style={styles.card}>
<InfoRow <InfoRow
label="Purchase Start" label="Purchase Start"
value={formatDateTime( value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.startDateTime : tenderData?.keydates?.tenderPurchaseDate?.startDateTime tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.startDateTime : tenderData?.keydates?.tenderPurchaseDate?.startDateTime
)} )}
styles={styles}
/> />
<InfoRow <InfoRow
label="Purchase End" label="Purchase End"
value={formatDateTime( value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.endDateTime : tenderData?.keydates?.tenderPurchaseDate?.endDateTime tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.endDateTime : tenderData?.keydates?.tenderPurchaseDate?.endDateTime
)} )}
styles={styles}
/> />
<InfoRow <InfoRow
label="Bid Submission Start" label="Bid Submission Start"
value={formatDateTime( value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.startDateTime : tenderData?.keydates?.bidSubmissionDate?.startDateTime tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.startDateTime : tenderData?.keydates?.bidSubmissionDate?.startDateTime
)} )}
styles={styles}
/> />
<InfoRow <InfoRow
label="Bid Submission End" label="Bid Submission End"
value={formatDateTime( value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.endDateTime : tenderData?.keydates?.bidSubmissionDate?.endDateTime, tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.endDateTime : tenderData?.keydates?.bidSubmissionDate?.endDateTime,
)} )}
styles={styles}
/> />
<InfoRow <InfoRow
label="Opening Envelope A" label="Opening Envelope A"
value={formatDateTime( value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEA : tenderData?.keydates?.openingEA tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEA : tenderData?.keydates?.openingEA
)} )}
styles={styles}
/> />
<InfoRow <InfoRow
label="Opening Envelope B" label="Opening Envelope B"
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEB : tenderData?.keydates?.openingEB)} value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEB : tenderData?.keydates?.openingEB)}
styles={styles}
/> />
<InfoRow <InfoRow
label="Opening Envelope C" label="Opening Envelope C"
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEC : tenderData?.keydates?.openingEC)} value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEC : tenderData?.keydates?.openingEC)}
styles={styles}
/> />
</View> </View>
</Section> </Section>
<Section title="Envelope Configuration" icon={Layers}> <Section title="Envelope Configuration" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}> <View style={styles.card}>
<InfoRow <InfoRow
label="Envelope Type" label="Envelope Type"
value={tenderData?.envelopeType} value={tenderData?.envelopeType}
styles={styles}
/> />
<InfoRow <InfoRow
label="Envelope A Enabled" label="Envelope A Enabled"
value={tenderData?.envelopeA ? 'Yes' : 'No'} value={tenderData?.envelopeA ? 'Yes' : 'No'}
styles={styles}
/> />
<InfoRow <InfoRow
label="Envelope B Enabled" label="Envelope B Enabled"
value={tenderData?.envelopeB ? 'Yes' : 'No'} value={tenderData?.envelopeB ? 'Yes' : 'No'}
styles={styles}
/> />
<InfoRow <InfoRow
label="Envelope C Enabled" label="Envelope C Enabled"
value={tenderData?.envelopeC ? 'Yes' : 'No'} value={tenderData?.envelopeC ? 'Yes' : 'No'}
styles={styles}
/> />
</View> </View>
</Section> </Section>
<Section title="Department & Location" icon={Layers}> <Section title="Department & Location" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}> <View style={styles.card}>
<InfoRow label="Department" value={tenderData?.deptName} /> <InfoRow label="Department" value={tenderData?.deptName} styles={styles} />
<InfoRow label="Circle" value={tenderData?.circle} /> <InfoRow label="Circle" value={tenderData?.circle} styles={styles} />
<InfoRow <InfoRow
label="Division" label="Division"
value={tenderData?.tenderSelectedDivision} value={tenderData?.tenderSelectedDivision}
styles={styles}
/> />
<InfoRow <InfoRow
label="Sub Division" label="Sub Division"
value={tenderData?.tenderSelectedSubDivision} value={tenderData?.tenderSelectedSubDivision}
styles={styles}
/> />
</View> </View>
</Section> </Section>
<Section title="Officers" icon={Layers}> <Section title="Officers" icon={Layers} styles={styles} colors={colors}>
<View style={styles.card}> <View style={styles.card}>
<InfoRow label="Tender Master" value={tenderData?.tenderMaster} /> <InfoRow label="Tender Master" value={tenderData?.tenderMaster} styles={styles} />
{tenderData?.approvalOfficerNameEmail?.map((o: any, i: number) => ( {tenderData?.approvalOfficerNameEmail?.map((o: any, i: number) => (
<InfoRow <InfoRow
key={i} key={i}
label={`Approval Officer ${i + 1}`} label={`Approval Officer ${i + 1}`}
value={`${o.name} (${o.email})`} value={`${o.name} (${o.email})`}
styles={styles}
/> />
))} ))}
</View> </View>
</Section> </Section>
<Section title="Documents" icon={Download}> <Section title="Documents" icon={Download} styles={styles} colors={colors}>
<View style={styles.card}> <View style={styles.card}>
{tenderData?.tenderDocuments?.map((doc: any, i: number) => ( {tenderData?.tenderDocuments?.map((doc: any, i: number) => (
<View key={i} style={styles.documentRow}> <View key={i} style={styles.documentRow}>
@@ -292,7 +456,7 @@ const LiveTenderDetail = () => {
<View style={styles.fileIcon}> <View style={styles.fileIcon}>
<FileText size={18} color={colors.primary} /> <FileText size={18} color={colors.primary} />
</View> </View>
<Text style={styles.docName}>{doc.fileName}</Text> <AppText variant="bodySmall" style={styles.docName}>{doc.fileName}</AppText>
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.downloadBtn} style={styles.downloadBtn}
@@ -302,12 +466,12 @@ const LiveTenderDetail = () => {
<Download <Download
size={18} size={18}
color={ color={
downloadingId === doc?.docId ? '#9ca3af' : colors.primary downloadingId === doc?.docId ? colors.textSecondary : colors.primary
} }
/> />
<Text style={styles.downloadText}> <AppText variant="bodySmall" style={styles.downloadText}>
{downloadingId === doc?.docId ? 'Downloading...' : 'Download'} {downloadingId === doc?.docId ? 'Downloading...' : 'Download'}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
))} ))}
@@ -319,143 +483,3 @@ const LiveTenderDetail = () => {
}; };
export default LiveTenderDetail; export default LiveTenderDetail;
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8fafc',
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal:10,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
backgroundColor: '#fff',
},
backBtn: {
padding: 8,
marginRight: 8,
},
headerTitle: {
fontSize: 18,
fontWeight: '800',
color: '#0f172a',
},
headerSub: {
fontSize: 13,
color: '#64748b',
},
liveBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
borderWidth: 1,
borderColor: '#e2e8f0',
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
liveText: {
fontSize: 11,
fontWeight: '800',
color: '#475569',
},
content: {
padding: 16,
paddingBottom: 32,
},
sectionContainer: {
backgroundColor: '#ffffff',
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: '#e2e8f0',
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 12,
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: '#f8fafc',
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: '#1e293b',
textTransform: 'uppercase',
letterSpacing: 0.4,
},
card: {
backgroundColor: '#ffffff',
borderRadius: 12,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
paddingVertical: 8,
borderBottomWidth: 0.5,
borderBottomColor: '#e5e7eb',
},
label: {
fontSize: 13,
color: '#64748b',
flex: 1,
paddingRight: 8,
fontWeight: '600',
},
value: {
fontSize: 13,
color: '#111827',
flex: 1,
textAlign: 'right',
fontWeight: '500',
},
documentRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
gap: 10,
borderBottomWidth: 1,
borderBottomColor: '#f1f5f9',
},
docInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1.4,
},
fileIcon: {
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: '#e0f2fe',
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
docName: {
flex: 1,
color: '#111827',
fontSize: 13,
fontWeight: '600',
},
downloadBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
downloadText: {
fontSize: 13,
fontWeight: '700',
color: colors.primary,
},
});
+149 -157
View File
@@ -1,13 +1,16 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import colors from '../../Constant/Color';
import { LiveTenderTileProps } from './types/types'; import { LiveTenderTileProps } from './types/types';
import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native'; import { Calendar, Tag, Info, ArrowRight, Clock, MapPin } from 'lucide-react-native';
import TenderDetail from './Modal/TenderFullDetail';
import TenderFullDetail from './Modal/TenderFullDetail';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { format } from '../../Helper/DateFormat'; import { format } from '../../Helper/DateFormat';
import { useTheme } from '../../Hooks/useTheme';
import LinearGradient from 'react-native-linear-gradient';
import AppText from '../../components/AppText';
import NavigationString from '../../Constant/NavigationString';
const { width } = Dimensions.get('window');
const LiveTenderTile: React.FC<LiveTenderTileProps> = ({ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
contractorId, contractorId,
@@ -21,6 +24,8 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
tenderNumber, tenderNumber,
}) => { }) => {
const navigation: any = useNavigation(); const navigation: any = useNavigation();
const { colors, shadows, spacing } = useTheme();
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss')); const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss'));
const contractorIds = typeof data?.contractorId === 'string' const contractorIds = typeof data?.contractorId === 'string'
@@ -30,227 +35,214 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
: []; : [];
const purchased = contractorIds.includes(String(contractorId)); const purchased = contractorIds.includes(String(contractorId));
if(data.tenderStatus==="Amended"){
} const getStatusColor = () => {
if (isPurchased) return colors.error;
if (purchaseEnded) return colors.textSecondary;
if (purchased) return colors.textSecondary;
return colors.success;
};
const statusColor = getStatusColor();
return ( return (
<View style={styles.cardWrapper}>
{/* Status Tag */}
<View
style={[
styles.statusTag,
{ backgroundColor: isPurchased ? colors.error : purchaseEnded ? '#94a3b8' : (purchased ? '#94a3b8' : '#4ade80') },
]}
/>
<TouchableOpacity <TouchableOpacity
style={styles.container}
activeOpacity={0.9} activeOpacity={0.9}
onPress={() => navigation.navigate('LiveTenderDetail', data)} onPress={() => navigation.navigate('LiveTenderDetail', data)}
style={[styles.container, {
backgroundColor: colors.surface,
shadowColor: colors.shadow,
borderColor: colors.border
}]}
> >
{/* Header */}
<View style={styles.header}> <View style={styles.contentContainer}>
<View style={styles.badge}> {/* Header Section */}
<View style={styles.headerRow}>
<View style={[styles.idTag, { backgroundColor: colors.background }]}>
<Tag size={14} color={colors.primary} /> <Tag size={14} color={colors.primary} />
<Text style={styles.badgeText}>#{tenderNumber}</Text> <AppText variant="caption" style={{ color: colors.primary, fontWeight: '700' }}>
</View> #{tenderNumber}
<Text style={styles.priceText}>₹{tenderFees}</Text> </AppText>
</View> </View>
{/* Work Title */} <AppText variant="body" style={{ color: colors.primary }}>
<Text style={styles.workTitle} numberOfLines={2}> ₹{tenderFees}
</AppText>
</View>
{/* Title Section */}
<AppText
variant="body"
style={[styles.title, { color: colors.text }]}
numberOfLines={2}
>
{tenderOfWork ?? 'General Tender Work'} {tenderOfWork ?? 'General Tender Work'}
</Text> </AppText>
{/* Info Grid */} {/* Info Grid */}
<View style={styles.infoGrid}> <View style={[styles.infoContainer, { backgroundColor: colors.background }]}>
<View style={styles.infoItem}> <View style={styles.infoRow}>
<Calendar size={16} color="#64748b" /> <View style={styles.infoCol}>
<View style={styles.infoTextGroup}> <View style={styles.iconBox}>
<Text style={styles.infoLabel}>Start</Text> <Calendar size={16} color={colors.textSecondary} />
<Text style={styles.infoValue}>{format.date6(`${tenderDispatchDate}`)}</Text> </View>
<View>
<AppText variant="caption" style={{ color: colors.textSecondary }}>Start Date</AppText>
<AppText variant='caption' style={{ color: colors.text, fontWeight: '600' }}>
{format.date6(`${tenderDispatchDate}`)}
</AppText>
</View> </View>
</View> </View>
<View style={styles.infoItem}> <View style={styles.divider} />
<Calendar size={16} color={purchaseEnded ? "#ef4444" : "#22c55e"} />
<View style={styles.infoTextGroup}> <View style={[styles.infoCol, { alignItems: 'flex-end' }]}>
<Text style={styles.infoLabel}>Deadline</Text> <View style={styles.iconBox}>
<Text style={[styles.infoValue, purchaseEnded && { color: '#ef4444' }]}> <Clock size={16} color={purchaseEnded ? colors.error : colors.success} />
</View>
<View style={{ alignItems: 'flex-end' }}>
<AppText variant="caption" style={{ color: colors.textSecondary }}>Deadline</AppText>
<AppText variant="caption" style={{ color: purchaseEnded ? colors.error : colors.text, fontWeight: '600' }}>
{moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss').format('DD MMM, YY')} {moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss').format('DD MMM, YY')}
</Text> </AppText>
</View>
</View> </View>
</View> </View>
</View> </View>
{/* Action Bar */} {/* Actions Section */}
<View style={styles.actionBar}> <View style={styles.actionRow}>
<TouchableOpacity style={styles.iconBtn} onPress={() => navigation.navigate('LiveTenderDetail', data)}> <TouchableOpacity
<Info size={18} color={colors.primary} /> style={[styles.detailsBtn, { borderColor: colors.border }]}
<Text style={styles.iconBtnText}>Details</Text> onPress={() => navigation.navigate('LiveTenderDetail', data)}
>
<AppText variant="bodySmall" style={{ color: colors.textSecondary, fontWeight: '600' }}>
View Details
</AppText>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[
styles.mainButton,
purchaseEnded
? styles.btnDisabled
: (purchased ? styles.btnDisabled : isPurchased
? styles.btnPurchased
: styles.btnActive),
]}
onPress={onPurchasePress}
disabled={purchaseEnded || purchased} disabled={purchaseEnded || purchased}
onPress={isPurchased ? () => navigation.navigate(NavigationString.PurchaseTenderList) : onPurchasePress}
style={styles.actionBtnWrapper}
> >
<Text <View
style={[ style={[
styles.mainButtonText, styles.actionBtn,
isPurchased && !purchaseEnded ? { color: colors.white } : { color: '#fff' }, {
backgroundColor: (purchaseEnded || purchased)
? colors.textSecondary
: isPurchased
? colors.success // Green for Go to Cart
: colors.primary // Orange for Add
}
]} ]}
> >
{purchaseEnded ? (purchased ? 'Purchased' : 'Expired') : (purchased ? 'Purchased' : isPurchased ? 'Remove' : 'Add to Cart')} <AppText variant="button" style={{ color: '#fff', fontSize: 12 }}>
</Text> {purchaseEnded ? (purchased ? 'Purchased' : 'Closed') : (purchased ? 'Purchased' : isPurchased ? 'GO TO CART' : 'Add to Cart')}
{(!purchaseEnded && !purchased) && ( </AppText>
<ArrowRight size={16} color={isPurchased ? colors.white : '#fff'} /> {(!purchaseEnded && !purchased) && <ArrowRight size={14} color="#fff" />}
)} </View>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</TouchableOpacity>
</View> </View>
</TouchableOpacity>
); );
}; };
export default memo(LiveTenderTile); export default memo(LiveTenderTile);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
cardWrapper: {
marginVertical: 10,
marginHorizontal: 16,
backgroundColor: '#fff',
borderRadius: 24,
flexDirection: 'row',
overflow: 'hidden',
// Modern shadow
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.08,
shadowRadius: 20,
elevation: 8,
},
statusTag: {
width: 6,
height: '100%',
},
container: { container: {
flex: 1, marginHorizontal: 16,
padding: 20, marginVertical: 10,
borderRadius: 20,
borderWidth: 1,
overflow: 'hidden',
elevation: 4,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
}, },
header: { topLine: {
height: 4,
width: '100%',
},
contentContainer: {
padding: 16,
},
headerRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: 12,
}, },
badge: { idTag: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.primary + '25', paddingVertical: 4,
paddingHorizontal: 12, paddingHorizontal: 10,
paddingVertical: 5, borderRadius: 20,
borderRadius: 12,
gap: 6, gap: 6,
}, },
badgeText: { title: {
fontSize: 12, marginBottom: 16,
fontWeight: '700', fontWeight: '700',
color: colors.primary,
},
priceText: {
fontSize: 20,
fontWeight: '800',
color: '#0f172a',
},
workTitle: {
fontSize: 16,
fontWeight: '600',
color: '#334155',
lineHeight: 24, lineHeight: 24,
},
infoContainer: {
borderRadius: 16,
padding: 12,
marginBottom: 16, marginBottom: 16,
}, },
infoGrid: { infoRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
backgroundColor: '#f0f4f8', alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 16,
marginBottom: 18,
}, },
infoItem: { infoCol: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 10, gap: 10,
flex: 1,
}, },
infoTextGroup: { divider: {
flexDirection: 'column', width: 1,
height: 30,
backgroundColor: 'rgba(0,0,0,0.05)',
marginHorizontal: 10,
}, },
infoLabel: { iconBox: {
fontSize: 10, // width: 32,
color: '#94a3b8', // height: 32,
textTransform: 'uppercase', // borderRadius: 16,
fontWeight: '700', // alignItems: 'center',
// justifyContent: 'center',
// backgroundColor: 'rgba(0,0,0,0.03)',
}, },
infoValue: { actionRow: {
fontSize: 13,
fontWeight: '700',
color: '#1e293b',
},
actionBar: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12, gap: 12,
}, },
iconBtn: { detailsBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 10,
backgroundColor: '#e2e8f0',
},
iconBtnText: {
fontSize: 14,
fontWeight: '700',
color: colors.primary,
},
mainButton: {
flex: 1, flex: 1,
height: 46, paddingVertical: 10,
borderRadius: 16, borderWidth: 1,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
actionBtnWrapper: {
flex: 1.5,
},
actionBtn: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 8, paddingVertical: 10,
}, borderRadius: 14,
btnActive: { gap: 6,
backgroundColor: '#4ade80', // light green
},
btnPurchased: {
backgroundColor: colors.error,
borderWidth: 2,
borderColor: colors.error,
},
btnDisabled: {
backgroundColor: '#e2e8f0',
},
mainButtonText: {
fontSize: 14,
fontWeight: '800',
}, },
}); });
@@ -7,7 +7,6 @@ import {
ScrollView, ScrollView,
SafeAreaView, SafeAreaView,
Platform, Platform,
StatusBar,
} from 'react-native'; } from 'react-native';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import moment from 'moment'; import moment from 'moment';
@@ -74,8 +73,6 @@ const TenderFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
statusBarTranslucent statusBarTranslucent
> >
<SafeAreaView style={styles.safeArea}> <SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
{/* Header */} {/* Header */}
<View style={[styles.header, { paddingTop: inset.top }]}> <View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}> <TouchableOpacity onPress={onClose} style={styles.backBtn}>
+348 -415
View File
@@ -1,12 +1,24 @@
import { StyleSheet, Text, View, ScrollView, StatusBar, TouchableOpacity, RefreshControl, ActivityIndicator, Platform } from 'react-native' import { StyleSheet, View, ScrollView, TouchableOpacity, RefreshControl, ActivityIndicator } from 'react-native'
import React, { useCallback, useEffect } from 'react' import React, { useCallback } from 'react'
import { InsideHeader } from '../../Component/Index' import { InsideHeader } from '../../Component/Index'
import colors from '../../Constant/Color'
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native' import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'
import { useFetchTenderById, useProceedToBid } from '../../Services/ApiCall/Hooks' import { useFetchTenderById, useProceedToBid } from '../../Services/ApiCall/Hooks'
import { useTheme } from '../../Hooks/useTheme'
import AppText from '../../components/AppText'
import { CircleDot, Clock, CheckCircle, XCircle, Calendar, FileText, Shield, AlertCircle } from 'lucide-react-native'
import Animated, { FadeInUp, FadeInDown, FadeIn, SlideInRight, Layout } from 'react-native-reanimated'
import { apiCall } from '../../Axios/Axios'
import NavigationString from '../../Constant/NavigationString'
import DeviceInfo from 'react-native-device-info'
import { BASE_URL } from '../../Services/ApiCall/ApiUrls'
import { useSelector } from 'react-redux'
import { useESetuAdharVarification } from '../../Services/ApiCall/Hooks'
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog'
import { generateTransactionId } from '../../Helper/uniqueId'
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView)
const ProceedToBidDetail = () => { const ProceedToBidDetail = () => {
// Sample data based on your JSON const { colors, spacing, shadows, typography } = useTheme();
const route = useRoute<any>(); const route = useRoute<any>();
const tenderDataId = route.params?.tender; const tenderDataId = route.params?.tender;
const { data, refetch, isRefetching } = useProceedToBid(tenderDataId?.tenderId); const { data, refetch, isRefetching } = useProceedToBid(tenderDataId?.tenderId);
@@ -15,101 +27,95 @@ const ProceedToBidDetail = () => {
const [refreshing, setRefreshing] = React.useState(false); const [refreshing, setRefreshing] = React.useState(false);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const navigation = useNavigation<any>();
const user = useSelector((state: any) => state.token?.token);
const { mutateAsync: eSetuAdharVarification, isPending: isESetuPending } = useESetuAdharVarification();
const [showESetuDialog, setShowESetuDialog] = React.useState(false);
const [isESetuNavigating, setIsESetuNavigating] = React.useState(false);
const navigation = useNavigation<any>() const handleStartBidding = async () => {
// Format date from array
const handleStartBidding = () => {
setLoading(true); setLoading(true);
try { try {
setLoading(true); const deviceId = await DeviceInfo.getUniqueId();
const url = `${BASE_URL}tenderBidding/getDsc-with-device-details?deviceId=${deviceId}`;
navigation.navigate('TenderBidding', { tenderId: tenderDataId.tenderId }); const response: any = await apiCall('get', url);
console.log("Response: ", response);
const { dsc, deviceBinding, deviceVerification, dscExpiryStatus } = response?.data || {};
if (!dsc) {
navigation.navigate(NavigationString.DscBinding, {
message: "Please map your DSC from the web screen"
});
} else if (!dscExpiryStatus) {
navigation.navigate(NavigationString.DscBinding, {
message: "Your DSC has been expired. Please map new DSC from the web screen"
});
} else if (deviceBinding && deviceVerification === 'PENDING') {
navigation.navigate(NavigationString.DeviceBinding, {
message: "Please approve this device to enable bidding access from the web.",
showBindButton: false
});
} else if (deviceBinding && deviceVerification === 'REVOKE') {
navigation.navigate(NavigationString.DeviceBinding, {
message: "Your device has been revoked. Please bind your device from the web portal.",
showBindButton: true
});
} else if (!deviceBinding) {
navigation.navigate(NavigationString.DeviceBinding, {
message: "Please bind your device from the web portal.",
showBindButton: true
});
} else {
// All checks passed (dsc is true, device is bound and approved)
// Now check Aadhaar verification (eSetu)
// setShowESetuDialog(true);
navigation.navigate(NavigationString.TenderBidding, {
tenderId: tenderDataId.tenderId
});
}
} catch (error) { } catch (error) {
console.log("Error in starting bidding: ", error); console.log("Error in starting bidding: ", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
} }
const handleProceedESetu = async () => {
try {
setIsESetuNavigating(true);
const payload = {
contId: user.id,
transactionId: generateTransactionId(),
udf1: "23.77",
udf2: "77.34",
udf3: "string",
udf4: "string",
responseUrl: "http://dev.192.168.6.194:8090/"
};
const res = await eSetuAdharVarification(payload);
setShowESetuDialog(false);
// Pass returnRoute so KycWebView can come back here
navigation.navigate(NavigationString.KycWebViewScreen, {
res,
returnRoute: NavigationString.TenderBidding,
returnParams: { tenderId: tenderDataId.tenderId }
});
} catch (err) {
console.log('eSetu Error:', err);
setIsESetuNavigating(false);
} finally {
setIsESetuNavigating(false);
}
};
const formatDate = (dateArray: any[]) => { const formatDate = (dateArray: any[]) => {
if (!dateArray || dateArray.length < 3) return "N/A"; if (!dateArray || dateArray.length < 3) return "N/A";
const [year, month, day, hour = 0, minute = 0] = dateArray; const [year, month, day, hour = 0, minute = 0] = dateArray;
return `${day.toString().padStart(2, '0')}/${month.toString().padStart(2, '0')}/${year} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; return `${day.toString().padStart(2, '0')}/${month.toString().padStart(2, '0')}/${year} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
}; };
// Status indicator component
const StatusIndicator = ({ status, label }: { status: boolean; label: string }) => (
<View style={styles.statusContainer}>
<View style={[styles.statusDot, { backgroundColor: status ? colors.success : colors.error }]} />
<Text style={[styles.statusText, { color: status ? colors.success : colors.error }]}>
{label}: {status ? "Submitted" : "Not Submitted"}
</Text>
</View>
);
// Envelope log card component
const EnvelopeLogCard = ({ log, title }: { log: any; title: string }) => (
<View style={styles.envelopeCard}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}>{title}</Text>
<View style={[styles.envelopeBadge, { backgroundColor: colors.primary }]}>
<Text style={styles.envelopeBadgeText}>{log?.envelope}</Text>
</View>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>Activity:</Text>
<Text style={styles.value}>{log?.activity}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>Date & Time:</Text>
<Text style={styles.value}>{log?.dateTime}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>System Info:</Text>
<Text style={styles.value}>{log?.osName} / {log?.browserName}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>IP Address:</Text>
<Text style={styles.value}>{log?.ipAddress}</Text>
</View>
<View style={styles.logDetailRow}>
<Text style={styles.label}>Status Message:</Text>
<Text style={[styles.value, styles.successText]}>{log?.logMessage}</Text>
</View>
{log?.headingMsg && (
<View style={styles.logDetailRow}>
<Text style={styles.label}>Heading:</Text>
<Text style={styles.value}>{log?.headingMsg}</Text>
</View>
)}
</View>
);
// Information row component
const InfoRow = ({ label, value, isHighlighted = false }: { label: string; value: string; isHighlighted?: boolean }) => (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}:</Text>
<Text style={[styles.infoValue, isHighlighted && styles.highlightedValue]}>{value}</Text>
</View>
);
// Feature chip component
const FeatureChip = ({ label, enabled }: { label: string; enabled: boolean }) => (
<View style={[styles.chip, { backgroundColor: enabled ? colors.successLight : colors.lightGray }]}>
<Text style={[styles.chipText, { color: enabled ? colors.success : colors.darkGray }]}>
{label} {enabled ? '✓' : '✗'}
</Text>
</View>
);
const onRefresh = async () => { const onRefresh = async () => {
try { try {
setRefreshing(true); setRefreshing(true);
@@ -125,483 +131,410 @@ const ProceedToBidDetail = () => {
}, [tenderData?.tenderId]) }, [tenderData?.tenderId])
); );
// Status indicator component with animation
const StatusIndicator = ({ status, label, index }: { status: boolean; label: string; index: number }) => (
<Animated.View
entering={FadeInUp.delay(100 + index * 80).springify()}
style={styles.statusContainer}
>
{status ? (
<CheckCircle size={18} color={colors.success} strokeWidth={2} />
) : (
<XCircle size={18} color={colors.error} strokeWidth={2} />
)}
<AppText variant="bodySmall" color={status ? colors.success : colors.error}>
{label}: {status ? "Submitted" : "Not Submitted"}
</AppText>
</Animated.View>
);
// Envelope log card component with animation
const EnvelopeLogCard = ({ log, title, index }: { log: any; title: string; index: number }) => (
<Animated.View
entering={SlideInRight.delay(100 + index * 100).springify()}
style={[styles.envelopeCard, { borderColor: colors.border }]}
>
<View style={styles.cardHeader}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>{title}</AppText>
<View style={[styles.envelopeBadge, { backgroundColor: colors.primary }]}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '700' }}>{log?.envelope}</AppText>
</View>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Activity:</AppText>
<AppText variant="caption" color={colors.text}>{log?.activity}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Date & Time:</AppText>
<AppText variant="caption" color={colors.text}>{log?.dateTime}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>System Info:</AppText>
<AppText variant="caption" color={colors.text}>{log?.osName} / {log?.browserName}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>IP Address:</AppText>
<AppText variant="caption" color={colors.text}>{log?.ipAddress}</AppText>
</View>
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Status Message:</AppText>
<AppText variant="caption" color={colors.success}>{log?.logMessage}</AppText>
</View>
{log?.headingMsg && (
<View style={styles.logDetailRow}>
<AppText variant="caption" color={colors.textSecondary}>Heading:</AppText>
<AppText variant="caption" color={colors.text}>{log?.headingMsg}</AppText>
</View>
)}
</Animated.View>
);
// Information row component with animation
const InfoRow = ({ label, value, isHighlighted = false, index }: { label: string; value: string; isHighlighted?: boolean; index: number }) => (
<Animated.View
entering={FadeInUp.delay(50 + index * 50).springify()}
style={[styles.infoRow, { borderBottomColor: colors.border }]}
>
<AppText variant="bodySmall" color={colors.textSecondary}>{label}:</AppText>
<AppText variant="bodySmall" color={isHighlighted ? colors.success : colors.text} style={{ fontWeight: '600' }}>{value}</AppText>
</Animated.View>
);
const styles = makeStyles(colors, spacing, shadows);
return ( return (
<View style={{ flex: 1, backgroundColor: colors.white }}> <View style={{ flex: 1, backgroundColor: colors.background }}>
<InsideHeader title="Tender Detail" showArrow /> <InsideHeader title="Tender Detail" showArrow />
<ScrollView style={styles.container} showsVerticalScrollIndicator={false} <AnimatedScrollView
style={styles.container}
showsVerticalScrollIndicator={false}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={refreshing} refreshing={refreshing}
onRefresh={onRefresh} onRefresh={onRefresh}
colors={[colors.primary]} // Android colors={[colors.primary]}
tintColor={colors.primary} // iOS tintColor={colors.primary}
/> />
}> }
>
{/* Tender Header Section */} {/* Tender Header Section */}
<View style={styles.headerCard}> <Animated.View
entering={FadeInDown.duration(400).springify()}
layout={Layout.springify()}
style={[styles.headerCard, shadows.medium]}
>
<View style={styles.tenderHeader}> <View style={styles.tenderHeader}>
<View> <View>
<Text style={styles.tenderNumber}>{tenderData?.tenderNumber}</Text> <AppText variant="h3" color={colors.primary} style={{ fontWeight: '700' }}>{tenderData?.tenderNumber}</AppText>
<Text style={styles.tenderId}>ID: {tenderData?.tenderId}</Text> <AppText variant="caption" color={colors.textSecondary}>ID: {tenderData?.tenderId}</AppText>
</View> </View>
<View style={[styles.statusBadge, { backgroundColor: tenderData?.biddingStatus ? colors.success : colors.warning }]}> <Animated.View
<Text style={styles.statusBadgeText}> entering={SlideInRight.delay(300).springify()}
style={[styles.statusBadge, { backgroundColor: tenderData?.biddingStatus ? colors.success : colors.primary }]}
>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '600' }}>
{tenderData?.biddingStatus ? "Active" : "Inactive"} {tenderData?.biddingStatus ? "Active" : "Inactive"}
</Text> </AppText>
</View> </Animated.View>
</View> </View>
<Text style={styles.workDescription}>{tenderData?.nameofWork}</Text> <AppText variant="body" color={colors.text} style={{ marginTop: spacing.s, lineHeight: 22, fontSize: typography.size.s, color: colors.textSecondary }}>{tenderData?.nameofWork}</AppText>
<View style={styles.headerInfoRow}> <View style={[styles.headerInfoRow, { backgroundColor: colors.surface }]}>
<View style={styles.headerInfoItem}> <View style={styles.headerInfoItem}>
<Text style={styles.headerInfoLabel}>NIT No.</Text> <AppText variant="caption" color={colors.textSecondary}>NIT No.</AppText>
<Text style={styles.headerInfoValue}>{tenderData?.nitNo}</Text> <AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{tenderData?.nitNo}</AppText>
</View> </View>
<View style={styles.separator} /> <View style={[styles.separator, { backgroundColor: colors.border }]} />
<View style={styles.headerInfoItem}> <View style={styles.headerInfoItem}>
<Text style={styles.headerInfoLabel}>EMD Amount</Text> <AppText variant="caption" color={colors.textSecondary}>EMD Amount</AppText>
<Text style={styles.headerInfoValue}>₹{tenderData?.emdInFig}</Text> <AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>₹{tenderData?.emdInFig}</AppText>
</View>
</View> </View>
</View> </View>
</Animated.View>
{/* Envelope Submission Status */} {/* Envelope Submission Status */}
<View style={styles.section}> <Animated.View
<Text style={styles.sectionTitle}>Envelope Submission Status</Text> entering={FadeInUp.delay(200).springify()}
<View style={styles.statusGrid}> style={styles.section}
<StatusIndicator status={tenderData?.envelopeASubmittStatus} label="Envelope A" /> >
{tenderData?.showEB &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope B" />} <AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Envelope Submission Status</AppText>
{tenderData?.showLCS &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope LCS" />} <View style={[styles.statusGrid, shadows.light]}>
{tenderData?.showQCBS &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope QCBS" />} <StatusIndicator status={tenderData?.envelopeASubmittStatus} label="Envelope A" index={0} />
{tenderData?.showEC &&<StatusIndicator status={tenderData?.envelopeCSubmittStatus} label="Envelope C" />} {tenderData?.showEB && <StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope B" index={1} />}
{tenderData?.showLCS && <StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope LCS" index={2} />}
{tenderData?.showQCBS && <StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope QCBS" index={3} />}
</View> {tenderData?.showEC && <StatusIndicator status={tenderData?.envelopeCSubmittStatus} label="Envelope C" index={4} />}
</View> </View>
</Animated.View>
{/* Envelope Logs */} {/* Envelope Logs */}
{tenderData?.envelopeA_Logs && ( {tenderData?.envelopeA_Logs && (
<View style={styles.section}> <Animated.View
<Text style={styles.sectionTitle}>Submission Logs</Text> entering={FadeInUp.delay(300).springify()}
{tenderData?.envelopeA_Logs && <EnvelopeLogCard log={tenderData?.envelopeA_Logs} title="Envelope A Submission" />} style={styles.section}
{tenderData?.envelopeB_Logs && <EnvelopeLogCard log={tenderData?.envelopeB_Logs} title="Envelope B Submission" />} >
{tenderData?.envelopeC_Logs && <EnvelopeLogCard log={tenderData?.envelopeC_Logs} title="Envelope C Submission" />} <AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Submission Logs</AppText>
</View> {tenderData?.envelopeA_Logs && <EnvelopeLogCard log={tenderData?.envelopeA_Logs} title="Envelope A Submission" index={0} />}
{tenderData?.envelopeB_Logs && <EnvelopeLogCard log={tenderData?.envelopeB_Logs} title="Envelope B Submission" index={1} />}
{tenderData?.envelopeC_Logs && <EnvelopeLogCard log={tenderData?.envelopeC_Logs} title="Envelope C Submission" index={2} />}
</Animated.View>
)} )}
{/* Tender Details */} {/* Tender Details */}
<View style={styles.section}> <Animated.View
<Text style={styles.sectionTitle}>Tender Details</Text> entering={FadeInUp.delay(400).springify()}
<View style={styles.detailsCard}> style={styles.section}
<InfoRow label="Envelope Type" value={tenderData?.envelopeType} /> >
<InfoRow label="Tender Stages" value={tenderData?.tenderStages} /> <AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Tender Details</AppText>
<InfoRow label="Bid Withdrawal" value={tenderData?.bidWithDraw} isHighlighted={tenderData?.bidWithdrawStatus} /> <View style={[styles.detailsCard, shadows.light]}>
<InfoRow label="Re-bid" value={tenderData?.reBid} /> <InfoRow label="Envelope Type" value={tenderData?.envelopeType} index={0} />
<InfoRow label="DSC Status" value={tenderData?.dscStatus ? "Required" : "Not Required"} /> <InfoRow label="Tender Stages" value={tenderData?.tenderStages} index={1} />
</View> <InfoRow label="Bid Withdrawal" value={tenderData?.bidWithDraw} isHighlighted={tenderData?.bidWithdrawStatus} index={2} />
<InfoRow label="Re-bid" value={tenderData?.reBid} index={3} />
<InfoRow label="DSC Status" value={tenderData?.dscStatus ? "Required" : "Not Required"} index={4} />
</View> </View>
</Animated.View>
{/* Time Period */} {/* Time Period */}
<View style={styles.section}> <Animated.View
<Text style={styles.sectionTitle}>Submission Time Period</Text> entering={FadeInUp.delay(500).springify()}
<View style={styles.timeCard}> style={styles.section}
<View style={styles.timeRow}> >
<Text style={styles.timeLabel}>Starts On:</Text> <AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Submission Time Period</AppText>
<Text style={styles.timeValue}>{formatDate(tenderData?.submittedStartDate)}</Text> <View style={[styles.timeCard, shadows.light]}>
<Animated.View
entering={FadeIn.delay(600)}
style={[styles.timeRow, { borderBottomColor: colors.border }]}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Calendar size={16} color={colors.primary} />
<AppText variant="bodySmall" color={colors.textSecondary}>Starts On:</AppText>
</View> </View>
<View style={styles.timeRow}> <AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{formatDate(tenderData?.submittedStartDate)}</AppText>
<Text style={styles.timeLabel}>Ends On:</Text> </Animated.View>
<Text style={styles.timeValue}>{formatDate(tenderData?.submittedEndDate)}</Text> <Animated.View
entering={FadeIn.delay(700)}
style={styles.timeRow}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Calendar size={16} color={colors.error} />
<AppText variant="bodySmall" color={colors.textSecondary}>Ends On:</AppText>
</View> </View>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{formatDate(tenderData?.submittedEndDate)}</AppText>
</Animated.View>
</View> </View>
</View> </Animated.View>
{/* Feature Flags
<View style={styles.section}>
<Text style={styles.sectionTitle}>Available Features</Text>
<View style={styles.chipsContainer}>
<FeatureChip label="View Bid" enabled={tenderData?.viewBid} />
<FeatureChip label="Proceed to Bid" enabled={tenderData?.proceedtoBid} />
<FeatureChip label="Show Envelope B" enabled={tenderData?.showEB} />
<FeatureChip label="Show Envelope C" enabled={tenderData?.showEC} />
<FeatureChip label="QCBS" enabled={tenderData?.showQCBS} />
<FeatureChip label="LCS" enabled={tenderData?.showLCS} />
</View>
</View> */}
{/* Final Status */} {/* Final Status */}
<View style={styles.finalStatusCard}> <Animated.View
<Text style={styles.finalStatusTitle}>Overall Status</Text> entering={FadeInUp.delay(600).springify()}
<View style={styles.finalStatusRow}> style={[styles.finalStatusCard, { borderColor: colors.primary }]}
<Text style={styles.finalStatusLabel}>All Envelopes Submitted:</Text> >
<Text style={[styles.finalStatusValue, <AppText variant="body" color={colors.primary} style={{ fontWeight: '700', marginBottom: spacing.m, textAlign: 'center' }}>Overall Status</AppText>
{
color: tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus <Animated.View
? colors.success : colors.error entering={FadeIn.delay(700)}
}]}> style={[styles.finalStatusRow, { borderBottomColor: colors.border }]}
{tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus >
? "COMPLETED" : "PENDING"} <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
</Text> <Shield size={16} color={colors.textSecondary} />
<AppText variant="bodySmall" color={colors.text}>All Envelopes Submitted:</AppText>
</View> </View>
<View style={styles.finalStatusRow}> <AppText variant="bodySmall" color={tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus ? colors.success : colors.error} style={{ fontWeight: '700' }}>
<Text style={styles.finalStatusLabel}>Bidding Status:</Text> {tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus ? "COMPLETED" : "PENDING"}
<Text style={[styles.finalStatusValue, </AppText>
{ color: tenderData?.biddingStatus ? colors.success : colors.error }]}> </Animated.View>
<Animated.View
entering={FadeIn.delay(800)}
style={styles.finalStatusRow}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<CircleDot size={16} color={colors.textSecondary} />
<AppText variant="bodySmall" color={colors.text}>Bidding Status:</AppText>
</View>
<AppText variant="bodySmall" color={tenderData?.biddingStatus ? colors.success : colors.error} style={{ fontWeight: '700' }}>
{tenderData?.biddingStatus ? "ACTIVE" : "CLOSED"} {tenderData?.biddingStatus ? "ACTIVE" : "CLOSED"}
</Text> </AppText>
</View> </Animated.View>
</View> </Animated.View>
<View style={styles.footerSpace} /> <View style={styles.footerSpace} />
</ScrollView> </AnimatedScrollView>
<View style={styles.footer}>
{
tenderData?.proceedtoBid && !tenderData?.viewBid && tenderData?.biddingStatus && tenderData?.bidWithdrawStatus === false ? (
<Animated.View
entering={FadeInDown.duration(300)}
style={[styles.footer, { borderTopColor: colors.border }]}
>
{tenderData?.proceedtoBid && !tenderData?.viewBid && tenderData?.biddingStatus && tenderData?.bidWithdrawStatus === false ? (
<TouchableOpacity style={styles.proceedButton} onPress={handleStartBidding} disabled={isRefetching}> <TouchableOpacity style={styles.proceedButton} onPress={handleStartBidding} disabled={isRefetching}>
<Text style={styles.proceedButtonText}>{loading ? <ActivityIndicator size={'small'} color={'#ffff'} /> : 'Start Bidding'}</Text> {loading ? (
<ActivityIndicator size="small" color={colors.buttonText} />
) : (
<AppText variant="button" color={colors.buttonText}>Start Bidding</AppText>
)}
</TouchableOpacity> </TouchableOpacity>
) : (
) : <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
<Text style={styles.footerText}>One time bidding is completed</Text> <AlertCircle size={16} color={colors.textSecondary} />
} <AppText variant="bodySmall" color={colors.textSecondary}>One time bidding is completed</AppText>
</View>
</View> </View>
)}
</Animated.View>
<ConfirmDialog
visible={showESetuDialog}
title="Aadhaar Verification Required"
message="To access bidding, please complete your Aadhaar verification."
confirmText={isESetuPending || isESetuNavigating ? "Processing..." : "Proceed"}
cancelText="Cancel"
onConfirm={handleProceedESetu}
onClose={() => !isESetuPending && !isESetuNavigating && setShowESetuDialog(false)}
loading={isESetuPending || isESetuNavigating}
/>
</View>
) )
} }
export default ProceedToBidDetail export default ProceedToBidDetail
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, shadows: any) => StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
padding: 16, padding: spacing.m,
marginBottom: 60,
}, },
headerCard: { headerCard: {
backgroundColor: colors.primaryLight, backgroundColor: colors.surface,
borderRadius: 12, borderRadius: 16,
padding: 20, padding: spacing.m,
marginBottom: 20, marginBottom: spacing.m,
borderLeftWidth: 4,
borderLeftColor: colors.primary,
}, },
tenderHeader: { tenderHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 12,
},
tenderNumber: {
fontSize: 22,
fontWeight: 'bold',
color: colors.primary,
},
tenderId: {
fontSize: 14,
color: colors.darkGray,
marginTop: 4,
}, },
statusBadge: { statusBadge: {
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 6, paddingVertical: 6,
borderRadius: 20, borderRadius: 20,
}, },
statusBadgeText: {
color: colors.white,
fontWeight: '600',
fontSize: 12,
},
workDescription: {
fontSize: 16,
color: colors.darkText,
lineHeight: 22,
marginBottom: 16,
},
headerInfoRow: { headerInfoRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.white, borderRadius: 10,
borderRadius: 8, padding: spacing.s,
padding: 12, marginTop: spacing.m,
}, },
headerInfoItem: { headerInfoItem: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
}, },
headerInfoLabel: {
fontSize: 12,
color: colors.darkGray,
marginBottom: 4,
},
headerInfoValue: {
fontSize: 16,
fontWeight: '600',
color: colors.primary,
},
separator: { separator: {
width: 1, width: 1,
height: 30, height: 30,
backgroundColor: colors.lightGray,
}, },
section: { section: {
marginBottom: 20, marginBottom: spacing.m,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.primary,
marginBottom: 12,
paddingLeft: 4,
}, },
statusGrid: { statusGrid: {
backgroundColor: colors.white, backgroundColor: colors.surface,
borderRadius: 12, borderRadius: 12,
padding: 16, padding: spacing.m,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
}, },
statusContainer: { statusContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: spacing.s,
}, gap: 8,
statusDot: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 10,
},
statusText: {
fontSize: 15,
fontWeight: '500',
}, },
envelopeCard: { envelopeCard: {
backgroundColor: colors.white, backgroundColor: colors.surface,
borderRadius: 12, borderRadius: 12,
padding: 16, padding: spacing.m,
marginBottom: 12, marginBottom: spacing.s,
borderWidth: 1, borderWidth: 1,
borderColor: colors.lightGray,
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: spacing.s,
},
cardTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.primary,
}, },
envelopeBadge: { envelopeBadge: {
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 4, paddingVertical: 4,
borderRadius: 12, borderRadius: 12,
}, },
envelopeBadgeText: {
color: colors.white,
fontWeight: 'bold',
fontSize: 12,
},
logDetailRow: { logDetailRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 8, marginBottom: 6,
},
label: {
fontSize: 14,
color: colors.darkGray,
flex: 1,
},
value: {
fontSize: 14,
color: colors.darkText,
fontWeight: '500',
flex: 2,
textAlign: 'right',
},
successText: {
color: colors.success,
fontWeight: '600',
}, },
detailsCard: { detailsCard: {
backgroundColor: colors.white, backgroundColor: colors.surface,
borderRadius: 12, borderRadius: 12,
padding: 16, padding: spacing.m,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
}, },
infoRow: { infoRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
paddingVertical: 10, paddingVertical: spacing.s,
borderBottomWidth: 1, borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.lightestGray,
},
infoLabel: {
fontSize: 14,
color: colors.darkGray,
},
infoValue: {
fontSize: 15,
fontWeight: '500',
color: colors.darkText,
},
highlightedValue: {
color: colors.success,
fontWeight: '600',
}, },
timeCard: { timeCard: {
backgroundColor: colors.white, backgroundColor: colors.surface,
borderRadius: 12, borderRadius: 12,
padding: 16, padding: spacing.m,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
}, },
timeRow: { timeRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
paddingVertical: 12, paddingVertical: spacing.s,
borderBottomWidth: 1, borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.lightestGray,
},
timeLabel: {
fontSize: 15,
color: colors.darkGray,
fontWeight: '500',
},
timeValue: {
fontSize: 15,
color: colors.primary,
fontWeight: '600',
},
chipsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
chip: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
marginBottom: 8,
},
chipText: {
fontSize: 14,
fontWeight: '500',
}, },
finalStatusCard: { finalStatusCard: {
backgroundColor: colors.primaryLight, backgroundColor: colors.surface,
borderRadius: 12, borderRadius: 12,
padding: 20, padding: spacing.m,
marginTop: 10, marginTop: spacing.s,
marginBottom: spacing.l,
borderWidth: 2, borderWidth: 2,
borderColor: colors.primary,
},
finalStatusTitle: {
fontSize: 18,
fontWeight: 'bold',
color: colors.primary,
marginBottom: 16,
textAlign: 'center',
}, },
finalStatusRow: { finalStatusRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, paddingVertical: spacing.s,
paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomWidth: 1,
borderBottomColor: 'rgba(0, 0, 0, 0.1)',
},
finalStatusLabel: {
fontSize: 16,
color: colors.darkText,
fontWeight: '500',
},
finalStatusValue: {
fontSize: 16,
fontWeight: 'bold',
}, },
footerSpace: { footerSpace: {
height: 30, height: 80,
},
success: {
color: colors.success,
},
error: {
color: colors.error,
},
primary: {
color: colors.primary,
},
warning: {
color: colors.warning,
},
successLight: {
backgroundColor: colors.successLight,
},
lightGray: {
backgroundColor: colors.lightGray,
}, },
footer: { footer: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
backgroundColor: colors.white, backgroundColor: colors.surface,
padding: 16, padding: spacing.m,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: colors.lightGray,
elevation: 10,
}, },
proceedButton: { proceedButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
paddingVertical: 14, paddingVertical: 14,
borderRadius: 30,
alignItems: 'center',
},
proceedButtonText: {
color: colors.white,
fontSize: 16,
fontWeight: '600',
},
footerText: {
textAlign: 'center',
color: colors.darkGray,
fontSize: 14,
},
actionButton: {
width: '100%',
height: 48,
borderRadius: 12, borderRadius: 12,
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
}),
}, },
}); });
+366 -125
View File
@@ -1,31 +1,53 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native'; import {
View,
FlatList,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import Animated, { FadeInDown, FadeIn } from 'react-native-reanimated';
import {
CreditCard,
Calendar,
Hash,
IndianRupee,
Percent,
Server,
Wifi,
WifiOff,
Receipt,
CheckCircle2,
} from 'lucide-react-native';
import { InsideHeader, Loader } from '../../Component/Index'; import { InsideHeader, Loader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useOfflineTenderPurchase } from '../../Services/ApiCall/Hooks'; import { useOfflineTenderPurchase } from '../../Services/ApiCall/Hooks';
import { StackActions, useNavigation } from '@react-navigation/native'; import { StackActions, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import { showErrorAlert, showToast } from '../../Constant/ShowDailog'; import { showErrorAlert, showToast } from '../../Constant/ShowDailog';
import { clearPurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender'; import { clearPurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const ProceedToPurTender = () => { const ProceedToPurTender = () => {
const navigation: any = useNavigation(); const navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { mutate, isPending } = useOfflineTenderPurchase() const { mutate, isPending } = useOfflineTenderPurchase();
const calculateTotalAmount = (item: any) => { const calculateTotalAmount = (item: any) => {
const gstAmount = const gstAmount =
(Number(item.documentFees) + Number(item.portalCharge)) * (Number(item.documentFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100); (Number(item.gst) / 100);
return Number(item.documentFees) + Number(item.portalCharge) + gstAmount;
};
const payableAmount = const grandTotal = SelectedPurchaseList.reduce(
Number(item.documentFees) + Number(item.portalCharge) + gstAmount; (sum: number, item: any) => sum + calculateTotalAmount(item),
return payableAmount 0
} );
const handleOfflinePurchase = async () => { const handleOfflinePurchase = async () => {
const orderNo = `ORD${Date.now()}`; const orderNo = `ORD${Date.now()}`;
@@ -40,120 +62,208 @@ const ProceedToPurTender = () => {
tenderFees: item.documentFees, tenderFees: item.documentFees,
tenderId: item.tenderId, tenderId: item.tenderId,
tenderNumber: item.tenderNumber, tenderNumber: item.tenderNumber,
tenderStatus: item.tenderStatus ?? "", tenderStatus: item.tenderStatus ?? '',
tenderType: item.tenderType ?? "", tenderType: item.tenderType ?? '',
isPurchased: item.purchaseStatus ?? false, isPurchased: item.purchaseStatus ?? false,
totalPayableAmount: calculateTotalAmount(item), totalPayableAmount: calculateTotalAmount(item),
percentageAmount: calculateTotalAmount(item) / 100, percentageAmount: calculateTotalAmount(item) / 100,
newTenderDocumentFees: item.documentFees * index + 1, newTenderDocumentFees: item.documentFees * index + 1,
})), })),
orderNo: orderNo, orderNo,
}; };
try { try {
mutate(body, { mutate(body, {
onSuccess: (res) => { onSuccess: (res: any) => {
if (res.statusCode === 200) { if (res.statusCode === 200) {
showToast(res.message); showToast(res.message);
navigation.dispatch(StackActions.pop(2)); navigation.dispatch(StackActions.pop(2));
Dispatch( Dispatch(clearPurchaseTender());
clearPurchaseTender()
);
} }
}, },
onError: (error) => { onError: (error: any) => {
showErrorAlert('Something went wrong'); showErrorAlert('Something went wrong');
console.log(error, "================purchse Error"); console.log(error, '================purchse Error');
} },
}) });
} catch (error) { } catch (error) {
console.log("purchase offline error", error); console.log('purchase offline error', error);
}
} }
};
const handlePurchase = (type: 'online' | 'offline') => { const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true); setLoading(true);
setTimeout(() => { setTimeout(() => {
setLoading(false); setLoading(false);
type === 'online' ? showErrorAlert('Online Purchase is disabled') : handleOfflinePurchase(); type === 'online'
? showErrorAlert('Online Purchase is disabled')
: handleOfflinePurchase();
}, 1200); }, 1200);
}; };
const renderItem = ({ item }: { item: any[0] }) => ( const styles = makeStyles(colors, spacing, shadows);
<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}> const renderItem = ({ item, index }: { item: any; index: number }) => {
<Calendar size={18} color={colors.primary} /> const total = calculateTotalAmount(item);
<Text style={styles.text}>End Date: {item.purchaseEnd}</Text> const gstAmount =
(Number(item.documentFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return (
<Animated.View
entering={FadeInDown.delay(index * 90).springify()}
style={styles.card}
>
{/* Card Top Accent */}
<View style={styles.cardAccent} />
<View style={styles.cardBody}>
{/* Header */}
<View style={styles.cardHeader}>
<View style={styles.indexBadge}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '800' } as any}>
{String(index + 1).padStart(2, '0')}
</AppText>
</View>
<View style={{ flex: 1, marginLeft: spacing.s }}>
<AppText variant="h4" color={colors.text} numberOfLines={2}>
{item.auctionOfWork || item.auctionNumber}
</AppText>
{item.auctionPattern ? (
<View style={styles.patternBadge}>
<AppText variant="caption" color={colors.primary} style={{ fontWeight: '600' } as any}>
{item.auctionPattern}
</AppText>
</View>
) : null}
</View>
</View> </View>
<View style={styles.cardRow}> {/* Divider */}
<User size={18} color={colors.primary} /> <View style={styles.divider} />
<Text style={styles.text}>Tender No: {item.tenderNumber}</Text>
</View> {/* Info Rows */}
<InfoRow
icon={<Hash size={15} color={colors.primary} />}
label="Tender No"
value={item.tenderNumber}
colors={colors}
/>
<InfoRow
icon={<Calendar size={15} color={colors.primary} />}
label="Purchase End"
value={item.purchaseEnd || '—'}
colors={colors}
/>
{/* Divider */}
<View style={[styles.divider, { marginVertical: spacing.s }]} />
<View style={styles.cardRow}> {/* Fee breakdown */}
<DollarSign size={18} color={colors.primary} /> <View style={styles.feeSection}>
<Text style={styles.text}>Tender Fees: ₹{item.documentFees}</Text> <AppText variant="caption" color={colors.textSecondary} style={styles.feeHeader as any}>
</View> FEE BREAKDOWN
</AppText>
<View style={styles.cardRow}> <FeeRow
<DollarSign size={18} color={colors.primary} /> label="Document Fees"
<Text style={styles.text}>GST ({item.gst}%): ₹{item.gst / 100}</Text> value={`₹ ${Number(item.documentFees).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> </View>
<View style={styles.cardRow}> {/* Total Banner */}
<DollarSign size={18} color={colors.primary} /> <View style={styles.totalBanner}>
<Text style={styles.text}>Portal Charge: ₹{item.portalCharge}</Text> <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> </View>
<AppText variant="h4" color={colors.buttonText}>
<View style={styles.totalRow}> ₹ {total.toFixed(2)}
<CreditCard size={20} color={colors.white} /> </AppText>
<Text style={styles.total}>Total: ₹{calculateTotalAmount(item)}</Text>
</View> </View>
</View> </View>
</Animated.View>
); );
};
return ( return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<InsideHeader title="Tender Purchase" showArrow />
<View style={styles.screen}> <View style={styles.screen}>
<InsideHeader title="Tender Purchase" showArrow />
{/* Grand Total Strip */}
<Animated.View entering={FadeInDown.springify()} style={styles.grandTotalStrip}>
<View>
<AppText variant="caption" color={colors.textSecondary}>
{SelectedPurchaseList.length} tender{SelectedPurchaseList.length !== 1 ? 's' : ''}
</AppText>
<AppText variant="h3" color={colors.text}>
₹ {grandTotal.toFixed(2)}
</AppText>
</View>
<View style={styles.grandTotalRight}>
<CheckCircle2 size={18} color={colors.success} />
<AppText variant="caption" color={colors.success} style={{ fontWeight: '600' } as any}>
Ready to pay
</AppText>
</View>
</Animated.View>
<FlatList <FlatList
data={SelectedPurchaseList} data={SelectedPurchaseList}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item) => item?.tenderId?.toString()} keyExtractor={(item) => item?.tenderId?.toString()}
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
/> />
</View>
<View style={styles.bottomButtonContainer}> {/* Bottom Action Buttons */}
<View style={styles.footer}>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { marginRight: 8 }]} style={[styles.button, styles.onlineButton]}
activeOpacity={0.85}
onPress={() => handlePurchase('online')} onPress={() => handlePurchase('online')}
> >
<Text style={styles.buttonText}>Purchase Online</Text> <Wifi size={18} color={colors.primary} />
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} /> <AppText variant="button" color={colors.primary} style={styles.onlineBtnText as any}>
Online
</AppText>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { marginLeft: 8 }]} style={[styles.button, styles.offlineButton]}
activeOpacity={0.85}
onPress={() => handlePurchase('offline')} onPress={() => handlePurchase('offline')}
> >
<Text style={styles.buttonText}>Purchase Offline</Text> <WifiOff size={18} color={colors.buttonText} />
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} /> <AppText variant="button" color={colors.buttonText} style={styles.offlineBtnText as any}>
Offline
</AppText>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{loading && ( {/* Loading Overlay */}
<View style={styles.loadingOverlay}> {(loading || isPending) && (
<Loader /> <Animated.View entering={FadeIn.duration(200)} style={styles.loadingOverlay}>
<View style={styles.loadingCard}>
<ActivityIndicator size="large" color={colors.primary} />
<AppText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: spacing.s } as any}>
Processing purchase…
</AppText>
</View> </View>
</Animated.View>
)} )}
</View> </View>
); );
@@ -161,95 +271,226 @@ const ProceedToPurTender = () => {
export default ProceedToPurTender; export default ProceedToPurTender;
const styles = StyleSheet.create({ /* ─── Sub-components ─── */
const InfoRow = ({
icon,
label,
value,
colors,
}: {
icon: React.ReactNode;
label: string;
value: string;
colors: any;
}) => (
<View style={infoRowStyles.row}>
<View style={infoRowStyles.iconWrap}>{icon}</View>
<AppText variant="caption" color={colors.textSecondary} style={infoRowStyles.label as any}>
{label}
</AppText>
<AppText variant="bodySmall" color={colors.text} style={infoRowStyles.value as any}>
{value}
</AppText>
</View>
);
const infoRowStyles = StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 5,
gap: 6,
},
iconWrap: {
width: 22,
alignItems: 'center',
},
label: {
flex: 1,
fontWeight: '500',
} as any,
value: {
fontWeight: '600',
} as any,
});
const FeeRow = ({
label,
value,
colors,
}: {
label: string;
value: string;
colors: any;
}) => (
<View style={feeRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary}>
{label}
</AppText>
<AppText variant="caption" color={colors.text} style={{ fontWeight: '600' } as any}>
{value}
</AppText>
</View>
);
const feeRowStyles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 3,
},
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: { screen: {
flex: 1, flex: 1,
backgroundColor: colors.background,
},
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,
backgroundColor: `${colors.success}15`,
paddingHorizontal: spacing.s + 2,
paddingVertical: spacing.xs + 2,
borderRadius: 20,
}, },
container: { container: {
padding: 16, paddingHorizontal: spacing.m,
paddingTop: spacing.s,
paddingBottom: 120, paddingBottom: 120,
}, },
card: { card: {
backgroundColor: '#fff', backgroundColor: colors.surface,
borderRadius: 16, borderRadius: 20,
padding: 16, marginBottom: spacing.m,
marginBottom: 16, overflow: 'hidden',
elevation: 5, ...shadows.medium,
shadowColor: '#000', },
shadowOpacity: 0.12, cardAccent: {
shadowOffset: { width: 0, height: 3 }, height: 4,
shadowRadius: 8, backgroundColor: colors.primary,
},
cardBody: {
padding: spacing.m,
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'flex-start',
marginBottom: 12, marginBottom: spacing.s,
}, },
title: { indexBadge: {
fontSize: 16, width: 32,
fontWeight: '700', height: 32,
color: '#0F172A', borderRadius: 10,
},
pattern: {
fontSize: 14,
color: colors.primary,
fontWeight: '600',
},
cardRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginBottom: 4,
},
text: {
fontSize: 14,
color: '#555',
},
totalRow: {
marginTop: 10,
backgroundColor: colors.primary, backgroundColor: colors.primary,
borderRadius: 12,
paddingVertical: 10,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
gap: 8, justifyContent: 'center',
marginTop: 2,
}, },
total: { patternBadge: {
fontSize: 15, alignSelf: 'flex-start',
backgroundColor: `${colors.primary}15`,
paddingHorizontal: spacing.s,
paddingVertical: 2,
borderRadius: 8,
marginTop: 4,
},
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginVertical: spacing.s,
},
feeSection: {
backgroundColor: colors.overlay,
borderRadius: 12,
padding: spacing.s + 2,
marginBottom: spacing.s,
},
feeHeader: {
letterSpacing: 1.2,
marginBottom: spacing.xs + 2,
fontWeight: '700', fontWeight: '700',
color: '#fff', } as any,
totalBanner: {
backgroundColor: colors.primary,
borderRadius: 14,
paddingVertical: 12,
paddingHorizontal: spacing.m,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}, },
bottomButtonContainer: { totalLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
footer: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0,
flexDirection: 'row', flexDirection: 'row',
backgroundColor: colors.white, paddingHorizontal: spacing.m,
width: '100%', paddingTop: spacing.s + 4,
paddingVertical: 20, paddingBottom: spacing.m + 4,
paddingHorizontal: 10, backgroundColor: colors.surface,
elevation: 5 gap: spacing.s,
...shadows.strong,
}, },
button: { button: {
flex: 1, flex: 1,
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 14,
borderRadius: 16,
gap: spacing.s,
}, },
buttonText: { onlineButton: {
color: '#fff', backgroundColor: colors.surface,
fontWeight: '700', borderWidth: 1.5,
fontSize: 14, borderColor: colors.primary,
}, },
offlineButton: {
backgroundColor: colors.buttonPrimary,
...shadows.medium,
},
onlineBtnText: {
letterSpacing: 1.2,
} as any,
offlineBtnText: {
letterSpacing: 1.2,
} as any,
loadingOverlay: { loadingOverlay: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.2)', backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
zIndex: 10, zIndex: 10,
}, },
loadingCard: {
backgroundColor: colors.surface,
borderRadius: 20,
padding: spacing.l,
alignItems: 'center',
...shadows.strong,
},
}); });
+1 -1
View File
@@ -73,7 +73,7 @@ const PurchaseTenderAllList = () => {
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<SearchInput onChange={() => { }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} /> <SearchInput onChange={() => { }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} filter={false} />
{showRefreshBanner && ( {showRefreshBanner && (
<Animated.View <Animated.View
+350 -163
View File
@@ -1,223 +1,410 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; import {
import Animated, { FadeIn, FadeOut, Layout, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; View,
import { ChevronDown, Trash2, Info } from 'lucide-react-native'; StyleSheet,
ScrollView,
TouchableOpacity,
useWindowDimensions,
} from 'react-native';
import Animated, {
FadeIn,
FadeOut,
FadeInDown,
Layout,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, FileText, Tag } from 'lucide-react-native';
import { InsideHeader } from '../../Component/Index'; import { InsideHeader } from '../../Component/Index';
import colors from '../../Constant/Color';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString'; import NavigationString from '../../Constant/NavigationString';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender'; import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { useEffect } from 'react';
const PurchaseTenderList = () => { const PurchaseTenderList = () => {
const Navigation: any = useNavigation() const Navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows, typography } = useTheme();
const [expandedIndex, setExpandedIndex] = useState<number | null>(null); const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { width } = useWindowDimensions();
const toggleExpand = (index: number) => { const toggleExpand = (index: number) => {
setExpandedIndex(prev => (prev === index ? null : index)); setExpandedIndex(prev => (prev === index ? null : index));
}; };
useEffect(() => { useEffect(() => {
if (SelectedPurchaseList.length === 0) { if (SelectedPurchaseList.length === 0) {
Navigation.goBack() Navigation.goBack();
} }
},[SelectedPurchaseList] }, [SelectedPurchaseList]);
) const styles = makeStyles(colors, spacing, shadows);
return ( return (
<View style={styles.screen}> <View style={styles.screen}>
<InsideHeader title="Purchase" showArrow /> <InsideHeader title="Purchase Tenders" showArrow />
<ScrollView contentContainerStyle={styles.container}> {/* 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>
{' '}tender{SelectedPurchaseList.length !== 1 ? 's' : ''} selected for purchase
</AppText>
</View>
{/* Cards */} <ScrollView
{SelectedPurchaseList.map((item: any, index: any) => { contentContainerStyle={styles.container}
const expanded = expandedIndex === index; showsVerticalScrollIndicator={false}
return (
<Animated.View
key={item.auctionId}
layout={Layout.springify()}
style={styles.card}
> >
<TouchableOpacity {SelectedPurchaseList.map((item: any, index: number) => (
activeOpacity={0.9} <TenderItem
onPress={() => toggleExpand(index)} key={item.auctionId || index}
> item={item}
<CardHeader index={index}
title={item.auctionOfWork} expanded={expandedIndex === index}
expanded={expanded} toggleExpand={toggleExpand}
colors={colors}
styles={styles}
/> />
))}
<Text style={styles.subTitle}> <View style={{ height: 90 }} />
Tender No: {item.tenderNumber}
</Text>
<Text style={styles.subTitle}>
Status:{' '}
<Text
style={{
color: item.tenderStatus === 'Opened' ? '#16A34A' : '#dc6f26ff',
fontWeight: '700',
}}
>
{item.tenderStatus === 'Opened' ? 'Open' : 'Approved'}
</Text>
</Text>
</TouchableOpacity>
{/* Expanded Content */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(180)}
layout={Layout.springify()}
style={styles.details}
>
{/* {renderRow('', item.departmentName)} */}
{/* {renderRow('Pattern', item.auctionPattern)} */}
{renderRow('Fees', `₹ ${item.documentFees}`)}
{renderRow('Department', item.departmentName)}
{renderRow('Purchase End', item.purchaseEnd)}
<TouchableOpacity style={styles.removeButton} onPress={() => { Dispatch(togglePurchaseTender(item)) }}>
<Trash2 size={18} color="#fff" />
<Text style={styles.removeText}>Remove</Text>
</TouchableOpacity>
</Animated.View>
)}
</Animated.View>
);
})}
</ScrollView> </ScrollView>
{/* Footer */} {/* Floating Proceed Button */}
<TouchableOpacity disabled={Boolean(!SelectedPurchaseList.length)} style={[styles.proceedButton, { backgroundColor: Boolean(!SelectedPurchaseList.length) ? colors.lightText : colors.primary }]} onPress={() => { Navigation.navigate(NavigationString.ProceedToPurTender) }}> <View style={styles.footerContainer}>
<Text style={styles.proceedText}>Proceed to Purchase</Text> <TouchableOpacity
activeOpacity={0.9}
disabled={!SelectedPurchaseList.length}
style={[
styles.proceedButton,
{
backgroundColor: SelectedPurchaseList.length
? colors.buttonPrimary
: colors.secondary,
},
]}
onPress={() => Navigation.navigate(NavigationString.ProceedToPurTender)}
>
<AppText variant="button" color={colors.buttonText} style={styles.proceedText as any}>
Proceed to Purchase
</AppText>
<View style={styles.countBadge}>
<AppText variant="caption" color={colors.buttonPrimary} style={{ fontWeight: '800' } as any}>
{SelectedPurchaseList.length}
</AppText>
</View>
<ChevronRight size={20} color={colors.buttonText} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View>
); );
}; };
export default PurchaseTenderList; export default PurchaseTenderList;
const CardHeader = ({ title, expanded }: { /* ─── Sub-components ─── */
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` }],
}));
return ( const DetailRow = ({
<View style={styles.cardHeader}> label,
<Text style={styles.title}>{title}</Text> value,
<Animated.View style={arrowStyle}> colors,
<ChevronDown size={22} color={colors.black} /> valueColor,
</Animated.View> }: {
</View> label: string;
); value: string;
}; colors: any;
valueColor?: string;
const renderRow = (label: string, value: string) => ( }) => (
<View style={styles.row}> <View style={detailRowStyles.row}>
<Text style={styles.label}>{label}</Text> <AppText variant="caption" color={colors.textSecondary} style={detailRowStyles.label as any}>
<Text style={styles.value}>{value}</Text> {label}
</AppText>
<AppText variant="caption" color={valueColor || colors.text} style={detailRowStyles.value as any}>
{value}
</AppText>
</View> </View>
); );
const detailRowStyles = StyleSheet.create({
const styles = StyleSheet.create({ row: {
screen: {
flex: 1,
backgroundColor: '#F8FAFC',
},
container: {
padding: 16,
paddingBottom: 110,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 14,
},
heading: {
fontSize: 22,
fontWeight: '800',
color: colors.black,
},
card: {
backgroundColor: '#fff',
borderRadius: 18,
padding: 16,
marginBottom: 14,
borderWidth: 1,
borderColor: '#E5E7EB',
},
cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
}, paddingVertical: 8,
title: { borderBottomWidth: StyleSheet.hairlineWidth,
fontSize: 16, borderBottomColor: 'rgba(0,0,0,0.06)',
fontWeight: '700',
color: '#0F172A',
flex: 1,
marginRight: 12,
},
subTitle: {
fontSize: 14,
color: '#64748B',
marginTop: 6,
},
details: {
marginTop: 16,
paddingTop: 14,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
row: {
marginVertical: 6,
}, },
label: { label: {
fontSize: 13, flex: 1,
color: '#64748B', fontWeight: '500',
}, } as any,
value: { value: {
fontSize: 15, flex: 1,
textAlign: 'right',
fontWeight: '600', fontWeight: '600',
color: '#0F172A', } as any,
});
/* ─── Chevron animated style hook ─── */
function useChevronStyle(expanded: boolean) {
const rotation = useSharedValue(expanded ? 1 : 0);
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
return useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
}
/* ─── Extracted Tender Item Component ─── */
const TenderItem = ({ item, index, expanded, toggleExpand, colors, styles }: any) => {
const Dispatch = useDispatch();
const isOpen = item.tenderStatus === 'Opened';
const chevronStyle = useChevronStyle(expanded);
console.log(item, "itemmmmmmm");
return (
<Animated.View
entering={FadeInDown.delay(index * 80).springify()}
layout={Layout.springify()}
style={[styles.card, expanded && styles.cardExpanded]}
>
<View style={[styles.accentBar, { backgroundColor: isOpen ? colors.success : colors.primary }]} />
<View style={styles.cardInner}>
<TouchableOpacity
activeOpacity={0.85}
onPress={() => toggleExpand(index)}
>
<View style={styles.badgeRow}>
<View style={styles.tenderBadge}>
<FileText size={11} color={colors.primary} />
<AppText variant="caption" color={colors.primary} style={styles.badgeText as any}>
{item.tenderNumber}
</AppText>
</View>
</View>
<View style={styles.titleRow}>
<AppText variant="h4" style={styles.title as any} numberOfLines={2}>
{item.auctionOfWork}
</AppText>
<Animated.View style={chevronStyle}>
<ChevronDown size={20} color={colors.textSecondary} />
</Animated.View>
</View>
<View style={styles.deptRow}>
<Tag size={12} color={colors.textSecondary} />
<AppText variant="caption" color={colors.textSecondary} style={styles.deptText as any}>
{item.departmentName || 'Department'}
</AppText>
</View>
</TouchableOpacity>
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(160)}
layout={Layout.springify()}
style={styles.detailsContainer}
>
<View style={styles.divider} />
<DetailRow
label="Document Fees"
value={`₹ ${item.documentFees}`}
colors={colors}
valueColor={colors.primary}
/>
<DetailRow
label="Department"
value={item.departmentName}
colors={colors}
/>
<DetailRow
label="Purchase End"
value={item.purchaseEnd}
colors={colors}
/>
<TouchableOpacity
style={styles.removeButton}
activeOpacity={0.85}
onPress={() => Dispatch(togglePurchaseTender(item))}
>
<Trash2 size={16} color="#fff" />
<AppText variant="button" color="#fff" style={styles.removeText as any}>
Remove
</AppText>
</TouchableOpacity>
</Animated.View>
)}
</View>
</Animated.View>
);
};
/* ─── 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,
},
tenderBadge: {
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,
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.s + 2,
paddingVertical: 3,
borderRadius: 20,
gap: 5,
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
},
titleRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: spacing.s,
},
title: {
flex: 1,
color: undefined, // will use AppText default from theme
fontWeight: '700',
} as any,
deptRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
marginTop: spacing.xs + 2,
},
deptText: {
letterSpacing: 0.3,
} as any,
detailsContainer: {
marginTop: spacing.s,
},
divider: {
height: 1,
backgroundColor: colors.border,
marginBottom: spacing.s,
}, },
removeButton: { removeButton: {
marginTop: 18, marginTop: spacing.m,
backgroundColor: colors.primary, backgroundColor: colors.error,
borderRadius: 14, borderRadius: 14,
paddingVertical: 12, paddingVertical: 12,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
gap: 8, alignItems: 'center',
gap: spacing.s,
}, },
removeText: { removeText: {
color: '#fff', letterSpacing: 1,
fontWeight: '700', } as any,
footerContainer: {
position: 'absolute',
bottom: 20,
left: spacing.m,
right: spacing.m,
}, },
proceedButton: { proceedButton: {
position: 'absolute', flexDirection: 'row',
bottom: 16,
left: 16,
right: 16,
backgroundColor: colors.primary,
paddingVertical: 16,
borderRadius: 18,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
paddingVertical: 16,
borderRadius: 20,
gap: spacing.s,
...shadows.strong,
}, },
proceedText: { proceedText: {
color: '#fff', letterSpacing: 1.5,
fontSize: 16, } as any,
fontWeight: '800', countBadge: {
backgroundColor: '#fff',
borderRadius: 12,
minWidth: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
}, },
}); });
+170 -175
View File
@@ -1,268 +1,263 @@
import React, { use, useEffect } from 'react'; import React, { useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native'; import { View, StyleSheet, TouchableOpacity, Platform } from 'react-native';
import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import moment from 'moment'; import moment from 'moment';
import CountdownTimer from '../../Component/Counter/CountdownTimer'; import CountdownTimer from '../../Component/Counter/CountdownTimer';
import colors from '../../Constant/Color';
import { useFetchTenderById } from '../../Services/ApiCall/Hooks'; import { useFetchTenderById } from '../../Services/ApiCall/Hooks';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import TenderFullDetail from './Modal/TenderFullDetail'; import TenderFullDetail from './Modal/TenderFullDetail';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { Calendar, Wallet, Banknote, ArrowRight, CircleDot, Clock } from 'lucide-react-native';
const PurchaseTenderTile = ({ item, onPressDetails }: any) => { const PurchaseTenderTile = ({ item, onPressDetails }: any) => {
const scale = useSharedValue(1); const { colors, spacing, shadows } = useTheme();
const elevation = useSharedValue(8);
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const scale = useSharedValue(1);
const elevation = useSharedValue(2); // Reduced initial elevation for a sleeker look
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }], transform: [{ scale: scale.value }],
elevation: elevation.value, elevation: elevation.value,
})); }));
const { data: tenderResponse, }: any = useFetchTenderById(item?.tenderId); const { data: tenderResponse }: any = useFetchTenderById(item?.tenderId); // Assuming id is reachable here, verified in original
// Fix: Original code used item?.tenderId, but useFetchTenderById might need checks.
// Keeping logic same as original: item contains tenderId?
// Original check: const { data: tenderResponse, }: any = useFetchTenderById(item?.tenderId);
const tender = tenderResponse?.data; const tender = tenderResponse?.data;
const handlePressIn = () => { const handlePressIn = () => {
scale.value = withTiming(0.97, { duration: 120 }); scale.value = withTiming(0.98, { duration: 150 });
elevation.value = withTiming(3, { duration: 120 });
}; };
const handlePressOut = () => { const handlePressOut = () => {
scale.value = withTiming(1, { duration: 160 }); scale.value = withTiming(1, { duration: 150 });
elevation.value = withTiming(8, { duration: 160 });
}; };
const isLive = item?.biddingStatus === true; const isLive = item?.biddingStatus === true;
const statusColor = isLive ? colors.primary : '#94A3B8';
const lightStatusBg = isLive ? '#EFF6FF' : '#F8FAFC';
const [showDetail, setShowDetail] = React.useState(false); const [showDetail, setShowDetail] = React.useState(false);
// Dynamic colors based on status
const statusConfig = useMemo(() => {
if (isLive) {
return {
color: colors.primary,
bg: colors.primary + '15', // 15 = ~8% opacity
label: 'LIVE NOW',
icon: CircleDot
};
}
return {
color: colors.textSecondary,
bg: colors.secondary + '15',
label: 'CLOSED',
icon: Clock
};
}, [isLive, colors]);
const styles = useMemo(() => styling(colors, spacing), [colors, spacing]);
return ( return (
<Animated.View <Animated.View
entering={FadeInUp.duration(400)} entering={FadeInUp.duration(500).springify()}
style={[styles.shadowContainer, animatedStyle]} style={[styles.container, shadows.light, animatedStyle]}
> >
<TouchableOpacity <TouchableOpacity
activeOpacity={1} activeOpacity={1}
onPress={onPressDetails} onPress={onPressDetails}
onPressIn={handlePressIn} onPressIn={handlePressIn}
onPressOut={handlePressOut} onPressOut={handlePressOut}
style={styles.cardInternal} style={styles.card}
> >
<View style={[styles.statusAccent, { backgroundColor: statusColor }]} /> {/* Header Section */}
<View style={styles.header}>
<View style={styles.headerTopUser}>
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bg }]}>
<statusConfig.icon size={12} color={statusConfig.color} strokeWidth={3} />
<AppText variant="caption" style={[styles.statusText, { color: statusConfig.color }]}>
{statusConfig.label}
</AppText>
</View>
<AppText variant="caption" color={colors.textSecondary} style={{ letterSpacing: 0.5 }}>
ID: {item?.tenderNumber}
</AppText>
</View>
<View style={styles.mainInfo}> <View style={{ marginTop: 8 }}>
{/* Header */} <AppText variant="bodySmall" color={colors.text} numberOfLines={2}>
<View style={styles.headerRow}> {tender?.workDescription || item?.tenderNumber || 'Tender Details'}
</AppText>
</View>
</View>
<View style={styles.divider} />
{/* Info Grid */}
<View style={styles.infoContainer}>
<View style={styles.infoRow}>
<View style={styles.iconBox}>
<Banknote size={18} color={colors.primary} />
</View>
<View> <View>
<View style={[styles.statusBadge, { backgroundColor: lightStatusBg }]}> <AppText variant="caption" color={colors.textSecondary}>Tender Fee</AppText>
<View style={[styles.statusDot, { backgroundColor: statusColor }]} /> <AppText variant="caption" style={{ fontWeight: '700' }}>
<Text style={[styles.statusText, { color: statusColor }]}>
{isLive ? 'LIVE' : 'CLOSED'}
</Text>
</View>
<Text style={styles.title}>{item?.tenderNumber}</Text>
</View>
<View style={styles.amountContainer}>
<Text style={styles.amount}>
₹{Number(item?.tenderFeeInFig || 0).toLocaleString('en-IN')} ₹{Number(item?.tenderFeeInFig || 0).toLocaleString('en-IN')}
</Text> </AppText>
<Text style={styles.feeLabel}>Tender Fee</Text>
</View> </View>
</View> </View>
{/* Date */} <View style={styles.infoRow}>
<TouchableOpacity style={styles.dateRow} onPress={() => { <View style={styles.iconBox}>
setShowDetail(true) <Calendar size={18} color={colors.primary} />
}}> </View>
<Text style={styles.dateText}> <View>
<Text style={styles.dateLabel}>Bid Ends: </Text> <AppText variant="caption" color={colors.textSecondary}>Bid Ends</AppText>
{moment( <AppText variant="caption" style={{ fontWeight: '600' }}>
item?.keydates?.bidSubmissionDate?.endDateTime, {moment(item?.keydates?.bidSubmissionDate?.endDateTime, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')}
'DD-MM-YYYY HH:mm:ss' </AppText>
).format('DD MMM YYYY, hh:mm A')} </View>
</Text> </View>
</TouchableOpacity> </View>
{/* Footer */} {/* Footer / Timer Action */}
<View style={styles.footer}> <View style={styles.footer}>
<View style={styles.footerTop}> <AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' }}>
<Text style={styles.footerLabel}>Remaining Time</Text> Time Remaining
</AppText>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
{/* Timer */}
<View style={styles.timerContainer}>
{isLive ? ( {isLive ? (
<CountdownTimer <CountdownTimer
endDate={ endDate={item?.keydates?.bidSubmissionDate?.endDateTime}
item?.keydates?.bidSubmissionDate?.endDateTime
}
/> />
) : ( ) : (
<Text style={styles.endedText}>--:--:--</Text> <AppText variant="bodySmall" style={{ fontWeight: '700', color: colors.textSecondary }}>
-- : -- : --
</AppText>
)} )}
</View> </View>
{/* CTA Button */}
<TouchableOpacity <TouchableOpacity
disabled={!isLive} disabled={!isLive}
onPress={() => { onPress={() => {
if (isLive) { if (isLive && tender) {
navigation.navigate('ProceedToBidDetail', { tender }); navigation.navigate('ProceedToBidDetail', { tender });
} else if (isLive && !tender) {
console.warn("Tender data possibly missing for navigation");
navigation.navigate('ProceedToBidDetail', { tender: item });
} }
}} }}
activeOpacity={0.85}
style={[ style={[
styles.actionButton, styles.actionButton,
{ backgroundColor: statusColor }, { backgroundColor: isLive ? colors.primary : colors.border },
!isLive && styles.disabledButton, !isLive && styles.disabledButton
]} ]}
> >
<Text style={styles.addText}> <AppText variant="button" color={isLive ? colors.buttonText : colors.textSecondary} style={{ marginRight: 8 }}>
{isLive ? 'Proceed to Bid' : 'Tender Closed'} {isLive ? 'Bid Now' : 'Closed'}
</Text> </AppText>
{isLive && <ArrowRight size={16} color={colors.buttonText} />}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
{/* Tender Detail Modal */}
<TenderFullDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} /> <TenderFullDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
); );
}; };
export default PurchaseTenderTile; const styling = (colors: any, spacing: any) => StyleSheet.create({
container: {
marginHorizontal: spacing.s,
const styles = StyleSheet.create({ marginBottom: spacing.m,
shadowContainer: {
marginHorizontal: 5,
marginTop: 14,
marginBottom: 6,
borderRadius: 16, borderRadius: 16,
backgroundColor: '#fff', backgroundColor: colors.surface,
elevation: 8,
}, },
cardInternal: { card: {
borderRadius: 16, borderRadius: 16,
overflow: 'hidden', backgroundColor: colors.surface,
flexDirection: 'row', padding: spacing.m,
backgroundColor: '#fff', borderWidth: 1,
borderColor: 'rgba(0,0,0,0.03)',
}, },
statusAccent: { header: {
width: 5, marginBottom: spacing.m,
height: '100%',
}, },
mainInfo: { headerTopUser: {
flex: 1,
padding: 16,
},
headerRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'center',
marginBottom: 12,
}, },
statusBadge: { statusBadge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 8, paddingHorizontal: 10,
paddingVertical: 3, paddingVertical: 5,
borderRadius: 6, borderRadius: 20,
marginBottom: 6, gap: 6
alignSelf: 'flex-start',
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 5,
}, },
statusText: { statusText: {
fontWeight: '700',
fontSize: 10, 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', textTransform: 'uppercase',
}, },
endedText: { divider: {
fontSize: 13, height: 1,
fontWeight: '700', backgroundColor: colors.border,
color: '#CBD5E1', marginBottom: spacing.m,
opacity: 0.5
}, },
actionButton: { infoContainer: {
width: '100%', flexDirection: 'row',
height: 48, justifyContent: 'space-between',
borderRadius: 12, marginBottom: spacing.l,
gap: spacing.s
},
infoRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flex: 1,
},
iconBox: {
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: colors.primary + '15', // light opacity primary
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
}, },
}), footer: {
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: colors.background, // Slightly distinct bg for footer
marginHorizontal: -spacing.m,
marginBottom: -spacing.m,
padding: spacing.m,
borderBottomLeftRadius: 16,
borderBottomRightRadius: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
},
timerContainer: {
flex: 1,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 12,
}, },
disabledButton: { disabledButton: {
backgroundColor: '#E2E8F0', opacity: 0.7
}, }
addText: {
color: '#fff',
fontSize: 14,
fontWeight: '800',
letterSpacing: 0.2,
},
}); });
export default PurchaseTenderTile;

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