updated code with dsc and device binding
@@ -46,7 +46,7 @@ yarn-error.log
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
# http://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { LogBox, StatusBar, View } from 'react-native';
|
||||
import { LogBox, View } from 'react-native';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { AlertNotificationRoot } from 'react-native-alert-notification';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
@@ -11,6 +11,7 @@ import { NavigationContainer } from '@react-navigation/native';
|
||||
import colors from './src/Constant/Color';
|
||||
import { TextEncoder } from 'text-encoding';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import AppStatusBar from './src/components/AppStatusBar';
|
||||
LogBox.ignoreAllLogs(true);
|
||||
|
||||
const App = () => {
|
||||
@@ -25,7 +26,7 @@ const App = () => {
|
||||
<Provider store={store}>
|
||||
<PersistGate persistor={persistor}>
|
||||
<View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
<AppStatusBar />
|
||||
<Routes />
|
||||
</View>
|
||||
</PersistGate>
|
||||
@@ -38,3 +39,4 @@ const App = () => {
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
source 'https://rubygems.org'
|
||||
source 'http://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
# You may use http://rbenv.org/ or http://rvm.io/ to install and use this version
|
||||
ruby ">= 2.6.10"
|
||||
|
||||
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
|
||||
This is a new [**React Native**](http://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](http://github.com/react-native-community/cli).
|
||||
|
||||
# Getting Started
|
||||
|
||||
> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
|
||||
> **Note**: Make sure you have completed the [Set Up Your Environment](http://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
|
||||
|
||||
## Step 1: Start Metro
|
||||
|
||||
@@ -48,7 +48,7 @@ Then, and every time you update your native dependencies, run:
|
||||
bundle exec pod install
|
||||
```
|
||||
|
||||
For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).
|
||||
For more information, please visit [CocoaPods Getting Started guide](http://guides.cocoapods.org/using/getting-started.html).
|
||||
|
||||
```sh
|
||||
# Using npm
|
||||
@@ -66,7 +66,7 @@ This is one way to run your app — you can also build it directly from Android
|
||||
|
||||
Now that you have successfully run the app, let's make changes!
|
||||
|
||||
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh).
|
||||
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](http://reactnative.dev/docs/fast-refresh).
|
||||
|
||||
When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
|
||||
|
||||
@@ -79,19 +79,19 @@ You've successfully run and modified your React Native App. :partying_face:
|
||||
|
||||
### Now what?
|
||||
|
||||
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
|
||||
- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started).
|
||||
- If you want to add this new React Native code to an existing application, check out the [Integration guide](http://reactnative.dev/docs/integration-with-existing-apps).
|
||||
- If you're curious to learn more about React Native, check out the [docs](http://reactnative.dev/docs/getting-started).
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
|
||||
If you're having issues getting the above steps to work, see the [Troubleshooting](http://reactnative.dev/docs/troubleshooting) page.
|
||||
|
||||
# Learn More
|
||||
|
||||
To learn more about React Native, take a look at the following resources:
|
||||
|
||||
- [React Native Website](https://reactnative.dev) - learn more about React Native.
|
||||
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
|
||||
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
|
||||
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
|
||||
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
|
||||
- [React Native Website](http://reactnative.dev) - learn more about React Native.
|
||||
- [Getting Started](http://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
|
||||
- [Learn the Basics](http://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
|
||||
- [Blog](http://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
|
||||
- [`@facebook/react-native`](http://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
arguments=--init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\init\\init.gradle --init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\protobuf\\init.gradle
|
||||
arguments=--init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\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
|
||||
build.scans.enabled=false
|
||||
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
||||
|
||||
@@ -40,7 +40,7 @@ react {
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// See http://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
@@ -99,7 +99,7 @@ android {
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
// see http://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
This file is a copy of abc_edit_text_material (http://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
For more info, see http://bit.ly/3CdLStv (react-native/pull/29452) and http://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
@@ -19,13 +19,13 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
# http://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -57,10 +57,10 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# http://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
# You can find Gradle at http://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
@@ -85,7 +85,7 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
# Discard cd standard output in case $CDPATH is set (http://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem http://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
||||
@@ -24,7 +24,7 @@ target 'procure' do
|
||||
)
|
||||
|
||||
post_install do |installer|
|
||||
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
|
||||
# http://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string/>
|
||||
<string>We need your location to securely bind this device for tender bidding verification.</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const path = require("path");
|
||||
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
|
||||
|
||||
const defaultConfig = getDefaultConfig(__dirname);
|
||||
@@ -5,7 +6,7 @@ const { assetExts, sourceExts } = defaultConfig.resolver;
|
||||
|
||||
/**
|
||||
* Metro configuration
|
||||
* https://reactnative.dev/docs/metro
|
||||
* http://reactnative.dev/docs/metro
|
||||
*
|
||||
* @type {import('metro-config').MetroConfig}
|
||||
*/
|
||||
@@ -17,7 +18,22 @@ const config = {
|
||||
},
|
||||
resolver: {
|
||||
assetExts: assetExts.filter((ext) => ext !== "svg"),
|
||||
sourceExts: [...sourceExts, "svg"]
|
||||
sourceExts: [...sourceExts, "svg"],
|
||||
blockList: [
|
||||
// Exclude native Android build artifacts that are created/deleted
|
||||
// during Gradle builds, which crash Metro's file watcher (ENOENT)
|
||||
/node_modules\/.*\/android\/\.cxx\/.*/,
|
||||
/node_modules\/.*\/android\/build\/.*/,
|
||||
/node_modules\/.*\/android\/\.gradle\/.*/,
|
||||
]
|
||||
},
|
||||
watcher: {
|
||||
additionalExclusions: [
|
||||
// Also exclude from the watcher layer for safety
|
||||
"**/android/.cxx/**",
|
||||
"**/android/build/**",
|
||||
"**/android/.gradle/**",
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,16 +7,18 @@
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"create:users": "node scripts/create-users.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-community/blur": "4.4.1",
|
||||
"@react-native-documents/picker": "11.0.3",
|
||||
"@react-native-masked-view/masked-view": "0.3.2",
|
||||
"@react-native-masked-view/masked-view": "0.2.9",
|
||||
"@react-native-vector-icons/material-icons": "12.4.0",
|
||||
"@react-navigation/bottom-tabs": "7.9.0",
|
||||
"@react-navigation/native": "7.1.26",
|
||||
"@react-navigation/drawer": "^7.8.1",
|
||||
"@react-navigation/native": "^7.1.28",
|
||||
"@react-navigation/stack": "7.6.13",
|
||||
"@reduxjs/toolkit": "2.11.2",
|
||||
"@stomp/stompjs": "7.2.1",
|
||||
@@ -39,16 +41,18 @@
|
||||
"react-native-blob-util": "0.24.6",
|
||||
"react-native-bouncy-checkbox": "4.1.2",
|
||||
"react-native-date-picker": "5.0.13",
|
||||
"react-native-device-info": "^15.0.2",
|
||||
"react-native-element-dropdown": "2.12.4",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-gesture-handler": "2.30.0",
|
||||
"react-native-get-random-values": "2.0.0",
|
||||
"react-native-geolocation-service": "^5.3.1",
|
||||
"react-native-gesture-handler": "^2.30.0",
|
||||
"react-native-get-random-values": "1.11.0",
|
||||
"react-native-gifted-charts": "1.4.70",
|
||||
"react-native-haptic-feedback": "2.3.3",
|
||||
"react-native-linear-gradient": "2.8.3",
|
||||
"react-native-modal": "14.0.0-rc.1",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-modal": "^14.0.0-rc.1",
|
||||
"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-safe-area-context": "5.6.2",
|
||||
"react-native-screens": "4.12.0",
|
||||
@@ -69,7 +73,7 @@
|
||||
"@babel/preset-env": "7.25.3",
|
||||
"@babel/runtime": "7.25.0",
|
||||
"@react-native-community/cli": "18.0.1",
|
||||
"@react-native-community/cli-platform-android": "18.0.0",
|
||||
"@react-native-community/cli-platform-android": "^18.0.0",
|
||||
"@react-native-community/cli-platform-ios": "18.0.0",
|
||||
"@react-native/babel-preset": "0.79.0",
|
||||
"@react-native/eslint-config": "0.79.0",
|
||||
@@ -81,7 +85,7 @@
|
||||
"@types/react-test-renderer": "19.0.0",
|
||||
"@types/sockjs-client": "1.5.4",
|
||||
"@types/text-encoding": "0.0.40",
|
||||
"eslint": "8.19.0",
|
||||
"eslint": "^8.57.0",
|
||||
"jest": "29.6.3",
|
||||
"jsencrypt": "^3.5.4",
|
||||
"prettier": "2.8.8",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -102,6 +102,7 @@ export const apiCall = async <R, D = {}>(
|
||||
success: false,
|
||||
message:
|
||||
error?.response?.data?.message || 'No Response From Server',
|
||||
...(error?.response?.data || {})
|
||||
} as R;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,37 +1,8 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
||||
import { Dropdown } from 'react-native-element-dropdown';
|
||||
import { Shield } from 'lucide-react-native';
|
||||
|
||||
const colors = {
|
||||
primary: '#F97316',
|
||||
primaryLight: '#FDBA74',
|
||||
primaryHighlight: '#f4f1efff',
|
||||
secondary: '#1B4B66',
|
||||
secondaryLight: '#639599',
|
||||
success: '#22C55E',
|
||||
error: '#EF4444',
|
||||
warning: '#F59E0B',
|
||||
info: '#3B82F6',
|
||||
golden: '#FFD700',
|
||||
white: '#FFFFFF',
|
||||
black: '#000000',
|
||||
darkText: '#2B2B2B',
|
||||
grayText: '#6B7280',
|
||||
lightText: '#9CA3AF',
|
||||
border: '#E5E7EB',
|
||||
lightGray: '#F9FAFB',
|
||||
disabled: '#D1D5DB',
|
||||
placeholder: '#9CA3AF',
|
||||
backdrop: 'rgba(0,0,0,0.5)',
|
||||
shadow: 'rgba(0,0,0,0.12)',
|
||||
Orange: '#F97316',
|
||||
OrangeLight: '#FDBA74',
|
||||
OrangeSoft: '#FFF3E8',
|
||||
darkGray: '#5f6368',
|
||||
lightestGray: '#f1f3f4',
|
||||
successLight: '#e6f4ea',
|
||||
};
|
||||
import { Shield, ChevronDown } from 'lucide-react-native';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
|
||||
type DropdownItem = {
|
||||
[key: string]: any;
|
||||
@@ -74,9 +45,22 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
|
||||
dropdownStyle,
|
||||
...rest
|
||||
}) => {
|
||||
const { colors, spacing, typography, shadows } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={[{ zIndex: 4000 }, containerStyle]}>
|
||||
{label && <Text style={styles.label}>{label}</Text>}
|
||||
<View style={[{ zIndex: 4000, marginBottom: spacing.m }, containerStyle]}>
|
||||
{label && (
|
||||
<Text style={[styles.label, {
|
||||
color: colors.text,
|
||||
fontSize: typography.size.s,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: spacing.xs
|
||||
}]}>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
<Dropdown
|
||||
data={data}
|
||||
labelField={labelField}
|
||||
@@ -86,23 +70,70 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
|
||||
onChange={onChange}
|
||||
style={[
|
||||
styles.dropdown,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: focusedField ? colors.primary : colors.border,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: 12,
|
||||
height: 56,
|
||||
},
|
||||
focusedField && shadows.light,
|
||||
style,
|
||||
{ borderColor: focusedField ? colors.primary : '#e0e0e0' },
|
||||
]}
|
||||
containerStyle={[styles.dropdownContainer, dropdownStyle]}
|
||||
selectedTextStyle={[styles.selectedText, selectedTextStyle]}
|
||||
placeholderStyle={[styles.placeholderText, placeholderStyle]}
|
||||
containerStyle={[
|
||||
styles.dropdownContainer,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
...shadows.medium,
|
||||
},
|
||||
dropdownStyle
|
||||
]}
|
||||
selectedTextStyle={[
|
||||
styles.selectedText,
|
||||
{
|
||||
color: colors.text,
|
||||
fontSize: typography.size.m,
|
||||
fontWeight: '500'
|
||||
},
|
||||
selectedTextStyle
|
||||
]}
|
||||
placeholderStyle={[
|
||||
styles.placeholderText,
|
||||
{
|
||||
color: colors.warmGray,
|
||||
fontSize: typography.size.m
|
||||
},
|
||||
placeholderStyle
|
||||
]}
|
||||
itemTextStyle={{ color: colors.textSecondary }}
|
||||
activeColor={colors.background} // Beige for active item bg
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
search
|
||||
searchPlaceholder="Search Class"
|
||||
searchPlaceholder="Search..."
|
||||
inputSearchStyle={{
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.border,
|
||||
color: colors.text
|
||||
}}
|
||||
renderLeftIcon={() => (
|
||||
<Shield
|
||||
style={{ marginRight: 10 }}
|
||||
color={focusedField ? colors.primary : '#aaa'}
|
||||
color={focusedField ? colors.primary : colors.warmGray}
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
renderRightIcon={() => (
|
||||
<ChevronDown
|
||||
size={20}
|
||||
color={focusedField ? colors.primary : colors.warmGray}
|
||||
/>
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</View>
|
||||
@@ -113,25 +144,19 @@ export default MyDropdown;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
label: {
|
||||
marginVertical: 10,
|
||||
color: colors.darkText,
|
||||
// marginVertical: 10, handled inline
|
||||
},
|
||||
dropdown: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 12,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.white,
|
||||
// handled inline with theme
|
||||
},
|
||||
dropdownContainer: {
|
||||
borderRadius: 12,
|
||||
borderColor: colors.border,
|
||||
// handled inline
|
||||
marginTop: 4,
|
||||
},
|
||||
selectedText: {
|
||||
color: colors.darkText,
|
||||
// handled inline
|
||||
},
|
||||
placeholderText: {
|
||||
color: colors.placeholder,
|
||||
// handled inline
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,10 +2,9 @@ import React from 'react';
|
||||
import { View, Text, TouchableOpacity, Image, StyleSheet, Dimensions } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { ArrowLeft } from 'lucide-react-native';
|
||||
import colors from '../../Constant/Color';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import ImagePath from '../../Constant/ImagePath';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import FontsFamily from '../../Constant/FontsFamily';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
@@ -18,6 +17,7 @@ interface HeaderProps {
|
||||
const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
|
||||
const navigation: any = useNavigation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { colors, spacing, typography, shadows } = useTheme();
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -25,6 +25,8 @@ const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
|
||||
styles.container,
|
||||
{
|
||||
paddingTop: insets.top - 10,
|
||||
backgroundColor: colors.background, // Off-white
|
||||
...shadows.light, // Soft shadow
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -36,13 +38,21 @@ const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<ArrowLeft size={26} color={colors.black} strokeWidth={2.5} />
|
||||
<ArrowLeft size={24} color={colors.primary} strokeWidth={2.5} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={[styles.title, {
|
||||
color: colors.primary,
|
||||
fontSize: typography.size.xl,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.5
|
||||
}]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Optional: Add Logo or Right Action */}
|
||||
<TouchableOpacity onPress={() => { navigation.navigate(NavigationString.Home) }}>
|
||||
<Image
|
||||
source={ImagePath.Icon.Logo2}
|
||||
@@ -60,25 +70,21 @@ export default InsideHeader;
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width,
|
||||
backgroundColor: colors.white, // IMPORTANT for shadow
|
||||
shadowColor: '#000', // iOS shadow
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 10,
|
||||
elevation: 6,
|
||||
zIndex: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 10,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
|
||||
leftRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
backButton: {
|
||||
@@ -88,16 +94,16 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 8,
|
||||
// backgroundColor: 'rgba(0,0,0,0.03)' // Optional touch target bg
|
||||
},
|
||||
|
||||
title: {
|
||||
fontSize: 20,
|
||||
color: colors.black,
|
||||
fontFamily: FontsFamily.poppinsSemiBold,
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
logo: {
|
||||
height: 60,
|
||||
width: 60,
|
||||
height: 48, // Slightly smaller for cleaner look
|
||||
width: 48,
|
||||
opacity: 0.9,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView, StatusBar } from 'react-native';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView } from 'react-native';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { AlertCircle, Clock, RefreshCw, Activity } from 'lucide-react-native';
|
||||
@@ -10,7 +10,6 @@ import FontsFamily from '../../Constant/FontsFamily';
|
||||
const ServerDown = () => {
|
||||
return (
|
||||
<>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StyleSheet, Text, TouchableOpacity, ViewStyle } from 'react-native';
|
||||
import { StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
||||
import React from 'react';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import colors from '../../Constant/Color';
|
||||
@@ -13,16 +13,8 @@ type PrimaryButtonProps = {
|
||||
|
||||
const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, disabled }) => {
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={[
|
||||
colors.primary, // #F97316 (Orange)
|
||||
colors.primaryLight, // #FDBA74 (OrangeLight)
|
||||
colors.OrangeLight, // #FDBA74 (backup)
|
||||
colors.OrangeSoft // #FFF3E8 (super soft orange end)
|
||||
]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
locations={[0.0, 0.4, 0.7, 1.0]}
|
||||
<View
|
||||
|
||||
style={[styles.primaryButton, style]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
@@ -33,7 +25,7 @@ const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, di
|
||||
>
|
||||
<Text style={styles.buttonText}>{title}</Text>
|
||||
</TouchableOpacity>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import Animated, {
|
||||
withTiming,
|
||||
interpolateColor,
|
||||
} from "react-native-reanimated";
|
||||
import colors from "../../Constant/Color";
|
||||
import { useTheme } from "../../Hooks/useTheme";
|
||||
|
||||
export interface SearchType {
|
||||
placeholder?: string;
|
||||
@@ -15,7 +15,7 @@ export interface SearchType {
|
||||
label?: string;
|
||||
filter?: boolean;
|
||||
filterPress?: () => void;
|
||||
debounceDelay?: number; // 👈 optional
|
||||
debounceDelay?: number;
|
||||
}
|
||||
|
||||
const AnimatedView = Animated.createAnimatedComponent(View);
|
||||
@@ -26,8 +26,9 @@ export const SearchInput: React.FC<SearchType> = ({
|
||||
label,
|
||||
filter = true,
|
||||
filterPress,
|
||||
debounceDelay = 500, // 👈 default debounce
|
||||
debounceDelay = 500,
|
||||
}) => {
|
||||
const { colors, shadows } = useTheme();
|
||||
const isFocused = useSharedValue(0);
|
||||
const [text, setText] = useState('');
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -36,18 +37,17 @@ export const SearchInput: React.FC<SearchType> = ({
|
||||
const borderColor = interpolateColor(
|
||||
isFocused.value,
|
||||
[0, 1],
|
||||
[colors.grayText + '50', colors.grayText]
|
||||
[colors.overlay, colors.primary] // Use theme overlay/primary
|
||||
);
|
||||
|
||||
return {
|
||||
borderColor,
|
||||
borderWidth: withTiming(isFocused.value ? 1.5 : 1),
|
||||
borderWidth: withTiming(isFocused.value ? 1 : 1), // Constant thin border
|
||||
transform: [{ scale: withTiming(isFocused.value ? 1.01 : 1) }],
|
||||
shadowOpacity: withTiming(isFocused.value ? 0.1 : 0),
|
||||
};
|
||||
});
|
||||
|
||||
// 🔥 Debounce logic
|
||||
// Debounce logic
|
||||
useEffect(() => {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
@@ -66,34 +66,47 @@ export const SearchInput: React.FC<SearchType> = ({
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{label && <Text style={styles.label}>{label}</Text>}
|
||||
{label && <Text style={[styles.label, { color: colors.text }]}>{label}</Text>}
|
||||
|
||||
<View style={styles.row}>
|
||||
<AnimatedView style={[styles.inputWrapper, inputContainerStyle]}>
|
||||
<AnimatedView style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: colors.glass,
|
||||
...shadows.light // Use theme shadow
|
||||
},
|
||||
inputContainerStyle
|
||||
]}>
|
||||
<Search
|
||||
size={18}
|
||||
color={colors.grayText}
|
||||
color={colors.primary} // Theme primary
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder={placeholder}
|
||||
value={text}
|
||||
onChangeText={setText} // 👈 update local state only
|
||||
placeholderTextColor={colors.grayText}
|
||||
onChangeText={setText}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
onFocus={() => (isFocused.value = withTiming(1))}
|
||||
onBlur={() => (isFocused.value = withTiming(0))}
|
||||
selectionColor={colors.grayText}
|
||||
selectionColor={colors.primary}
|
||||
/>
|
||||
</AnimatedView>
|
||||
|
||||
{filter && (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
style={styles.filterButton}
|
||||
style={[
|
||||
styles.filterButton,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.overlay
|
||||
}
|
||||
]}
|
||||
onPress={filterPress}
|
||||
>
|
||||
<Filter size={22} color={colors.black} />
|
||||
<Filter size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
@@ -110,7 +123,6 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
color: colors.black,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
row: {
|
||||
@@ -122,39 +134,32 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: 48,
|
||||
backgroundColor: '#F9FAFB',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
height: 50, // Slightly taller pill
|
||||
borderRadius: 25, // Full Pill
|
||||
paddingHorizontal: 16,
|
||||
borderWidth: 1,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: 8,
|
||||
marginRight: 10,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
fontSize: 15,
|
||||
color: colors.black,
|
||||
fontWeight: '500',
|
||||
paddingVertical: Platform.OS === 'ios' ? 0 : 8,
|
||||
},
|
||||
filterButton: {
|
||||
height: 48,
|
||||
width: 48,
|
||||
backgroundColor: '#FFFFFF',
|
||||
height: 50,
|
||||
width: 50,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 12,
|
||||
borderRadius: 25, // Circle
|
||||
borderWidth: 1,
|
||||
borderColor: colors.grayText + '30',
|
||||
elevation: 2,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,34 +1,110 @@
|
||||
import { View, Dimensions } from 'react-native';
|
||||
import React from 'react';
|
||||
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { View, Animated, StyleSheet, Dimensions } from 'react-native';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
|
||||
const Skeleton = () => {
|
||||
const { width } = Dimensions.get('screen');
|
||||
const cardWidth = width - 30; // full width with some padding
|
||||
// 1. Create a Reusable "Bone" Component
|
||||
const SkeletonBone = ({ width, height, style, borderRadius = 8 }) => {
|
||||
const { isDarkMode } = useTheme();
|
||||
const opacity = useRef(new Animated.Value(0.3)).current;
|
||||
|
||||
// Animation Loop
|
||||
useEffect(() => {
|
||||
const animation = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(opacity, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(opacity, {
|
||||
toValue: 0.3,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
);
|
||||
animation.start();
|
||||
|
||||
return () => animation.stop();
|
||||
}, [opacity]);
|
||||
|
||||
// Dynamic colors based on theme
|
||||
const boneColor = isDarkMode ? '#3E4145' : '#E1E9EE';
|
||||
|
||||
return (
|
||||
<View style={{ padding: 10 }}>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
width: width,
|
||||
height: height,
|
||||
backgroundColor: boneColor,
|
||||
borderRadius: borderRadius,
|
||||
opacity: opacity,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 2. Main Skeleton Layout
|
||||
const Skeleton = () => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<SkeletonPlaceholder key={index}>
|
||||
<SkeletonPlaceholder.Item
|
||||
width={cardWidth}
|
||||
marginBottom={20}
|
||||
borderRadius={12}
|
||||
overflow="hidden"
|
||||
<View
|
||||
key={index}
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
marginHorizontal: 16,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.surface,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
}}
|
||||
>
|
||||
{/* Image */}
|
||||
<SkeletonPlaceholder.Item width={cardWidth} height={150} borderRadius={12} />
|
||||
{/* Header Row: Tag + Price */}
|
||||
<View style={styles.row}>
|
||||
<SkeletonBone width={80} height={24} borderRadius={12} />
|
||||
<SkeletonBone width={60} height={24} borderRadius={4} />
|
||||
</View>
|
||||
|
||||
{/* Title */}
|
||||
<SkeletonPlaceholder.Item marginTop={10} width={cardWidth * 0.7} height={18} borderRadius={6} />
|
||||
{/* Title (2 lines) */}
|
||||
<View style={styles.section}>
|
||||
<SkeletonBone width="90%" height={18} borderRadius={4} style={{ marginBottom: 6 }} />
|
||||
<SkeletonBone width="60%" height={18} borderRadius={4} />
|
||||
</View>
|
||||
|
||||
{/* Subtitle / Price */}
|
||||
<SkeletonPlaceholder.Item marginTop={6} width={cardWidth * 0.5} height={14} borderRadius={6} />
|
||||
</SkeletonPlaceholder.Item>
|
||||
</SkeletonPlaceholder>
|
||||
{/* Info Grid (2 cols) */}
|
||||
<View style={styles.row}>
|
||||
<SkeletonBone width="45%" height={40} borderRadius={8} />
|
||||
<SkeletonBone width="45%" height={40} borderRadius={8} />
|
||||
</View>
|
||||
|
||||
{/* Action Row */}
|
||||
<View style={styles.row}>
|
||||
<SkeletonBone width="35%" height={40} borderRadius={14} />
|
||||
<SkeletonBone width="60%" height={40} borderRadius={14} />
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default Skeleton;
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import colors from '../Constant/Color';
|
||||
import React, { useMemo } from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import { useTheme } from '../Hooks/useTheme';
|
||||
import AppText from '../components/AppText';
|
||||
import { Check } from 'lucide-react-native';
|
||||
|
||||
type StepProgressBarProps = {
|
||||
currentStep: number;
|
||||
@@ -14,96 +16,132 @@ type StepProgressBarProps = {
|
||||
onStepPress?: (stepIndex: number) => void;
|
||||
};
|
||||
|
||||
const STEP_LABELS = ['Info', 'Register', 'KYC', 'Address', 'Docs'];
|
||||
|
||||
const StepProgressBar: React.FC<StepProgressBarProps> = ({
|
||||
currentStep,
|
||||
steps,
|
||||
onStepPress = () => { },
|
||||
}) => {
|
||||
const animations = useRef<Animated.Value[]>(
|
||||
steps.map(() => new Animated.Value(1))
|
||||
).current;
|
||||
|
||||
useEffect(() => {
|
||||
animations.forEach((anim, index) => {
|
||||
Animated.timing(anim, {
|
||||
toValue: index === currentStep ? 1.2 : 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
});
|
||||
}, [currentStep]);
|
||||
const { colors, spacing, typography, shadows } = useTheme();
|
||||
const styles = useMemo(() => makeStyles(colors, spacing), [colors, spacing]);
|
||||
const totalSteps = steps.length;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{steps.map((label, index) => {
|
||||
{/* 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 isCompleted = index < currentStep;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
onPress={() => onStepPress(index)}
|
||||
style={styles.stepContainer}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Animated.View
|
||||
<View key={index} style={styles.stepItem}>
|
||||
<View
|
||||
style={[
|
||||
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 }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.label,
|
||||
isCompleted && { color: '#4caf50' },
|
||||
isActive && { color: '#2196f3' },
|
||||
isActive && { ...shadows.light },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{isCompleted ? (
|
||||
<Check size={14} color={colors.buttonText} strokeWidth={3} />
|
||||
) : (
|
||||
<AppText
|
||||
variant="caption"
|
||||
color={isActive ? colors.buttonText : colors.textSecondary}
|
||||
style={{ fontWeight: '800', fontSize: 11 }}
|
||||
>
|
||||
{index + 1}
|
||||
</AppText>
|
||||
)}
|
||||
</View>
|
||||
<AppText
|
||||
variant="caption"
|
||||
color={isActive ? colors.primary : isCompleted ? colors.primary : colors.textSecondary}
|
||||
style={{
|
||||
marginTop: 4,
|
||||
fontWeight: isActive || isCompleted ? '700' : '500',
|
||||
fontSize: typography.size.xs,
|
||||
}}
|
||||
>
|
||||
{STEP_LABELS[index] || ''}
|
||||
</AppText>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default StepProgressBar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const makeStyles = (colors: any, spacing: any) => StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 20,
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.m,
|
||||
paddingHorizontal: spacing.s,
|
||||
},
|
||||
stepContainer: {
|
||||
lineContainer: {
|
||||
position: 'absolute',
|
||||
top: spacing.m + 14, // Center with circle
|
||||
left: 40,
|
||||
right: 40,
|
||||
height: 3,
|
||||
zIndex: 0,
|
||||
},
|
||||
lineBackground: {
|
||||
position: 'absolute',
|
||||
height: 3,
|
||||
width: '100%',
|
||||
borderRadius: 2,
|
||||
},
|
||||
lineFill: {
|
||||
position: 'absolute',
|
||||
height: 3,
|
||||
borderRadius: 2,
|
||||
},
|
||||
stepsRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
zIndex: 1,
|
||||
},
|
||||
stepItem: {
|
||||
alignItems: 'center',
|
||||
width: 70,
|
||||
width: 52,
|
||||
},
|
||||
circle: {
|
||||
width: 50,
|
||||
height: 5,
|
||||
borderRadius: 25,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
completedCircle: {
|
||||
backgroundColor: '#4caf50',
|
||||
},
|
||||
activeCircle: {
|
||||
backgroundColor: '#2196f3',
|
||||
},
|
||||
label: {
|
||||
// marginTop: 6,
|
||||
// fontSize: 12,
|
||||
textAlign: 'center',
|
||||
color: '#555',
|
||||
borderWidth: 2,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { View, Text, Modal, ScrollView, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
|
||||
import { CheckCircle, X } from 'lucide-react-native'; // Updated icons
|
||||
import colors from '../../Constant/Color';
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import { View, Modal, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { CheckCircle, X, FileText, Shield, Users, Brain, Lock, RefreshCw, Scale } from 'lucide-react-native';
|
||||
import Animated, { FadeInUp, FadeInDown, FadeIn } from 'react-native-reanimated';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
|
||||
type ModalProps = {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onChecked: () => void
|
||||
}
|
||||
onChecked: () => void;
|
||||
};
|
||||
|
||||
const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChecked }) => {
|
||||
const { colors, spacing, shadows, typography } = useTheme();
|
||||
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
animationType="slide"
|
||||
@@ -17,185 +23,282 @@ const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChe
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Terms & Conditions</Text>
|
||||
<Text style={styles.subtitle}>Last updated: {new Date().toLocaleDateString()}</Text>
|
||||
{/* ═══ Header ═══ */}
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(400).springify()}
|
||||
style={styles.header}
|
||||
>
|
||||
<View style={styles.headerRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<AppText variant="h3" color={colors.buttonText} style={{ fontWeight: '800' }}>
|
||||
Terms & Conditions
|
||||
</AppText>
|
||||
<AppText variant="caption" color={`${colors.buttonText}CC`} style={{ marginTop: 4 }}>
|
||||
Last updated: {new Date().toLocaleDateString()}
|
||||
</AppText>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
style={styles.closeButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<X size={20} color={colors.buttonText} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
<View style={styles.content}>
|
||||
<Section title="1. Acceptance of Terms">
|
||||
<Text style={styles.paragraph}>
|
||||
{/* ═══ Content ═══ */}
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Section
|
||||
title="1. Acceptance of Terms"
|
||||
icon={<FileText size={18} color={colors.primary} />}
|
||||
styles={styles}
|
||||
colors={colors}
|
||||
spacing={spacing}
|
||||
index={0}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
|
||||
By accessing or using our services, you agree to be bound by these Terms and Conditions.
|
||||
If you do not agree with any part of these terms, you must not use our services.
|
||||
</Text>
|
||||
</AppText>
|
||||
</Section>
|
||||
|
||||
<Section title="2. User Responsibilities">
|
||||
<Text style={styles.paragraph}>
|
||||
<Section
|
||||
title="2. User Responsibilities"
|
||||
icon={<Users size={18} color={colors.primary} />}
|
||||
styles={styles}
|
||||
colors={colors}
|
||||
spacing={spacing}
|
||||
index={1}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
|
||||
You are responsible for maintaining the confidentiality of your account information and
|
||||
for all activities that occur under your account. You agree to notify us immediately
|
||||
of any unauthorized use of your account.
|
||||
</Text>
|
||||
</AppText>
|
||||
</Section>
|
||||
|
||||
<Section title="3. Intellectual Property">
|
||||
<Text style={styles.paragraph}>
|
||||
<Section
|
||||
title="3. Intellectual Property"
|
||||
icon={<Brain size={18} color={colors.primary} />}
|
||||
styles={styles}
|
||||
colors={colors}
|
||||
spacing={spacing}
|
||||
index={2}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
|
||||
All content included on our platform, such as text, graphics, logos, and software, is
|
||||
the property of our company and protected by intellectual property laws. You may not
|
||||
reproduce, distribute, or create derivative works without our express permission.
|
||||
</Text>
|
||||
</AppText>
|
||||
</Section>
|
||||
|
||||
<Section title="4. Limitation of Liability">
|
||||
<Text style={styles.paragraph}>
|
||||
<Section
|
||||
title="4. Limitation of Liability"
|
||||
icon={<Shield size={18} color={colors.primary} />}
|
||||
styles={styles}
|
||||
colors={colors}
|
||||
spacing={spacing}
|
||||
index={3}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
|
||||
Our company shall not be liable for any indirect, incidental, special, or consequential
|
||||
damages resulting from the use or inability to use our services, even if we have been
|
||||
advised of the possibility of such damages.
|
||||
</Text>
|
||||
</AppText>
|
||||
</Section>
|
||||
|
||||
<Section title="5. Privacy Policy">
|
||||
<Text style={styles.paragraph}>
|
||||
<Section
|
||||
title="5. Privacy Policy"
|
||||
icon={<Lock size={18} color={colors.primary} />}
|
||||
styles={styles}
|
||||
colors={colors}
|
||||
spacing={spacing}
|
||||
index={4}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
|
||||
Your use of our services is also governed by our Privacy Policy, which explains how we
|
||||
collect, use, and protect your personal information. Please review our Privacy Policy
|
||||
to understand our practices.
|
||||
</Text>
|
||||
</AppText>
|
||||
</Section>
|
||||
|
||||
<Section title="6. Changes to Terms">
|
||||
<Text style={styles.paragraph}>
|
||||
<Section
|
||||
title="6. Changes to Terms"
|
||||
icon={<RefreshCw size={18} color={colors.primary} />}
|
||||
styles={styles}
|
||||
colors={colors}
|
||||
spacing={spacing}
|
||||
index={5}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
|
||||
We reserve the right to modify these terms at any time. We will notify you of any changes
|
||||
by posting the new Terms and Conditions on this page. Your continued use of the service
|
||||
after such modifications constitutes your acceptance of the new terms.
|
||||
</Text>
|
||||
</AppText>
|
||||
</Section>
|
||||
|
||||
<Section title="7. Governing Law">
|
||||
<Text style={styles.paragraph}>
|
||||
<Section
|
||||
title="7. Governing Law"
|
||||
icon={<Scale size={18} color={colors.primary} />}
|
||||
styles={styles}
|
||||
colors={colors}
|
||||
spacing={spacing}
|
||||
index={6}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.paragraph}>
|
||||
These Terms shall be governed by and construed in accordance with the laws of the
|
||||
jurisdiction in which our company is registered, without regard to its conflict of law provisions.
|
||||
</Text>
|
||||
</AppText>
|
||||
</Section>
|
||||
|
||||
<Text style={styles.footerText}>
|
||||
<Animated.View entering={FadeInUp.delay(500).springify()}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.footerText}>
|
||||
If you have any questions about these Terms, please contact us at legal@company.com.
|
||||
</Text>
|
||||
</View>
|
||||
</AppText>
|
||||
</Animated.View>
|
||||
|
||||
{/* Accept Button */}
|
||||
{/* Spacer for button */}
|
||||
<View style={{ height: 100 }} />
|
||||
</ScrollView>
|
||||
|
||||
{/* ═══ Accept Button ═══ */}
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(300).springify()}
|
||||
style={styles.acceptButtonWrapper}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.acceptButton}
|
||||
onPress={() => {
|
||||
onClose();
|
||||
onChecked();
|
||||
}}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<CheckCircle color={colors.white} size={28} />
|
||||
<Text style={styles.acceptButtonText}>I Accept</Text>
|
||||
<CheckCircle color={colors.buttonText} size={22} strokeWidth={2.5} />
|
||||
<AppText variant="button" color={colors.buttonText} style={{ marginLeft: 10 }}>
|
||||
I Accept
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// Reusable Section component
|
||||
|
||||
// ── Section sub-component ─────────────────────────────────────────────
|
||||
type SectionProps = {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
children: ReactNode;
|
||||
styles: any;
|
||||
colors: any;
|
||||
spacing: any;
|
||||
index: number;
|
||||
};
|
||||
|
||||
const Section: React.FC<SectionProps> = ({ title, children }) => (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
{children}
|
||||
const Section: React.FC<SectionProps> = ({ title, icon, children, styles, colors, spacing, index }) => (
|
||||
<Animated.View
|
||||
entering={FadeInUp.delay(100 + index * 60).springify()}
|
||||
style={styles.section}
|
||||
>
|
||||
<View style={styles.sectionHeader}>
|
||||
{icon}
|
||||
<AppText variant="bodySmall" color={colors.text} style={styles.sectionTitle}>
|
||||
{title}
|
||||
</AppText>
|
||||
</View>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
export default TermsAndConditionsModal;
|
||||
|
||||
|
||||
// ── Styles ────────────────────────────────────────────────────────────
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.lightGray, // Updated with your colors
|
||||
},
|
||||
scrollContainer: {
|
||||
paddingBottom: 120, // Space for accept button
|
||||
flexGrow: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: colors.primary, // Orange header
|
||||
paddingVertical: 32,
|
||||
paddingHorizontal: 24,
|
||||
borderBottomLeftRadius: 24,
|
||||
borderBottomRightRadius: 24,
|
||||
marginBottom: 24,
|
||||
shadowColor: colors.shadow,
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
backgroundColor: colors.primary,
|
||||
paddingTop: spacing.xl,
|
||||
paddingBottom: spacing.l,
|
||||
paddingHorizontal: spacing.l,
|
||||
borderBottomLeftRadius: 20,
|
||||
borderBottomRightRadius: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: '800',
|
||||
color: colors.white,
|
||||
marginBottom: 4,
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
closeButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: 24,
|
||||
scrollContainer: {
|
||||
padding: spacing.l,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 28,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 14,
|
||||
padding: spacing.m,
|
||||
marginBottom: spacing.m,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: spacing.s,
|
||||
paddingBottom: spacing.s,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: colors.darkText,
|
||||
marginBottom: 12,
|
||||
fontSize: typography.size.s,
|
||||
},
|
||||
paragraph: {
|
||||
fontSize: 16,
|
||||
lineHeight: 25,
|
||||
color: colors.grayText,
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 15,
|
||||
fontStyle: 'italic',
|
||||
color: colors.lightText,
|
||||
marginTop: 24,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
},
|
||||
acceptButton: {
|
||||
footerText: {
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
marginTop: spacing.m,
|
||||
},
|
||||
acceptButtonWrapper: {
|
||||
position: 'absolute',
|
||||
bottom: 40,
|
||||
left: 24,
|
||||
right: 24,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: spacing.m,
|
||||
paddingBottom: spacing.xl,
|
||||
backgroundColor: colors.surface,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
acceptButton: {
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 16,
|
||||
borderRadius: 14,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 20,
|
||||
shadowColor: colors.shadow,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 16,
|
||||
elevation: 12,
|
||||
},
|
||||
acceptButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
marginLeft: 12,
|
||||
letterSpacing: 0.5,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default TermsAndConditionsModal;
|
||||
|
||||
@@ -5,6 +5,9 @@ import Diamond from '../Images/diamond.svg'
|
||||
import Hill from '../Images/Svg/Hill.svg'
|
||||
const Logo = require('../Images/logo.png')
|
||||
const Logo2 = require('../Images/logo2.png')
|
||||
|
||||
|
||||
|
||||
const Icon = {
|
||||
Logo,
|
||||
Logo2
|
||||
@@ -15,7 +18,11 @@ const ImagePath = {
|
||||
OnBoard2,
|
||||
OnBoard3,
|
||||
Diamond,
|
||||
Hill
|
||||
Hill,
|
||||
|
||||
DarkBg: require('../Images/Backgrounds/darkBackground.png'),
|
||||
LightBg: require('../Images/Backgrounds/lightBackground.png'),
|
||||
DarkBg2: require('../Images/Backgrounds/darkBackground2.png'),
|
||||
LightBg2: require('../Images/Backgrounds/lightBackground2.png'),
|
||||
}
|
||||
export default ImagePath;
|
||||
@@ -24,6 +24,8 @@ const NavigationString = {
|
||||
ResetPassword:'ResetPassword',
|
||||
KycWebViewScreen:'KycWebViewScreen',
|
||||
StageThreeScreen:'StageThree',
|
||||
DecryptionScreen:'DecryptionScreen'
|
||||
DecryptionScreen:'DecryptionScreen',
|
||||
DeviceBinding: 'DeviceBinding',
|
||||
DscBinding: 'DscBinding'
|
||||
}
|
||||
export default NavigationString
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
After Width: | Height: | Size: 6.0 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 5.3 MiB |
|
After Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,124 +1,173 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
Animated,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import * as LucideIcons from 'lucide-react-native';
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withSpring,
|
||||
withTiming,
|
||||
interpolate,
|
||||
Extrapolation
|
||||
} from 'react-native-reanimated';
|
||||
import Screens from '../Screens';
|
||||
import colors from '../Constant/Color';
|
||||
|
||||
import NavigationString from '../Constant/NavigationString';
|
||||
import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview';
|
||||
import { useTheme } from '../Hooks/useTheme';
|
||||
import { typography } from '../theme/typography';
|
||||
// import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const TAB_BAR_HEIGHT = 60;
|
||||
const TAB_BAR_HEIGHT = 70; // Slightly taller for premium feel
|
||||
const TAB_BUTTON_WIDTH = width / 4;
|
||||
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
const TabButton = ({ route, isFocused, onPress }: any) => {
|
||||
const icons: Record<string, { outline: any; filled: any }> = {
|
||||
Home: { outline: "Home", filled: "Home" },
|
||||
const { colors } = useTheme();
|
||||
const icons: Record<string, { outline: keyof typeof LucideIcons; filled: keyof typeof LucideIcons }> = {
|
||||
Home: { outline: "House", filled: "House" },
|
||||
Auction: { outline: "Gavel", filled: "Gavel" },
|
||||
Tender: { outline: "FileText", filled: "FileText" },
|
||||
Profile: { outline: "User", filled: "User" },
|
||||
|
||||
};
|
||||
const IconName = LucideIcons[isFocused ? icons[route.name].filled : icons[route.name].outline];
|
||||
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||||
const translateYAnim = useRef(new Animated.Value(0)).current;
|
||||
const opacityAnim = useRef(new Animated.Value(1)).current;
|
||||
// Lucide icons don't always have 'filled' variants in the set, so we might use the same icon
|
||||
// or rely on color/strokeWidth to indicate state.
|
||||
const iconKey = icons[route.name]?.outline || "HelpCircle"; // Fallback
|
||||
const IconName = LucideIcons[iconKey] as React.ElementType;
|
||||
|
||||
const scale = useSharedValue(0);
|
||||
const translateY = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocused) {
|
||||
Animated.parallel([
|
||||
Animated.sequence([
|
||||
Animated.timing(translateYAnim, {
|
||||
toValue: -8,
|
||||
duration: 120,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(translateYAnim, {
|
||||
toValue: 0,
|
||||
friction: 3,
|
||||
tension: 80,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1.3,
|
||||
friction: 5,
|
||||
tension: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(opacityAnim, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
scale.value = withSpring(1, { damping: 12, stiffness: 100 });
|
||||
translateY.value = withSpring(-4, { damping: 12, stiffness: 100 });
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1,
|
||||
friction: 5,
|
||||
tension: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(opacityAnim, {
|
||||
toValue: 0.6,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
scale.value = withTiming(0, { duration: 200 });
|
||||
translateY.value = withSpring(0, { damping: 12, stiffness: 100 });
|
||||
}
|
||||
}, [isFocused]);
|
||||
|
||||
const animatedPillStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [{ scale: scale.value }],
|
||||
opacity: interpolate(scale.value, [0, 1], [0, 1], Extrapolation.CLAMP),
|
||||
};
|
||||
});
|
||||
|
||||
const animatedIconStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [{ scale: interpolate(scale.value, [0, 1], [1, 0.9]) }], // Slight shrink on press
|
||||
};
|
||||
});
|
||||
|
||||
const animatedTextStyle = useAnimatedStyle(() => {
|
||||
const opacity = interpolate(scale.value, [0, 1], [0, 1]);
|
||||
const translateX = interpolate(scale.value, [0, 1], [20, 0]); // Slide in from right
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }],
|
||||
display: scale.value === 0 ? 'none' : 'flex', // Hide when inactive
|
||||
marginLeft: 4, // Reduced space
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={[styles.tabButton, { width: TAB_BUTTON_WIDTH }]}
|
||||
style={styles.tabButton}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.iconContainer,
|
||||
{
|
||||
transform: [{ scale: scaleAnim }, { translateY: translateYAnim }],
|
||||
opacity: opacityAnim,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.tabContent}>
|
||||
<Animated.View style={animatedIconStyle}>
|
||||
{IconName && (
|
||||
<IconName
|
||||
size={20}
|
||||
color={isFocused ? colors.primary : '#888'}
|
||||
strokeWidth={2}
|
||||
size={24}
|
||||
color={isFocused ? colors.primary : colors.text}
|
||||
strokeWidth={isFocused ? 2.5 : 2}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
<Text
|
||||
{isFocused && (
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.label,
|
||||
animatedTextStyle,
|
||||
{
|
||||
color: isFocused ? colors.primary : '#888',
|
||||
fontWeight: isFocused ? '600' : '400',
|
||||
color: colors.primary,
|
||||
fontFamily: typography.fonts.medium,
|
||||
fontWeight: undefined, // Remove fontWeight as we use fontFamily
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{route.name === 'BrandDay' ? 'Coupons' : route.name}
|
||||
</Text>
|
||||
</Animated.Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomTabBar = ({ state, descriptors, navigation }: any) => {
|
||||
const { colors, shadows } = useTheme();
|
||||
const { width: windowWidth } = Dimensions.get('window');
|
||||
const TAB_BAR_WIDTH = windowWidth - 40; // 20 margin each side
|
||||
const TAB_PADDING_H = 10; // 5 padding left + 5 padding right
|
||||
const TAB_WIDTH = (TAB_BAR_WIDTH - TAB_PADDING_H) / state.routes.length;
|
||||
|
||||
const translateX = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
translateX.value = withSpring(state.index * TAB_WIDTH, {
|
||||
damping: 15,
|
||||
stiffness: 150,
|
||||
mass: 0.5,
|
||||
});
|
||||
}, [state.index]);
|
||||
|
||||
const animatedIndicatorStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [{ translateX: translateX.value }],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.tabBar}>
|
||||
<View style={[
|
||||
styles.tabBar,
|
||||
{
|
||||
backgroundColor: colors.background, // Use transparent to allow shadow to show
|
||||
shadowColor: colors.shadow, // Dynamic shadow color
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 0.2, // Consistent opacity
|
||||
shadowRadius: 10,
|
||||
borderColor: colors.border, // Subtle border for definition
|
||||
borderWidth: 3,
|
||||
elevation: 20,
|
||||
}
|
||||
]}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.slidingIndicator,
|
||||
{
|
||||
width: TAB_WIDTH,
|
||||
backgroundColor: colors.primary,
|
||||
opacity: 0.3,
|
||||
elevation: 10, // Subtle elevation for the indicator
|
||||
},
|
||||
animatedIndicatorStyle,
|
||||
]}
|
||||
/>
|
||||
{state.routes.map((route: any, index: number) => {
|
||||
const isFocused = state.index === index;
|
||||
|
||||
@@ -136,8 +185,9 @@ const CustomTabBar = ({ state, descriptors, navigation }: any) => {
|
||||
};
|
||||
|
||||
export default function BottomTab() {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<Tab.Navigator
|
||||
tabBar={(props) => <CustomTabBar {...props} />}
|
||||
screenOptions={{
|
||||
@@ -159,29 +209,54 @@ export default function BottomTab() {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
|
||||
},
|
||||
tabBar: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
flexDirection: 'row',
|
||||
height: TAB_BAR_HEIGHT,
|
||||
backgroundColor: '#fff',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#eee',
|
||||
elevation: 5,
|
||||
height: 70,
|
||||
borderRadius: 35,
|
||||
elevation: 20, // stronger Android elevation
|
||||
shadowColor: '#000', // iOS shadow color
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: Platform.OS === 'ios' ? 0.25 : 0.2,
|
||||
shadowRadius: 12,
|
||||
overflow: 'visible', // allow shadow to render outside rounded corners
|
||||
zIndex: 9999,
|
||||
paddingBottom: 0,
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
tabButton: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
iconContainer: {
|
||||
marginBottom: 4,
|
||||
justifyContent: 'center',
|
||||
tabContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 4, // Drastically reduced from 12
|
||||
paddingVertical: 4,
|
||||
borderRadius: 20,
|
||||
},
|
||||
slidingIndicator: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
left: 5,
|
||||
height: 40,
|
||||
borderRadius: 30,
|
||||
zIndex: 0,
|
||||
},
|
||||
label: {
|
||||
fontSize: 11,
|
||||
textAlign: 'center',
|
||||
includeFontPadding: false,
|
||||
fontSize: 11, // Slightly smaller text
|
||||
textAlign: 'left',
|
||||
textTransform: 'capitalize',
|
||||
letterSpacing: 0.2, // Tighter spacing
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -4,35 +4,20 @@ import NavigationString from '../Constant/NavigationString';
|
||||
import Screens from '../Screens';
|
||||
import colors from '../Constant/Color';
|
||||
import BottomTab from './Bottom';
|
||||
import Login from '../Screens/Login/Login';
|
||||
|
||||
export default function MainStack() {
|
||||
const Stack = createStackNavigator();
|
||||
const option = { headerShown: false };
|
||||
const fadeTransition = {
|
||||
gestureDirection: 'horizontal',
|
||||
transitionSpec: {
|
||||
open: { animation: 'timing', config: { duration: 300 } },
|
||||
close: { animation: 'timing', config: { duration: 300 } },
|
||||
},
|
||||
cardStyleInterpolator: ({ current }) => ({
|
||||
cardStyle: {
|
||||
opacity: current.progress,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
// ...
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
cardStyle: { backgroundColor: colors.white },
|
||||
...fadeTransition
|
||||
}}
|
||||
// initialRouteName={NavigationString.DecryptionScreen}
|
||||
// ...
|
||||
>
|
||||
<Stack.Screen name={NavigationString.Splash} component={Screens.Splash} options={option} />
|
||||
<Stack.Screen name={NavigationString.Signup} component={Screens.Signup} options={option} />
|
||||
<Stack.Screen name={NavigationString.Onboarding} component={Screens.Onboarding} options={option} />
|
||||
<Stack.Screen name={NavigationString.Login} component={Screens.Login} options={option} />
|
||||
<Stack.Screen name={NavigationString.Login} component={Login} options={option} />
|
||||
<Stack.Screen name={NavigationString.Home} component={BottomTab} options={option} />
|
||||
<Stack.Screen name={NavigationString.PurchaseList} component={Screens.PurchaseList} options={option} />
|
||||
<Stack.Screen name={NavigationString.Emd} component={Screens.Emd} options={option} />
|
||||
@@ -50,6 +35,8 @@ export default function MainStack() {
|
||||
<Stack.Screen name={NavigationString.KycWebViewScreen} component={Screens.KycWebViewScreen} options={option} />
|
||||
<Stack.Screen name={NavigationString.StageThreeScreen} component={Screens.StageThreeScreen} options={option} />
|
||||
<Stack.Screen name={NavigationString.DecryptionScreen} component={Screens.DecryptionScreen} options={option} />
|
||||
<Stack.Screen name={NavigationString.DeviceBinding} component={Screens.DeviceBinding} options={option} />
|
||||
<Stack.Screen name={NavigationString.DscBinding} component={Screens.DscBinding} options={option} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -17,4 +17,5 @@ export const tokenSlice = createSlice({
|
||||
},
|
||||
});
|
||||
export const { setToken, removeToken } = tokenSlice.actions;
|
||||
export const getToken = (state: any) => state.token.token;
|
||||
export default tokenSlice.reducer;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {auctionSlice} from './Slices/SelectedAuction';
|
||||
import selectPurchaseAuctionReducer from './Slices/selectPurchaseAuction'
|
||||
import selectPurchaseTenderReducer from './Slices/SelectedPurchaseTender';
|
||||
import { tenderSlice } from './Slices/SelectedTender';
|
||||
import { themeSlice } from './Slices/ThemeSlice';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
token: tokenSlice.reducer,
|
||||
@@ -14,11 +15,12 @@ const rootReducer = combineReducers({
|
||||
selectPurchaseAuction: selectPurchaseAuctionReducer,
|
||||
selectPurchaseTender: selectPurchaseTenderReducer,
|
||||
tender: tenderSlice.reducer,
|
||||
theme: themeSlice.reducer,
|
||||
});
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
storage: AsyncStorage,
|
||||
whitelist: ['token'],
|
||||
whitelist: ['token', 'theme'],
|
||||
};
|
||||
const persistReducers = persistReducer(persistConfig, rootReducer);
|
||||
export const store = configureStore({
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import React, { useState, useRef, useMemo } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Animated, Dimensions } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Animated,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { InsideHeader } from '../../Component/Index';
|
||||
import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
|
||||
import colors from '../../Constant/Color';
|
||||
import LiveAuction from './LiveAuction';
|
||||
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 NavigationString from '../../Constant/NavigationString';
|
||||
import FontsFamily from '../../Constant/FontsFamily';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
const HORIZONTAL_PADDING = 32;
|
||||
const SWITCH_INSETS = 2;
|
||||
const TAB_HEIGHT = 44;
|
||||
const SWITCH_INSETS = 3;
|
||||
const TAB_HEIGHT = 48;
|
||||
|
||||
const Auction = () => {
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const Navigation = useNavigation() as any;
|
||||
const { data: liveAuctionData = {} }: any = useFetchLiveAuction();
|
||||
const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction();
|
||||
@@ -31,14 +38,14 @@ const Auction = () => {
|
||||
? purchaseAuctionData.data.length
|
||||
: 0;
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('live');
|
||||
const translateX = useRef(new Animated.Value(0)).current;
|
||||
|
||||
const TAB_WIDTH = useMemo(() => {
|
||||
const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2;
|
||||
return availableWidth / 2.4;
|
||||
return availableWidth / 2;
|
||||
}, []);
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('purchase');
|
||||
const translateX = useRef(new Animated.Value(TAB_WIDTH)).current;
|
||||
|
||||
const onTabPress = (tab: 'live' | 'purchase', index: number) => {
|
||||
setSelectedTab(tab);
|
||||
Animated.spring(translateX, {
|
||||
@@ -48,252 +55,295 @@ const Auction = () => {
|
||||
}).start();
|
||||
};
|
||||
|
||||
// Check if user is tender type
|
||||
const isTenderUser = reduxToken?.userType?.toLowerCase() === 'tender';
|
||||
const cartCount = SelectedPurchaseList.length;
|
||||
|
||||
// Render restricted access message
|
||||
const renderRestrictedAccess = () => (
|
||||
const styles = makeStyles(colors, spacing, shadows, TAB_WIDTH, TAB_HEIGHT);
|
||||
|
||||
if (isTenderUser) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<InsideHeader title="Auction" showArrow />
|
||||
<View style={styles.restrictedContainer}>
|
||||
<View style={styles.restrictedCard}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Lock size={64} color={colors.primary} />
|
||||
{/* Icon ring */}
|
||||
<View style={styles.lockRing}>
|
||||
<View style={styles.lockInner}>
|
||||
<Lock size={40} color={colors.primary} strokeWidth={1.8} />
|
||||
</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}>
|
||||
You are currently logged in as a Tender user. Auction features are not available for your account type.
|
||||
</Text>
|
||||
<AppText variant="body" centered color={colors.textSecondary} style={styles.restrictedMessage as any}>
|
||||
You're logged in as a{' '}
|
||||
<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}>
|
||||
To access auction features, please login with an Auction user account.
|
||||
</Text>
|
||||
<View style={styles.hintBox}>
|
||||
<AppText variant="bodySmall" centered color={colors.textSecondary}>
|
||||
Switch to an Auction account to access live & purchase auction features.
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.loginButton}
|
||||
activeOpacity={0.85}
|
||||
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>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<InsideHeader title="Auction" showArrow />
|
||||
|
||||
{isTenderUser ? (
|
||||
// Show restricted access message for tender users
|
||||
renderRestrictedAccess()
|
||||
) : (
|
||||
// Show normal auction UI for auction users
|
||||
<>
|
||||
{/* Tab Row */}
|
||||
<View style={styles.topRow}>
|
||||
{/* Segmented Control */}
|
||||
<View
|
||||
style={[
|
||||
styles.switchWrapper,
|
||||
{
|
||||
width: TAB_WIDTH * 2 + SWITCH_INSETS * 2,
|
||||
},
|
||||
{ width: TAB_WIDTH * 2 + SWITCH_INSETS * 2 },
|
||||
]}
|
||||
>
|
||||
{/* Sliding pill */}
|
||||
<Animated.View
|
||||
pointerEvents="none"
|
||||
style={[
|
||||
styles.activeTab,
|
||||
{
|
||||
width: TAB_WIDTH,
|
||||
transform: [{ translateX }],
|
||||
},
|
||||
{ width: TAB_WIDTH, transform: [{ translateX }] },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Live Tab */}
|
||||
<TouchableOpacity
|
||||
style={[styles.tab, { width: TAB_WIDTH }]}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => onTabPress('live', 0)}
|
||||
>
|
||||
<Text
|
||||
<Radio
|
||||
size={13}
|
||||
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<AppText
|
||||
variant="caption"
|
||||
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
|
||||
style={[
|
||||
styles.tabText,
|
||||
selectedTab === 'live' && styles.activeText,
|
||||
selectedTab === 'live' && (styles.activeTabText as any),
|
||||
]}
|
||||
>
|
||||
Live ({liveCount})
|
||||
</Text>
|
||||
Live {liveCount ? `(${liveCount})` : ''}
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Purchase Tab */}
|
||||
<TouchableOpacity
|
||||
style={[styles.tab, { width: TAB_WIDTH }]}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => onTabPress('purchase', 1)}
|
||||
>
|
||||
<Text
|
||||
<Package
|
||||
size={13}
|
||||
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<AppText
|
||||
variant="caption"
|
||||
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
|
||||
style={[
|
||||
styles.tabText,
|
||||
selectedTab === 'purchase' && styles.activeText,
|
||||
selectedTab === 'purchase' && (styles.activeTabText as any),
|
||||
]}
|
||||
>
|
||||
Purchased ({purchaseCount})
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
{SelectedPurchaseList.length ? (
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 2,
|
||||
borderColor: colors.white,
|
||||
backgroundColor: colors.primaryLight,
|
||||
position: 'absolute',
|
||||
borderRadius: 50,
|
||||
zIndex: 20,
|
||||
right: 0,
|
||||
top: -10,
|
||||
paddingHorizontal: 2,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 12, color: colors.black }}>
|
||||
{SelectedPurchaseList.length}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: colors.primaryLight,
|
||||
borderRadius: 20,
|
||||
}}
|
||||
onPress={() => {
|
||||
Navigation.navigate(NavigationString.PurchaseList);
|
||||
}}
|
||||
>
|
||||
<ShoppingCart size={22} color={colors.black} strokeWidth={2} />
|
||||
Purchased {purchaseCount ? `(${purchaseCount})` : ''}
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
{selectedTab === 'live' ? <LiveAuction /> : <PurchaseAuction />}
|
||||
</>
|
||||
|
||||
{/* Cart Floating Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.cartFloatingButton}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => Navigation.navigate(NavigationString.PurchaseList)}
|
||||
>
|
||||
<ShoppingCart size={24} color={colors.buttonText} strokeWidth={2} />
|
||||
{cartCount > 0 && (
|
||||
<View style={styles.cartBadge}>
|
||||
<AppText variant="caption" color={colors.primary} style={styles.cartBadgeText as any}>
|
||||
{cartCount}
|
||||
</AppText>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Auction;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
/* ─── Styles factory ─── */
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any, TAB_WIDTH: number, TAB_HEIGHT: number) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
|
||||
/* ── Tab Row ── */
|
||||
topRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: spacing.s + 2,
|
||||
},
|
||||
switchWrapper: {
|
||||
flexDirection: 'row',
|
||||
marginVertical: 14,
|
||||
backgroundColor: '#f1f5f9',
|
||||
borderRadius: 18,
|
||||
padding: 2,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 16,
|
||||
padding: SWITCH_INSETS,
|
||||
height: TAB_HEIGHT,
|
||||
...shadows.light,
|
||||
},
|
||||
tab: {
|
||||
height: TAB_HEIGHT - 4,
|
||||
height: TAB_HEIGHT - SWITCH_INSETS * 2,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 5,
|
||||
zIndex: 2,
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 13,
|
||||
color: '#64748b',
|
||||
lineHeight: 18,
|
||||
fontFamily: FontsFamily.poppinsSemiBold,
|
||||
},
|
||||
activeText: {
|
||||
color: colors.primary,
|
||||
},
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.3,
|
||||
} as any,
|
||||
activeTabText: {
|
||||
fontWeight: '700',
|
||||
} as any,
|
||||
activeTab: {
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
left: 2,
|
||||
height: TAB_HEIGHT - 4,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 6,
|
||||
elevation: 3,
|
||||
top: SWITCH_INSETS,
|
||||
left: SWITCH_INSETS,
|
||||
height: TAB_HEIGHT - SWITCH_INSETS * 2,
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 13,
|
||||
...shadows.medium,
|
||||
},
|
||||
|
||||
/* ── Cart Floating Button ── */
|
||||
cartFloatingButton: {
|
||||
position: 'absolute',
|
||||
bottom: spacing.xl + 80,
|
||||
right: spacing.l,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: colors.primary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...shadows.strong,
|
||||
zIndex: 999,
|
||||
},
|
||||
cartBadge: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.background,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 4,
|
||||
borderWidth: 1.5,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
cartBadgeText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
lineHeight: 13,
|
||||
} as any,
|
||||
|
||||
/* ── Restricted Access ── */
|
||||
restrictedContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 24,
|
||||
backgroundColor: colors.lightGray,
|
||||
padding: spacing.l,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
restrictedCard: {
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: 24,
|
||||
padding: 32,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 28,
|
||||
padding: spacing.l,
|
||||
alignItems: 'center',
|
||||
maxWidth: 400,
|
||||
width: '100%',
|
||||
|
||||
maxWidth: 400,
|
||||
...shadows.strong,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
backgroundColor: colors.primaryHighlight,
|
||||
justifyContent: 'center',
|
||||
lockRing: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: `${colors.primary}12`,
|
||||
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: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
color: colors.darkText,
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
fontFamily: FontsFamily.poppinsBold,
|
||||
},
|
||||
marginBottom: spacing.s + 4,
|
||||
} as any,
|
||||
restrictedMessage: {
|
||||
fontSize: 16,
|
||||
color: colors.grayText,
|
||||
textAlign: 'center',
|
||||
marginBottom: spacing.s,
|
||||
lineHeight: 24,
|
||||
marginBottom: 12,
|
||||
fontFamily: FontsFamily.poppinsRegular,
|
||||
},
|
||||
restrictedSubMessage: {
|
||||
fontSize: 14,
|
||||
color: colors.grayText,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
marginBottom: 32,
|
||||
fontFamily: FontsFamily.poppinsRegular,
|
||||
} as any,
|
||||
hintBox: {
|
||||
backgroundColor: colors.overlay,
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: spacing.s + 2,
|
||||
marginBottom: spacing.l,
|
||||
width: '100%',
|
||||
},
|
||||
loginButton: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: 32,
|
||||
backgroundColor: colors.buttonPrimary,
|
||||
paddingHorizontal: spacing.l,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
shadowColor: colors.primary,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
loginButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
fontFamily: FontsFamily.poppinsSemiBold,
|
||||
borderRadius: 16,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
...shadows.medium,
|
||||
},
|
||||
loginBtnText: {
|
||||
letterSpacing: 1.2,
|
||||
} as any,
|
||||
});
|
||||
|
||||
@@ -1,55 +1,76 @@
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
|
||||
import { RefreshCw } from 'lucide-react-native';
|
||||
import LiveAuctionTile from './LiveAuctionTile';
|
||||
import { useFetchLiveAuction } from '../../Services/ApiCall/Hooks';
|
||||
import { SearchInput } from '../../Component/Serach/SeacrhInput';
|
||||
import { Skelton } from '../../Component/Index';
|
||||
import colors from '../../Constant/Color';
|
||||
import SkeltonList from '../../Component/Skelton/SkeltonList';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
|
||||
import { apiCall } from '../../Axios/Axios';
|
||||
import { GET_MY_AUCTION } from '../../Services/ApiCall/ApiUrls';
|
||||
import SkeltonList from '../../Component/Skelton/SkeltonList';
|
||||
import Skeleton from '../../Component/Skelton/Skelton';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
|
||||
const LiveAuction = () => {
|
||||
const Dispatch = useDispatch()
|
||||
const Dispatch = useDispatch();
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const user = useSelector((state: any) => state.token?.token);
|
||||
const onEndReachedCalledDuringMomentum = useRef(true);
|
||||
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
|
||||
const [showRefreshBanner, setShowRefreshBanner] = useState(false);
|
||||
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(
|
||||
({ item, index }: any) => {
|
||||
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.auctionNumber === item.auctionNumber)
|
||||
return <LiveAuctionTile
|
||||
const isPurchase = SelectedPurchaseList.findIndex(
|
||||
(prev: any) => prev.auctionNumber === item.auctionNumber,
|
||||
);
|
||||
return (
|
||||
<LiveAuctionTile
|
||||
data={item}
|
||||
index={index}
|
||||
isPurchasedByContractor={item.contractorId?.includes(String(user.contractorId))}
|
||||
isPurchasedByContractor={item.contractorId?.includes(
|
||||
String(user.contractorId),
|
||||
)}
|
||||
auctionId={item.auctionId}
|
||||
auctionDispatchDate={item.auctionDispatchDate}
|
||||
isPurchased={isPurchase !== -1 ? true : false}
|
||||
isPurchased={isPurchase !== -1}
|
||||
purchaseEnd={item.purchaseEnd}
|
||||
auctionOfWork={item.auctionOfWork}
|
||||
auctionFees={item.auctionFees}
|
||||
auctionNumber={item.auctionNumber}
|
||||
onPurchasePress={() => { Dispatch(togglePurchaseAuction(item)) }}
|
||||
onPurchasePress={() => {
|
||||
console.log('[LiveAuction] purchase toggled:', item?.auctionNumber);
|
||||
Dispatch(togglePurchaseAuction(item));
|
||||
}}
|
||||
onDetailsPress={() => {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[SelectedPurchaseList]
|
||||
[SelectedPurchaseList],
|
||||
);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (onEndReachedCalledDuringMomentum.current) return;
|
||||
|
||||
if (!hasNextPage || isFetchingNextPage) return;
|
||||
|
||||
console.log('[LiveAuction] loadMore: fetching next page');
|
||||
fetchNextPage();
|
||||
onEndReachedCalledDuringMomentum.current = true;
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
@@ -57,86 +78,80 @@ const LiveAuction = () => {
|
||||
const renderFooter = useCallback(() => {
|
||||
if (!isFetchingNextPage) return null;
|
||||
return (
|
||||
<View style={{ paddingVertical: 12 }}>
|
||||
<View style={styles.footerLoader}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<AppText variant="caption" color={colors.textSecondary}>
|
||||
Loading more…
|
||||
</AppText>
|
||||
</View>
|
||||
);
|
||||
}, [isFetchingNextPage]);
|
||||
}, [isFetchingNextPage, colors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRefetching) {
|
||||
console.log('[LiveAuction] refetch started');
|
||||
setShowRefreshBanner(true);
|
||||
} else {
|
||||
const timer = setTimeout(() => {
|
||||
setShowRefreshBanner(false);
|
||||
}, 1000);
|
||||
|
||||
console.log('[LiveAuction] refetch completed');
|
||||
const timer = setTimeout(() => setShowRefreshBanner(false), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isRefetching]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[LiveAuction] data updated:', {
|
||||
count: data?.result?.length ?? 0,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isPending,
|
||||
});
|
||||
}, [data, hasNextPage, isFetchingNextPage, isPending]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Skelton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<SearchInput onChange={() => { }} placeholder="Search...." filter={false} />
|
||||
<View style={styles.root}>
|
||||
{/* Search */}
|
||||
<SearchInput onChange={() => {}} placeholder="Search auctions…" filter={false} />
|
||||
|
||||
{/* Refresh Banner */}
|
||||
{showRefreshBanner && (
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(200)}
|
||||
exiting={FadeOutUp.duration(500)}
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 14,
|
||||
marginTop: 6,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 8,
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
elevation: 4,
|
||||
zIndex: 10, // Ensure it stays on top
|
||||
position: 'absolute',
|
||||
top: 60, // Adjust based on your SearchInput height
|
||||
}}
|
||||
exiting={FadeOutUp.duration(400)}
|
||||
style={[
|
||||
styles.refreshBanner,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
...shadows.medium,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
Refreshing auctions
|
||||
</Text>
|
||||
<AppText variant="caption" color={colors.text} style={styles.refreshText as any}>
|
||||
Refreshing auctions…
|
||||
</AppText>
|
||||
</Animated.View>
|
||||
)}
|
||||
{
|
||||
isPending ? <Skeleton /> :
|
||||
|
||||
{/* List */}
|
||||
{isPending ? (
|
||||
<SkeltonList />
|
||||
) : (
|
||||
<FlatList
|
||||
data={data?.result ?? []}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item?.auctionId?.toString()}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 50, paddingTop: 10 }}
|
||||
|
||||
contentContainerStyle={styles.listContent}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.1}
|
||||
|
||||
onMomentumScrollBegin={() => {
|
||||
onEndReachedCalledDuringMomentum.current = false;
|
||||
}}
|
||||
|
||||
ListFooterComponent={renderFooter}
|
||||
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefetching}
|
||||
@@ -145,18 +160,48 @@ const LiveAuction = () => {
|
||||
colors={[colors.primary]}
|
||||
/>
|
||||
}
|
||||
|
||||
initialNumToRender={6}
|
||||
maxToRenderPerBatch={6}
|
||||
windowSize={7}
|
||||
removeClippedSubviews
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import React, { memo } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
import moment from 'moment';
|
||||
import colors from '../../Constant/Color';
|
||||
import { LiveAuctionTileProps } from './types/types';
|
||||
import AuctionDetail from './Modal/AuctionDetail';
|
||||
import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native';
|
||||
import {
|
||||
Tag,
|
||||
Calendar,
|
||||
Clock,
|
||||
ShoppingCart,
|
||||
Trash2,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
} from 'lucide-react-native';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({
|
||||
const LiveAuctionTile: React.FC<LiveAuctionTileProps & { index?: number }> = ({
|
||||
data,
|
||||
index = 0,
|
||||
isPurchased = false,
|
||||
onPurchasePress,
|
||||
auctionDispatchDate,
|
||||
@@ -15,231 +28,252 @@ const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({
|
||||
purchaseEnd,
|
||||
auctionFees,
|
||||
auctionNumber,
|
||||
isPurchasedByContractor
|
||||
isPurchasedByContractor,
|
||||
}) => {
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const [showDetail, setShowDetail] = React.useState(false);
|
||||
|
||||
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss'));
|
||||
const deadlineFormatted = moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY');
|
||||
const Navigation = useNavigation() as any;
|
||||
const getStatus = () => {
|
||||
if (isPurchasedByContractor) return 'purchased';
|
||||
if (purchaseEnded) return 'expired';
|
||||
if (isPurchased) return 'in_cart';
|
||||
return 'active';
|
||||
};
|
||||
const status = getStatus();
|
||||
|
||||
const styles = makeStyles(colors, spacing, shadows);
|
||||
|
||||
if (showDetail) {
|
||||
return (
|
||||
<View style={styles.cardWrapper}>
|
||||
{
|
||||
!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 style={styles.card}>
|
||||
<AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
|
||||
</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>
|
||||
<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);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cardWrapper: {
|
||||
marginVertical: 10,
|
||||
marginHorizontal: 16,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 24,
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any) =>
|
||||
StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
marginVertical: 7,
|
||||
padding: spacing.m,
|
||||
...shadows.medium,
|
||||
},
|
||||
|
||||
// Modern shadow
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 20,
|
||||
elevation: 8,
|
||||
},
|
||||
statusTag: {
|
||||
width: 6,
|
||||
height: '100%',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
},
|
||||
header: {
|
||||
/* Top row */
|
||||
topRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
badge: {
|
||||
numberRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary + '25',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 12,
|
||||
gap: 6,
|
||||
gap: 5,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 12,
|
||||
auctionNo: {
|
||||
fontWeight: '700',
|
||||
color: colors.primary,
|
||||
},
|
||||
priceText: {
|
||||
fontSize: 20,
|
||||
} as any,
|
||||
fee: {
|
||||
fontWeight: '800',
|
||||
color: '#0f172a',
|
||||
},
|
||||
workTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#334155',
|
||||
} as any,
|
||||
|
||||
/* Title */
|
||||
title: {
|
||||
fontWeight: '700',
|
||||
lineHeight: 24,
|
||||
marginBottom: 16,
|
||||
},
|
||||
infoGrid: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: '#f0f4f8',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 16,
|
||||
marginBottom: 18,
|
||||
},
|
||||
infoItem: {
|
||||
marginBottom: spacing.m,
|
||||
} as any,
|
||||
|
||||
/* Date strip */
|
||||
dateStrip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
borderRadius: 12,
|
||||
paddingVertical: spacing.s + 2,
|
||||
paddingHorizontal: spacing.m,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
infoTextGroup: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 10,
|
||||
color: '#94a3b8',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: '700',
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
color: '#1e293b',
|
||||
},
|
||||
actionBar: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
iconBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#e2e8f0',
|
||||
},
|
||||
iconBtnText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: colors.primary,
|
||||
},
|
||||
mainButton: {
|
||||
dateCol: {
|
||||
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',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
gap: 6,
|
||||
},
|
||||
btnActive: {
|
||||
backgroundColor: '#4ade80', // light green
|
||||
},
|
||||
btnPurchased: {
|
||||
backgroundColor: colors.error,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.error,
|
||||
},
|
||||
btnDisabled: {
|
||||
backgroundColor: '#e2e8f0',
|
||||
},
|
||||
mainButtonText: {
|
||||
fontSize: 14,
|
||||
ctaText: {
|
||||
fontWeight: '800',
|
||||
},
|
||||
letterSpacing: 0.8,
|
||||
} as any,
|
||||
});
|
||||
|
||||
@@ -1,64 +1,66 @@
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, StatusBar, Modal } from 'react-native';
|
||||
import { Briefcase, Calendar, FileText, Download, Eye, Layers, ArrowLeft, Clock } from 'lucide-react-native';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Modal,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import {
|
||||
Briefcase,
|
||||
Calendar,
|
||||
FileText,
|
||||
Download,
|
||||
Eye,
|
||||
Layers,
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
IndianRupee,
|
||||
Wrench,
|
||||
Mail,
|
||||
} from 'lucide-react-native';
|
||||
import { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/ApiCall/Hooks';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
|
||||
import ReactNativeBlobUtil from 'react-native-blob-util';
|
||||
import AuctionItemDetail from './AuctionItemDetail';
|
||||
import { Loader } from '../../../Component/Index';
|
||||
import colors from '../../../Constant/Color';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
import moment from 'moment';
|
||||
|
||||
interface Props { visible: boolean; onClose: () => void; data?: any }
|
||||
|
||||
const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
||||
const inset = useSafeAreaInsets();
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
|
||||
const inset = useSafeAreaInsets()
|
||||
const { data: auctionData, isLoading } = useGetAuciton({ auctionId: data?.auctionId })
|
||||
const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData)
|
||||
const [allBoolean, setAllBoolean] = useState({ showItemDetail: false, selectedItem: {}, fileLoading: false });
|
||||
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId })
|
||||
const { mutate } = useAuctionItem()
|
||||
const { data: auctionData } = useGetAuciton({ auctionId: data?.auctionId });
|
||||
const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData);
|
||||
const [allBoolean, setAllBoolean] = useState({
|
||||
showItemDetail: false,
|
||||
selectedItem: {},
|
||||
fileLoading: false,
|
||||
});
|
||||
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId });
|
||||
const { mutate } = useAuctionItem();
|
||||
|
||||
const formatDate = (date?: string) => {
|
||||
if (!date) return '-';
|
||||
if (!date) return '—';
|
||||
return moment(date, 'DD-MM-YYYY HH:mm:ss').format('DD MMM YYYY, hh:mm A');
|
||||
};
|
||||
|
||||
const isLive = data?.purchaseEnd ? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss')) : false;
|
||||
|
||||
const Section = ({ title, icon: Icon, children }: any) => (
|
||||
<View style={styles.sectionContainer}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Icon size={20} color={colors.primary} />
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
|
||||
const InfoBox = ({ label, value, full = false }: any) => (
|
||||
<View style={[styles.infoBox, full ? { width: '100%' } : { width: '48%' }]}>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<Text style={styles.value}>{value || '-'}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const isLive = data?.purchaseEnd
|
||||
? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss'))
|
||||
: false;
|
||||
|
||||
const handleDownload = async (id: number, fileName: string) => {
|
||||
try {
|
||||
setAllBoolean((prev: any) => ({ ...prev, fileLoading: true }));
|
||||
setAllBoolean(prev => ({ ...prev, fileLoading: true }));
|
||||
const { dirs } = ReactNativeBlobUtil.fs;
|
||||
|
||||
const path = `${dirs.DownloadDir}/${fileName}.pdf`;
|
||||
|
||||
|
||||
|
||||
const downloadUrl = `${AUCTION_DOCUMENT_DOWNLOAD}?documentId=${id}&isAmendment=false`;
|
||||
|
||||
|
||||
const response = await ReactNativeBlobUtil.config({
|
||||
await ReactNativeBlobUtil.config({
|
||||
fileCache: false,
|
||||
addAndroidDownloads: {
|
||||
useDownloadManager: true,
|
||||
@@ -69,315 +71,340 @@ const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
||||
description: 'Downloading document',
|
||||
mediaScannable: true,
|
||||
},
|
||||
}).fetch('GET', downloadUrl, {
|
||||
Accept: 'application/pdf',
|
||||
});
|
||||
|
||||
const stat = await ReactNativeBlobUtil.fs.stat(path);
|
||||
|
||||
}).fetch('GET', downloadUrl, { Accept: 'application/pdf' });
|
||||
} catch (error: any) {
|
||||
console.log('Error:', error?.message || error);
|
||||
} finally {
|
||||
setAllBoolean((prev: any) => ({ ...prev, fileLoading: false }));
|
||||
setAllBoolean(prev => ({ ...prev, fileLoading: false }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
mutate({ auctionNumber: data?.auctionNumber, status: data?.auctionPattern }, {
|
||||
onSuccess: (res) => {
|
||||
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments }))
|
||||
}, onError: (err) => {
|
||||
mutate(
|
||||
{ auctionNumber: data?.auctionNumber, status: data?.auctionPattern },
|
||||
{
|
||||
onSuccess: (res: any) => {
|
||||
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments }));
|
||||
},
|
||||
onError: (err: any) => {
|
||||
console.log(err, '-----auction item error');
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}, [data.auctionNumber, auctionDocuments]);
|
||||
|
||||
const isAmended = auctionData?.auctionStatus === 'Amended';
|
||||
const kd = (key: any, amended: any) => isAmended ? amended : key;
|
||||
|
||||
const styles = makeStyles(colors, spacing, shadows);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent={false}
|
||||
animationType="none"
|
||||
animationType="slide"
|
||||
statusBarTranslucent
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.safeArea}>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
|
||||
{/* Full Header */}
|
||||
<View style={[styles.header, { paddingTop: inset.top }]}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
|
||||
<ArrowLeft size={24} color="#1e293b" />
|
||||
<View style={styles.root}>
|
||||
{/* ── Header ── */}
|
||||
<View style={[styles.header, { paddingTop: inset.top + 6 }]}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.backBtn} activeOpacity={0.7}>
|
||||
<ArrowLeft size={22} color={colors.text} strokeWidth={2.5} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.headerTitleContainer}>
|
||||
<Text style={styles.headerTitleText}>Auction Overview</Text>
|
||||
<Text style={styles.headerIdText}>Ref: {data?.auctionNumber || 'N/A'}</Text>
|
||||
|
||||
<View style={styles.headerCenter}>
|
||||
<AppText variant="h4" color={colors.text} style={styles.headerTitle as any}>
|
||||
Auction Overview
|
||||
</AppText>
|
||||
<AppText variant="caption" color={colors.textSecondary}>
|
||||
Ref: {data?.auctionNumber ?? 'N/A'}
|
||||
</AppText>
|
||||
</View>
|
||||
<View style={styles.liveIndicator}>
|
||||
<View style={[styles.dot, { backgroundColor: isLive ? '#22c55e' : '#94a3b8' }]} />
|
||||
<Text style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</Text>
|
||||
|
||||
<View style={[styles.statusChip, { borderColor: isLive ? `${colors.success}50` : colors.border }]}>
|
||||
<View style={[styles.statusDot, { backgroundColor: isLive ? colors.success : colors.textSecondary }]} />
|
||||
<AppText variant="caption" color={isLive ? colors.success : colors.textSecondary} style={styles.statusText as any}>
|
||||
{isLive ? 'LIVE' : 'CLOSED'}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Scrollable Body */}
|
||||
{/* ── Body ── */}
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={true}
|
||||
style={{ backgroundColor: colors.background }}
|
||||
>
|
||||
{/* 1. Basic Info Grid */}
|
||||
<Section title="Details" icon={Layers}>
|
||||
<View style={styles.grid}>
|
||||
<InfoBox label="Auction No." value={data?.auctionNumber} />
|
||||
<InfoBox label="Pattern" value={data?.auctionPattern || 'Forward'} />
|
||||
<InfoBox label="Mode" value={data?.auctionMode || 'Open'} />
|
||||
<InfoBox label="Category" value={data?.categoryOfAuction || 'General'} />
|
||||
<InfoBox label="EMD Type" value={data?.emd || 'Online'} />
|
||||
<InfoBox label="EMD Figure" value={data?.emdFigure || 'Item Wise'} />
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
{/* 2. Work Description */}
|
||||
<Section title="Auction Details" icon={Briefcase}>
|
||||
<View style={styles.grid}>
|
||||
<InfoBox label="Auction Name" value={auctionData?.auctionOfWork} />
|
||||
<InfoBox label="Category Of Auction " value={auctionData?.auctionCategories} />
|
||||
<InfoBox label="Auction Fees (In Figure) " value={auctionData?.auctionFees} />
|
||||
<InfoBox label="Auction Fees (In Words)" value={auctionData?.auctionFeesInWord} />
|
||||
<InfoBox label="Auction Keyword" value={auctionData?.auctionKeywordName} />
|
||||
</View>
|
||||
</Section>
|
||||
{/* BASIC DETAILS */}
|
||||
<SectionCard title="Basic Details" icon={Layers} colors={colors} spacing={spacing} shadows={shadows}>
|
||||
<InfoRow label="Auction No." value={data?.auctionNumber} colors={colors} />
|
||||
<InfoRow label="Pattern" value={data?.auctionPattern || 'Forward'} colors={colors} />
|
||||
<InfoRow label="Mode" value={data?.auctionMode || 'Open'} colors={colors} />
|
||||
<InfoRow label="Category" value={data?.categoryOfAuction || 'General'} colors={colors} />
|
||||
<InfoRow label="EMD Type" value={data?.emd || 'Online'} colors={colors} last />
|
||||
</SectionCard>
|
||||
|
||||
{/* 3. Timeline Section */}
|
||||
<Section title="Key Dates" icon={Calendar}>
|
||||
<View style={styles.timelineContainer}>
|
||||
<View style={styles.timelineRow}>
|
||||
<View style={styles.iconCircle}><Clock size={16} color="#64748b" /></View>
|
||||
<View>
|
||||
<Text style={styles.label}>Auction Purchase (Start)</Text>
|
||||
<Text style={styles.value}>{formatDate(
|
||||
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>
|
||||
{/* AUCTION DETAILS */}
|
||||
<SectionCard title="Auction Details" icon={Briefcase} colors={colors} spacing={spacing} shadows={shadows}>
|
||||
<InfoRow label="Name" value={auctionData?.auctionOfWork} colors={colors} multiline />
|
||||
<InfoRow label="Category" value={auctionData?.auctionCategories} colors={colors} />
|
||||
<InfoRow label="Fees" value={auctionData?.auctionFees ? `₹ ${auctionData.auctionFees}` : '—'} colors={colors} valueColor={colors.primary} />
|
||||
<InfoRow label="Fees (Words)" value={auctionData?.auctionFeesInWord} colors={colors} multiline />
|
||||
<InfoRow label="Keyword" value={auctionData?.auctionKeywordName} colors={colors} last />
|
||||
</SectionCard>
|
||||
|
||||
</View>
|
||||
<View style={styles.timelineConnector} />
|
||||
<View style={styles.timelineRow}>
|
||||
<View style={styles.iconCircle}><Clock size={16} color="#64748b" /></View>
|
||||
<View>
|
||||
<Text style={styles.label}>Bidding Start (Start)</Text>
|
||||
<Text style={styles.value}>{formatDate(
|
||||
auctionData?.auctionStatus === "Amended" ? auctionData?.amendedkeyDates.auctionBidding.startDateTime : auctionData?.keyDates.auctionBidding.startDateTime)}</Text>
|
||||
</View>
|
||||
{/* KEY DATES */}
|
||||
<SectionCard title="Key Dates" icon={Calendar} colors={colors} spacing={spacing} shadows={shadows}>
|
||||
<InfoRow label="Purchase Start" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.startDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.startDateTime))} colors={colors} />
|
||||
<InfoRow label="Purchase End" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.endDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.endDateTime))} colors={colors} valueColor={colors.error} />
|
||||
<InfoRow label="Bidding Start" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.startDateTime, auctionData?.amendedkeyDates?.auctionBidding?.startDateTime))} colors={colors} />
|
||||
<InfoRow label="Bidding End" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.endDateTime, auctionData?.amendedkeyDates?.auctionBidding?.endDateTime))} colors={colors} valueColor={colors.error} last />
|
||||
</SectionCard>
|
||||
|
||||
{/* AUCTION ITEMS */}
|
||||
{aucitonDetail?.auctionItems?.length > 0 && (
|
||||
<SectionCard title="Auction Items" icon={FileText} colors={colors} spacing={spacing} shadows={shadows}>
|
||||
{aucitonDetail.auctionItems.map((item: any, index: number) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.itemRow,
|
||||
{ borderBottomColor: colors.border },
|
||||
index === aucitonDetail.auctionItems.length - 1 && { borderBottomWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<View style={styles.itemLeft}>
|
||||
<AppText variant="bodySmall" color={colors.text} style={styles.itemName as any}>
|
||||
{item.productName}
|
||||
</AppText>
|
||||
<AppText variant="caption" color={colors.textSecondary}>
|
||||
{item?.categoryName} · {item.timeDurationType}
|
||||
</AppText>
|
||||
</View>
|
||||
<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}>Bidding Deadline (End)</Text>
|
||||
<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
|
||||
style={[styles.eyeBtn, { backgroundColor: `${colors.primary}15` }]}
|
||||
onPress={() => setAllBoolean(prev => ({ ...prev, showItemDetail: true, selectedItem: item }))}
|
||||
>
|
||||
<Eye size={16} color={colors.primary} strokeWidth={2} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
{/* 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" />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
))}
|
||||
</Section>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
</ScrollView>
|
||||
|
||||
{allBoolean.showItemDetail && <AuctionItemDetail data={allBoolean.selectedItem} onClose={() => { setAllBoolean((prev: any) => ({ ...prev, showItemDetail: false })) }} visible={allBoolean.showItemDetail} />}
|
||||
{allBoolean.showItemDetail && (
|
||||
<AuctionItemDetail
|
||||
data={allBoolean.selectedItem}
|
||||
onClose={() => setAllBoolean(prev => ({ ...prev, showItemDetail: false }))}
|
||||
visible={allBoolean.showItemDetail}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fullModal: {
|
||||
margin: 0,
|
||||
backgroundColor: '#fff',
|
||||
export default memo(AuctionFullDetail);
|
||||
|
||||
/* ─── Sub-components ─── */
|
||||
|
||||
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,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
|
||||
/* Header */
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#f1f5f9',
|
||||
backgroundColor: '#fff',
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingBottom: spacing.m,
|
||||
backgroundColor: colors.surface,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.border,
|
||||
gap: spacing.s,
|
||||
},
|
||||
backBtn: {
|
||||
padding: 8,
|
||||
marginRight: 8,
|
||||
},
|
||||
headerTitleContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
headerTitleText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
color: '#0f172a',
|
||||
},
|
||||
headerIdText: {
|
||||
fontSize: 13,
|
||||
color: '#64748b',
|
||||
},
|
||||
liveIndicator: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f8fafc',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 20,
|
||||
gap: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
dot: { width: 8, height: 8, borderRadius: 4 },
|
||||
liveText: { fontSize: 11, fontWeight: '800', color: '#475569' },
|
||||
|
||||
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,
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: colors.primary,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
btnDisabled: { backgroundColor: '#cbd5e1' },
|
||||
mainBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' },
|
||||
});
|
||||
headerCenter: {
|
||||
flex: 1,
|
||||
},
|
||||
headerTitle: {
|
||||
fontWeight: '800',
|
||||
} as any,
|
||||
statusChip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.s + 4,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1.2,
|
||||
gap: 5,
|
||||
},
|
||||
statusDot: {
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: 4,
|
||||
},
|
||||
statusText: {
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0.5,
|
||||
} as any,
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -1,38 +1,43 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import React, { memo, useState, useMemo } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, Modal } from 'react-native';
|
||||
import { Package, IndianRupee, FileText, Download, ArrowLeft, Info, } from 'lucide-react-native';
|
||||
import colors from '../../../Constant/Color';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useAucitonItemDetail } from '../../../Services/ApiCall/Hooks';
|
||||
import ReactNativeBlobUtil from 'react-native-blob-util';
|
||||
import { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
|
||||
import { Loader } from '../../../Component/Index';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
|
||||
interface Props { visible: boolean; onClose: () => void; data?: any; }
|
||||
|
||||
const Section = ({ title, icon: Icon, children }: any) => (
|
||||
const Section = ({ title, icon: Icon, children, styles, colors }: any) => (
|
||||
<View style={styles.sectionCard}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={styles.iconCircle}>
|
||||
<Icon size={18} color={colors.primary} strokeWidth={2.5} />
|
||||
</View>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
<AppText variant="caption" bold style={styles.sectionTitle}>{title}</AppText>
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
|
||||
const PriceTile = ({ label, fig, words, full = false }: any) => (
|
||||
const PriceTile = ({ label, fig, words, full = false, styles }: any) => (
|
||||
<View style={[styles.priceTile, full ? { width: '100%' } : { width: '48%' }]}>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<Text style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</Text>
|
||||
<Text style={styles.priceWords}>{words}</Text>
|
||||
<AppText variant="caption" color={styles.label.color} bold style={styles.label}>{label}</AppText>
|
||||
<AppText variant="h3" bold style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</AppText>
|
||||
<AppText variant="caption" style={styles.priceWords}>{words}</AppText>
|
||||
</View>
|
||||
);
|
||||
|
||||
const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
||||
const inset = useSafeAreaInsets()
|
||||
const { colors, typography } = useTheme();
|
||||
const styles = useMemo(() => createStyles(colors, typography), [colors, typography]);
|
||||
|
||||
const [allBoolean, setAllBoolean] = useState({ fileLoading: false });
|
||||
const { data: itemData } = useAucitonItemDetail({ auctionItemId: data.id });
|
||||
const { data: itemData } = useAucitonItemDetail({ auctionItemId: data?.id });
|
||||
|
||||
const handleDownload = async (id: number, fileName: string) => {
|
||||
try {
|
||||
@@ -41,11 +46,8 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
||||
|
||||
const path = `${dirs.DownloadDir}/${fileName}.pdf`;
|
||||
|
||||
|
||||
|
||||
const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`;
|
||||
|
||||
|
||||
const response = await ReactNativeBlobUtil.config({
|
||||
fileCache: false,
|
||||
addAndroidDownloads: {
|
||||
@@ -83,11 +85,11 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
||||
{/* Navbar */}
|
||||
<View style={[styles.header, { paddingTop: inset.top }]}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
|
||||
<ArrowLeft size={24} color="#1e293b" />
|
||||
<ArrowLeft size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<View>
|
||||
<Text style={styles.headerTitle}>Item Information</Text>
|
||||
<Text style={styles.headerSub}>Detail View</Text>
|
||||
<AppText variant="body" bold style={styles.headerTitle}>Item Information</AppText>
|
||||
<AppText variant="caption" style={styles.headerSub}>Detail View</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -96,69 +98,66 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Section title="Product Specification" icon={Package}>
|
||||
<Section title="Product Specification" icon={Package} styles={styles} colors={colors}>
|
||||
<View style={styles.identityHeader}>
|
||||
<Text style={styles.productNameText}>{data.productName.toUpperCase()}</Text>
|
||||
<AppText variant="h4" bold style={styles.productNameText}>{data?.productName?.toUpperCase()}</AppText>
|
||||
<View style={styles.categoryBadge}>
|
||||
<Text style={styles.categoryText}>{data.categoryName}</Text>
|
||||
<AppText variant="caption" bold style={styles.categoryText}>{data?.categoryName}</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.specGrid}>
|
||||
<View style={styles.specItem}>
|
||||
<Text style={styles.label}>Sub Category</Text>
|
||||
<Text style={styles.specValue}>{data.subCategoryName}</Text>
|
||||
<AppText variant="caption" bold style={styles.label}>Sub Category</AppText>
|
||||
<AppText variant="bodySmall" bold style={styles.specValue}>{data?.subCategoryName}</AppText>
|
||||
</View>
|
||||
<View style={styles.specItem}>
|
||||
<Text style={styles.label}>Duration</Text>
|
||||
<Text style={styles.specValue}>{data.timeDurationType}</Text>
|
||||
<AppText variant="caption" bold style={styles.label}>Duration</AppText>
|
||||
<AppText variant="bodySmall" bold style={styles.specValue}>{data?.timeDurationType}</AppText>
|
||||
</View>
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Reserve & Bid Values" icon={IndianRupee}>
|
||||
<Section title="Reserve & Bid Values" icon={IndianRupee} styles={styles} colors={colors}>
|
||||
<PriceTile
|
||||
label="Bidding Price"
|
||||
fig={data.reservePrice || data.emdAmount}
|
||||
words={data.reservePriceInWords || data.emdAmountInWords}
|
||||
fig={data?.reservePrice || data?.emdAmount}
|
||||
words={data?.reservePriceInWords || data?.emdAmountInWords}
|
||||
full
|
||||
styles={styles}
|
||||
/>
|
||||
<View style={styles.hDivider} />
|
||||
<PriceTile
|
||||
label="Auction Start Value"
|
||||
fig={data.reservePrice || data.auctionStartValueFigure}
|
||||
words={data.reservePriceInWords || data.auctionStartValueWords}
|
||||
fig={data?.reservePrice || data?.auctionStartValueFigure}
|
||||
words={data?.reservePriceInWords || data?.auctionStartValueWords}
|
||||
full
|
||||
styles={styles}
|
||||
/>
|
||||
<View style={styles.hDivider} />
|
||||
{/* <View style={styles.row}>
|
||||
<PriceTile label="Bid Variation" fig={data.bidVariationFig} words={data.bidVariationWords} />
|
||||
<PriceTile label="Auction Start Value" fig={data.auctionStartValue} words={data.auctionStartValueInWords
|
||||
} />
|
||||
</View> */}
|
||||
</Section>
|
||||
|
||||
<Section title="Brief Description" icon={Info}>
|
||||
<Text style={styles.descContent}>{data.briefDescription}</Text>
|
||||
<Section title="Brief Description" icon={Info} styles={styles} colors={colors}>
|
||||
<AppText variant="bodySmall" style={styles.descContent}>{data?.briefDescription}</AppText>
|
||||
</Section>
|
||||
|
||||
{itemData?.length && <Section title="View Documents" icon={FileText}>
|
||||
{itemData?.length && <Section title="View Documents" icon={FileText} styles={styles} colors={colors}>
|
||||
<View style={styles.docTableHead}>
|
||||
<Text style={styles.tableLabel}>Sr No.</Text>
|
||||
<Text style={[styles.tableLabel, { flex: 2 }]}>Document Name</Text>
|
||||
<Text style={styles.tableLabel}>Action</Text>
|
||||
<AppText variant="caption" bold style={styles.tableLabel}>Sr No.</AppText>
|
||||
<AppText variant="caption" bold style={[styles.tableLabel, { flex: 2 }]}>Document Name</AppText>
|
||||
<AppText variant="caption" bold style={styles.tableLabel}>Action</AppText>
|
||||
</View>
|
||||
|
||||
{itemData?.map((doc: any, index: any) => (
|
||||
<TouchableOpacity style={styles.docRow} activeOpacity={0.7} onPress={() => { handleDownload(doc.assetId, doc.fileName) }} key={doc.assetId}>
|
||||
<Text style={styles.docIndex}>{index + 1}</Text>
|
||||
<AppText variant="bodySmall" bold style={styles.docIndex}>{index + 1}</AppText>
|
||||
<View style={{ flex: 2 }}>
|
||||
<Text style={styles.docTitle} numberOfLines={1}>{doc.fileName}</Text>
|
||||
<AppText variant="bodySmall" bold style={styles.docTitle} numberOfLines={1}>{doc.fileName}</AppText>
|
||||
</View>
|
||||
<View style={styles.downloadIconBox}>
|
||||
{allBoolean.fileLoading ? (
|
||||
<View >
|
||||
<Loader size='small' color='#94a3b8' />
|
||||
<Loader size='small' color={colors.textSecondary} />
|
||||
</View>
|
||||
) : (
|
||||
<Download size={20} color={colors.primary} />
|
||||
@@ -173,26 +172,26 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fullModal: { margin: 0, backgroundColor: '#f8fafc' },
|
||||
safeArea: { flex: 1 },
|
||||
const createStyles = (colors: any, typography: any) => StyleSheet.create({
|
||||
fullModal: { margin: 0, backgroundColor: colors.background },
|
||||
safeArea: { flex: 1, backgroundColor: colors.background },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: colors.surface,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#f1f5f9',
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
backBtn: { padding: 8, marginRight: 8 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '800', color: '#0f172a' },
|
||||
headerSub: { fontSize: 12, color: '#94a3b8', fontWeight: '600', textTransform: 'uppercase' },
|
||||
headerTitle: { fontSize: 18, color: colors.text },
|
||||
headerSub: { fontSize: 12, color: colors.textSecondary, textTransform: 'uppercase' },
|
||||
|
||||
scrollView: { flex: 1 },
|
||||
scrollContent: { padding: 16, paddingBottom: 50 },
|
||||
|
||||
sectionCard: {
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
@@ -217,55 +216,54 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
color: '#334155',
|
||||
color: colors.text,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
|
||||
// Product Identity
|
||||
identityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
|
||||
productNameText: { fontSize: 20, fontWeight: '900', color: '#1e293b', flex: 1 },
|
||||
categoryBadge: { backgroundColor: '#f1f5f9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
|
||||
categoryText: { fontSize: 11, fontWeight: '800', color: '#64748b', textTransform: 'uppercase' },
|
||||
productNameText: { fontSize: 20, color: colors.text, flex: 1 },
|
||||
categoryBadge: { backgroundColor: colors.border, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
|
||||
categoryText: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase' },
|
||||
specGrid: { flexDirection: 'row', gap: 24, marginTop: 5 },
|
||||
specItem: { flex: 1 },
|
||||
specValue: { fontSize: 15, fontWeight: '700', color: '#334155', marginTop: 2 },
|
||||
specValue: { fontSize: 15, color: colors.text, marginTop: 2 },
|
||||
|
||||
// Pricing
|
||||
priceTile: { marginBottom: 16 },
|
||||
label: { fontSize: 11, color: '#94a3b8', fontWeight: '700', textTransform: 'uppercase', marginBottom: 4 },
|
||||
priceFig: { fontSize: 22, fontWeight: '900', color: colors.black },
|
||||
priceWords: { fontSize: 13, color: '#64748b', fontStyle: 'italic', marginTop: 2 },
|
||||
hDivider: { height: 1, backgroundColor: '#f1f5f9', marginBottom: 16 },
|
||||
label: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase', marginBottom: 4 },
|
||||
priceFig: { fontSize: 22, color: colors.text },
|
||||
priceWords: { fontSize: 13, color: colors.textSecondary, fontStyle: 'italic', marginTop: 2 },
|
||||
hDivider: { height: 1, backgroundColor: colors.border, marginBottom: 16 },
|
||||
row: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
|
||||
// Description
|
||||
descContent: { fontSize: 15, color: '#475569', lineHeight: 22, fontWeight: '500' },
|
||||
descContent: { fontSize: 15, color: colors.textSecondary, lineHeight: 22, fontFamily: typography.fonts.medium },
|
||||
|
||||
// Table
|
||||
docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', marginBottom: 10 },
|
||||
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 },
|
||||
docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 },
|
||||
docIndex: { flex: 1, fontSize: 14, fontWeight: '700', color: '#64748b' },
|
||||
docTitle: { fontSize: 14, fontWeight: '700', color: '#1e293b' },
|
||||
downloadIconBox: { backgroundColor: '#fff', padding: 8, borderRadius: 10, borderWidth: 1, borderColor: '#e2e8f0' },
|
||||
docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: colors.border, marginBottom: 10 },
|
||||
tableLabel: { fontSize: 11, color: colors.textSecondary, flex: 1 },
|
||||
docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background, padding: 12, borderRadius: 12 },
|
||||
docIndex: { flex: 1, fontSize: 14, color: colors.textSecondary },
|
||||
docTitle: { fontSize: 14, color: colors.text },
|
||||
downloadIconBox: { backgroundColor: colors.surface, padding: 8, borderRadius: 10, borderWidth: 1, borderColor: colors.border },
|
||||
|
||||
// Footer
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 0, width: '100%',
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: colors.surface,
|
||||
padding: 20,
|
||||
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
|
||||
borderTopWidth: 1, borderTopColor: '#f1f5f9'
|
||||
borderTopWidth: 1, borderTopColor: colors.border
|
||||
},
|
||||
primaryBtn: {
|
||||
backgroundColor: colors.primary,
|
||||
height: 56, borderRadius: 18,
|
||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10
|
||||
},
|
||||
primaryBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' }
|
||||
primaryBtnText: { color: colors.buttonText, fontSize: 16, fontFamily: typography.fonts.bold }
|
||||
});
|
||||
|
||||
export default memo(AuctionItemDetail);
|
||||
@@ -64,7 +64,7 @@ const PurchaseAuction = () => {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} />
|
||||
<SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} filter={false} />
|
||||
{isPending ? <Skelton />
|
||||
:
|
||||
<FlatList
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import React, { use, useEffect, useState } from 'react';
|
||||
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||
import { InsideHeader, Loader } from '../../Component/Index';
|
||||
import colors from '../../Constant/Color';
|
||||
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import Animated, { FadeInDown, FadeIn } from 'react-native-reanimated';
|
||||
import {
|
||||
Calendar,
|
||||
Hash,
|
||||
Wifi,
|
||||
WifiOff,
|
||||
Receipt,
|
||||
CheckCircle2,
|
||||
Gavel,
|
||||
Building2,
|
||||
BadgeIndianRupee,
|
||||
} from 'lucide-react-native';
|
||||
import { InsideHeader } from '../../Component/Index';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/ApiCall/Hooks';
|
||||
import { showSuccessAlert } from '../../Constant/ShowDailog';
|
||||
@@ -10,42 +26,33 @@ import { clearPurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
|
||||
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import Skeleton from '../../Component/Skelton/Skelton';
|
||||
|
||||
const dummyAuctionList = [
|
||||
{
|
||||
auctionId: 1,
|
||||
auctionNumber: 'AUC-2025-001',
|
||||
auctionPattern: 'Single Stage',
|
||||
auctionDispatchDate: '25 Feb 2025',
|
||||
auctionDispatcherNumber: 'DISP-001',
|
||||
auctionFees: 2500,
|
||||
gst: 18,
|
||||
gstAmount: 450,
|
||||
portalCharge: 100,
|
||||
payableAmount: 3050,
|
||||
},
|
||||
];
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
|
||||
const PurchaseAuctionList = () => {
|
||||
const Navigation = useNavigation()
|
||||
const Dispatch = useDispatch()
|
||||
const Navigation: any = useNavigation();
|
||||
const Dispatch = useDispatch();
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [list, setList] = useState([]);
|
||||
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
|
||||
const [list, setList] = useState<any[]>([]);
|
||||
|
||||
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
|
||||
const LoginUser = useSelector((state: any) => state.token.token);
|
||||
const { mutate, isPending } = useOfflineAuctionPurchase()
|
||||
|
||||
const { mutate, isPending } = useOfflineAuctionPurchase();
|
||||
const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList();
|
||||
|
||||
const calculateTotalAmount = (item: any) => {
|
||||
const gstAmount =
|
||||
(Number(item.auctionFees) + Number(item.portalCharge)) *
|
||||
(Number(item.gst) / 100);
|
||||
return Number(item.auctionFees) + Number(item.portalCharge) + gstAmount;
|
||||
};
|
||||
|
||||
const payableAmount =
|
||||
Number(item.auctionFees) + Number(item.portalCharge) + gstAmount;
|
||||
return payableAmount
|
||||
}
|
||||
const grandTotal = list.reduce(
|
||||
(sum: number, item: any) => sum + calculateTotalAmount(item),
|
||||
0,
|
||||
);
|
||||
|
||||
const handleOfflinePurchase = async () => {
|
||||
const body = {
|
||||
@@ -59,235 +66,426 @@ const PurchaseAuctionList = () => {
|
||||
gst: b?.gst,
|
||||
gstAmount: b?.gstAmount,
|
||||
payableAmount: calculateTotalAmount(b),
|
||||
portalCharge: b?.portalCharge
|
||||
portalCharge: b?.portalCharge,
|
||||
})),
|
||||
orderNo: ''
|
||||
orderNo: '',
|
||||
};
|
||||
|
||||
try {
|
||||
mutate(body, {
|
||||
onSuccess: (res) => {
|
||||
if (res.statusCode == 200) {
|
||||
showSuccessAlert(res.message)
|
||||
Dispatch(clearPurchaseAuction())
|
||||
onSuccess: res => {
|
||||
if (res.statusCode === 200) {
|
||||
showSuccessAlert(res.message);
|
||||
Dispatch(clearPurchaseAuction());
|
||||
Navigation.dispatch(
|
||||
CommonActions.reset({
|
||||
index: 0,
|
||||
routes: [{ name: NavigationString.Home }],
|
||||
})
|
||||
CommonActions.reset({ index: 0, routes: [{ name: NavigationString.Home }] }),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error, "purchse Error");
|
||||
}
|
||||
})
|
||||
|
||||
onError: error => console.log(error, 'purchase Error'),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("purchase offline error", error);
|
||||
}
|
||||
console.log('purchase offline error', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurchase = (type: 'online' | 'offline') => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
type === 'online' ? Alert.alert('Online Purchase Successful!') : handleOfflinePurchase();
|
||||
|
||||
type === 'online' ? undefined : handleOfflinePurchase();
|
||||
}, 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 auctionNumbers: string[] = SelectedPurchaseList.map(
|
||||
(item: { auctionNumber: string }) => item.auctionNumber
|
||||
(item: { auctionNumber: string }) => item.auctionNumber,
|
||||
);
|
||||
purchaseListMutation({ auctionNumbers, contractorId: LoginUser.contractorId }, {
|
||||
onSuccess: (res) => {
|
||||
console.log("purchaseListMutation",res)
|
||||
if (res.statusCode == 200) {
|
||||
|
||||
setList(res.data)
|
||||
}
|
||||
purchaseListMutation(
|
||||
{ auctionNumbers, contractorId: LoginUser.contractorId },
|
||||
{
|
||||
onSuccess: res => {
|
||||
if (res.statusCode === 200) setList(res.data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error, "purchase list error");
|
||||
}
|
||||
})
|
||||
}
|
||||
onError: error => console.log(error, 'purchase list error'),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
getPurchaseList()
|
||||
}, [SelectedPurchaseList])
|
||||
getPurchaseList();
|
||||
}, [SelectedPurchaseList]),
|
||||
);
|
||||
|
||||
const styles = makeStyles(colors, spacing, shadows);
|
||||
|
||||
/* ── Render Item ── */
|
||||
const renderItem = ({ item, index }: { item: any; index: number }) => {
|
||||
const total = calculateTotalAmount(item);
|
||||
const gstAmount =
|
||||
(Number(item.auctionFees) + Number(item.portalCharge)) *
|
||||
(Number(item.gst) / 100);
|
||||
|
||||
return (
|
||||
<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 (
|
||||
<View style={{ flex: 1, backgroundColor: colors.white }}>
|
||||
<InsideHeader title="Purchase List" showArrow />
|
||||
<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}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.auctionId.toString()}
|
||||
keyExtractor={item => item.auctionId?.toString()}
|
||||
contentContainerStyle={styles.container}
|
||||
/>}
|
||||
</View>
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={styles.bottomButtonContainer}>
|
||||
{/* Bottom Buttons */}
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { marginRight: 8 }]}
|
||||
style={[styles.button, styles.onlineButton, { borderColor: colors.primary }]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => handlePurchase('online')}
|
||||
>
|
||||
<Text style={styles.buttonText}>Purchase Online</Text>
|
||||
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
|
||||
<Wifi size={18} color={colors.primary} />
|
||||
<AppText variant="button" color={colors.primary} style={styles.btnText as any}>
|
||||
Online
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { marginLeft: 8 }]}
|
||||
style={[styles.button, styles.offlineButton, { backgroundColor: colors.buttonPrimary }]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => handlePurchase('offline')}
|
||||
>
|
||||
{
|
||||
isPending ? <Loader size='small' color={colors.white} /> : <>
|
||||
<Text style={styles.buttonText}>Purchase Offline</Text>
|
||||
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
|
||||
{isPending ? (
|
||||
<ActivityIndicator size="small" color="#fff" />
|
||||
) : (
|
||||
<>
|
||||
<WifiOff size={18} color={colors.buttonText} />
|
||||
<AppText variant="button" color={colors.buttonText} style={styles.btnText as any}>
|
||||
Offline
|
||||
</AppText>
|
||||
</>
|
||||
}
|
||||
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{(loading || isPending) && (
|
||||
<Animated.View entering={FadeIn.duration(200)} style={styles.loadingOverlay}>
|
||||
<View style={[styles.loadingCard, { backgroundColor: colors.surface }]}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: spacing.s } as any}>
|
||||
Processing purchase…
|
||||
</AppText>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseAuctionList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
/* ─── Sub-components ─── */
|
||||
|
||||
const InfoRow = ({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
colors,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
colors: any;
|
||||
}) => (
|
||||
<View style={infoRowStyles.row}>
|
||||
<View style={infoRowStyles.iconWrap}>{icon}</View>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={infoRowStyles.label as any}>
|
||||
{label}
|
||||
</AppText>
|
||||
<AppText variant="bodySmall" color={colors.text} style={infoRowStyles.value as any}>
|
||||
{value}
|
||||
</AppText>
|
||||
</View>
|
||||
);
|
||||
|
||||
const infoRowStyles = StyleSheet.create({
|
||||
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 5, gap: 6 },
|
||||
iconWrap: { width: 22, alignItems: 'center' },
|
||||
label: { flex: 1, fontWeight: '500' } as any,
|
||||
value: { fontWeight: '600' } as any,
|
||||
});
|
||||
|
||||
const FeeRow = ({
|
||||
label,
|
||||
value,
|
||||
colors,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
colors: any;
|
||||
}) => (
|
||||
<View style={feeRowStyles.row}>
|
||||
<AppText variant="caption" color={colors.textSecondary}>{label}</AppText>
|
||||
<AppText variant="caption" color={colors.text} style={{ fontWeight: '600' } as any}>{value}</AppText>
|
||||
</View>
|
||||
);
|
||||
|
||||
const feeRowStyles = StyleSheet.create({
|
||||
row: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 3 },
|
||||
});
|
||||
|
||||
/* ─── Styles factory ─── */
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any) =>
|
||||
StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
|
||||
/* Grand total strip */
|
||||
grandTotalStrip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: colors.surface,
|
||||
marginHorizontal: spacing.m,
|
||||
marginTop: spacing.m,
|
||||
marginBottom: spacing.s,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: spacing.m,
|
||||
borderRadius: 18,
|
||||
...shadows.medium,
|
||||
},
|
||||
grandTotalRight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
paddingHorizontal: spacing.s + 2,
|
||||
paddingVertical: spacing.xs + 2,
|
||||
borderRadius: 20,
|
||||
},
|
||||
|
||||
container: {
|
||||
padding: 16,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingTop: spacing.s,
|
||||
paddingBottom: 120,
|
||||
},
|
||||
|
||||
/* Card */
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
elevation: 5,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.12,
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
shadowRadius: 8,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 20,
|
||||
marginBottom: spacing.m,
|
||||
overflow: 'hidden',
|
||||
...shadows.medium,
|
||||
},
|
||||
cardAccent: {
|
||||
height: 4,
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
cardBody: {
|
||||
padding: spacing.m,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#0F172A',
|
||||
},
|
||||
pattern: {
|
||||
fontSize: 14,
|
||||
color: colors.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
cardRow: {
|
||||
flexDirection: 'row',
|
||||
indexBadge: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 10,
|
||||
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',
|
||||
marginTop: 2,
|
||||
},
|
||||
patternBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
gap: 4,
|
||||
alignSelf: 'flex-start',
|
||||
paddingHorizontal: spacing.s,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
},
|
||||
total: {
|
||||
fontSize: 15,
|
||||
divider: {
|
||||
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',
|
||||
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',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.white,
|
||||
width: '100%',
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 10,
|
||||
elevation: 5
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingTop: spacing.s + 4,
|
||||
paddingBottom: spacing.m + 4,
|
||||
backgroundColor: colors.surface,
|
||||
gap: spacing.s,
|
||||
...shadows.strong,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 14,
|
||||
borderRadius: 16,
|
||||
gap: spacing.s,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontWeight: '700',
|
||||
fontSize: 14,
|
||||
onlineButton: {
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
offlineButton: {
|
||||
...shadows.medium,
|
||||
},
|
||||
btnText: {
|
||||
letterSpacing: 1.2,
|
||||
} as any,
|
||||
|
||||
/* Loading overlay */
|
||||
loadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
loadingCard: {
|
||||
borderRadius: 20,
|
||||
padding: spacing.l,
|
||||
alignItems: 'center',
|
||||
...shadows.strong,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,262 +1,298 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native';
|
||||
import React, { useMemo } from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||
import moment from 'moment';
|
||||
import CountdownTimer from '../../Component/Counter/CountdownTimer';
|
||||
import colors from '../../Constant/Color';
|
||||
import AuctionDetail from './Modal/AuctionDetail';
|
||||
import AppText from '../../components/AppText';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import {
|
||||
Tag,
|
||||
Calendar,
|
||||
IndianRupee,
|
||||
ArrowRight,
|
||||
CircleDot,
|
||||
Clock,
|
||||
TimerOff,
|
||||
} from 'lucide-react-native';
|
||||
|
||||
const PurchaseAuctionTile = ({ item, onPressDetails, onStartBidding, buttonEnable, isAuctionEnd }: any) => {
|
||||
const PurchaseAuctionTile = ({
|
||||
item,
|
||||
onPressDetails,
|
||||
onStartBidding,
|
||||
buttonEnable,
|
||||
isAuctionEnd,
|
||||
}: any) => {
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const [showDetail, setShowDetail] = React.useState(false);
|
||||
const scale = useSharedValue(1);
|
||||
const elevation = useSharedValue(8);
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
elevation: elevation.value, // Android-safe
|
||||
};
|
||||
}, []);
|
||||
}));
|
||||
|
||||
const handlePressIn = () => {
|
||||
scale.value = withTiming(0.97, { duration: 120 });
|
||||
elevation.value = withTiming(3, { duration: 120 });
|
||||
};
|
||||
const handlePressIn = () => scale.value = withTiming(0.98, { duration: 140 });
|
||||
const handlePressOut = () => scale.value = withTiming(1, { duration: 160 });
|
||||
|
||||
const handlePressOut = () => {
|
||||
scale.value = withTiming(1, { duration: 160 });
|
||||
elevation.value = withTiming(8, { duration: 160 });
|
||||
};
|
||||
const isAmended = item?.auctionStatus === 'Amended';
|
||||
const biddingEnd = isAmended
|
||||
? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '')
|
||||
: item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', '');
|
||||
const biddingStart = isAmended
|
||||
? item?.amendedkeyDates?.auctionBidding?.startDateTime?.replace(' kk', '')
|
||||
: item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', '');
|
||||
|
||||
const statusColor = isAuctionEnd ? '#EF4444' : buttonEnable ? colors.primary : '#94A3B8';
|
||||
const statusConfig = useMemo(() => {
|
||||
if (isAuctionEnd) return { label: 'ENDED', color: colors.error, bg: `${colors.error}15`, Icon: TimerOff };
|
||||
if (buttonEnable) return { label: 'LIVE', color: colors.success, bg: `${colors.success}15`, Icon: CircleDot };
|
||||
return { label: 'PENDING', color: colors.textSecondary, bg: `${colors.textSecondary}12`, Icon: Clock };
|
||||
}, [isAuctionEnd, buttonEnable, colors]);
|
||||
|
||||
const lightStatusBg = isAuctionEnd ? '#FEF2F2' : buttonEnable ? '#EFF6FF' : '#F8FAFC';
|
||||
const styles = makeStyles(colors, spacing);
|
||||
|
||||
if (showDetail) {
|
||||
return <AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
{!showDetail ? <Animated.View
|
||||
entering={FadeInUp.duration(400)}
|
||||
style={[styles.shadowContainer, animatedStyle]}
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(450).springify()}
|
||||
style={[styles.container, shadows.medium, animatedStyle]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowDetail(true)}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
style={styles.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}>
|
||||
{/* Header */}
|
||||
<View style={styles.headerRow}>
|
||||
{/* Work title */}
|
||||
<AppText variant="bodySmall" color={colors.text} numberOfLines={2} style={styles.workTitle as any}>
|
||||
{item?.auctionOfWork ?? item?.auctionNumber ?? 'Auction Details'}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* ── Info Grid ── */}
|
||||
<View style={styles.infoGrid}>
|
||||
{/* Fee */}
|
||||
<View style={styles.infoRow}>
|
||||
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
|
||||
<IndianRupee size={17} color={colors.primary} strokeWidth={2} />
|
||||
</View>
|
||||
<View>
|
||||
<View style={[styles.statusBadge, { backgroundColor: lightStatusBg }]}>
|
||||
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
||||
<Text style={[styles.statusText, { color: statusColor }]}>
|
||||
{isAuctionEnd ? 'ENDED' : buttonEnable ? 'LIVE' : 'PENDING'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.title}>{item.auctionNumber}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.amountContainer}>
|
||||
<Text style={styles.amount}>
|
||||
₹{item.auctionFees?.toLocaleString()}
|
||||
</Text>
|
||||
<Text style={styles.feeLabel}>Auction Fee</Text>
|
||||
<AppText variant="caption" color={colors.textSecondary}>Auction Fee</AppText>
|
||||
<AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
|
||||
₹{Number(item?.auctionFees ?? 0).toLocaleString('en-IN')}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Date */}
|
||||
<View style={styles.dateRow}>
|
||||
<Text style={styles.dateText}>
|
||||
<Text style={styles.dateLabel}>Bidding Start: </Text>
|
||||
{moment(
|
||||
item?.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.startDateTime?.replace(' kk', '') : item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', ''),
|
||||
'DD-MM-YYYY HH:mm:ss'
|
||||
).format('DD MMM YYYY, hh:mm A')}
|
||||
</Text>
|
||||
<Text style={styles.dateText}>
|
||||
<Text style={styles.dateLabel}>Bidding Ends: </Text>
|
||||
{moment(
|
||||
item?.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '') : item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', ''),
|
||||
'DD-MM-YYYY HH:mm:ss'
|
||||
).format('DD MMM YYYY, hh:mm A')}
|
||||
</Text>
|
||||
{/* Bidding End */}
|
||||
<View style={styles.infoRow}>
|
||||
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
|
||||
<Calendar size={17} color={colors.primary} strokeWidth={2} />
|
||||
</View>
|
||||
<View>
|
||||
<AppText variant="caption" color={colors.textSecondary}>Bidding Ends</AppText>
|
||||
<AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
|
||||
{biddingEnd
|
||||
? moment(biddingEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')
|
||||
: '—'}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
{/* ── Footer ── */}
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.footerTop}>
|
||||
<Text style={styles.footerLabel}>Remaining Time</Text>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' } as any}>
|
||||
Time Remaining
|
||||
</AppText>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
||||
{/* Timer */}
|
||||
<View style={styles.timerSection}>
|
||||
|
||||
{buttonEnable && !isAuctionEnd ? (
|
||||
<CountdownTimer
|
||||
endDate={item?.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.endDateTime : item?.keyDates?.auctionBidding?.endDateTime}
|
||||
/>
|
||||
<CountdownTimer endDate={biddingEnd} />
|
||||
) : (
|
||||
<Text style={styles.endedText}>--:--:--</Text>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={{ fontWeight: '700' } as any}>
|
||||
-- : -- : --
|
||||
</AppText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* CTA Button */}
|
||||
<TouchableOpacity
|
||||
disabled={!buttonEnable}
|
||||
disabled={!buttonEnable || isAuctionEnd}
|
||||
onPress={onStartBidding}
|
||||
activeOpacity={0.85}
|
||||
style={[
|
||||
styles.actionButton,
|
||||
{ backgroundColor: statusColor },
|
||||
!buttonEnable && styles.disabledButton,
|
||||
styles.ctaBtn,
|
||||
{
|
||||
backgroundColor:
|
||||
isAuctionEnd
|
||||
? colors.error
|
||||
: buttonEnable
|
||||
? colors.primary
|
||||
: colors.border,
|
||||
opacity: (!buttonEnable || isAuctionEnd) ? 0.8 : 1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={styles.addText}>
|
||||
{isAuctionEnd
|
||||
? 'Bidding Closed'
|
||||
: !buttonEnable
|
||||
? 'Pending Bid'
|
||||
: 'Proceed to Start Bidding'}
|
||||
</Text>
|
||||
<AppText
|
||||
variant="button"
|
||||
color={colors.buttonText}
|
||||
style={styles.ctaText as any}
|
||||
>
|
||||
{isAuctionEnd ? 'Ended' : !buttonEnable ? 'Pending' : 'Bid Now'}
|
||||
</AppText>
|
||||
{buttonEnable && !isAuctionEnd && (
|
||||
<ArrowRight size={15} color={colors.buttonText} strokeWidth={2.5} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View> :
|
||||
<AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
|
||||
}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseAuctionTile;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
shadowContainer: {
|
||||
marginHorizontal: 5,
|
||||
marginTop: 14,
|
||||
marginBottom: 6,
|
||||
const makeStyles = (colors: any, spacing: any) =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
marginHorizontal: spacing.s,
|
||||
marginBottom: spacing.m,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#fff',
|
||||
elevation: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
cardInternal: {
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: colors.surface,
|
||||
padding: spacing.m,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0,0,0,0.03)',
|
||||
},
|
||||
statusAccent: {
|
||||
width: 5,
|
||||
height: '100%',
|
||||
|
||||
/* Header */
|
||||
header: {
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
mainInfo: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
headerRow: {
|
||||
headerTop: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
statusBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 6,
|
||||
marginBottom: 6,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
statusDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
marginRight: 5,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 20,
|
||||
gap: 5,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
fontSize: 10,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
} as any,
|
||||
numberRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
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',
|
||||
numberText: {
|
||||
letterSpacing: 0.4,
|
||||
fontWeight: '600',
|
||||
} as any,
|
||||
workTitle: {
|
||||
fontWeight: '600',
|
||||
lineHeight: 20,
|
||||
} as any,
|
||||
|
||||
divider: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
backgroundColor: colors.border,
|
||||
marginBottom: spacing.m,
|
||||
opacity: 0.6,
|
||||
},
|
||||
dateLabel: {
|
||||
fontWeight: '400',
|
||||
color: '#64748B',
|
||||
},
|
||||
footer: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#F1F5F9',
|
||||
paddingTop: 12,
|
||||
},
|
||||
footerTop: {
|
||||
|
||||
/* Info grid */
|
||||
infoGrid: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.l,
|
||||
gap: spacing.s,
|
||||
},
|
||||
infoRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
gap: 10,
|
||||
flex: 1,
|
||||
},
|
||||
footerLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
color: '#94A3B8',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
endedText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
color: '#CBD5E1',
|
||||
},
|
||||
actionButton: {
|
||||
width: '100%',
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
iconBox: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
infoValue: {
|
||||
fontWeight: '700',
|
||||
} as any,
|
||||
|
||||
/* Footer */
|
||||
footer: {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
backgroundColor: colors.background,
|
||||
marginHorizontal: -spacing.m,
|
||||
marginBottom: -spacing.m,
|
||||
padding: spacing.m,
|
||||
borderBottomLeftRadius: 16,
|
||||
borderBottomRightRadius: 16,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
}),
|
||||
timerSection: {
|
||||
flex: 1,
|
||||
},
|
||||
disabledButton: {
|
||||
backgroundColor: '#E2E8F0',
|
||||
},
|
||||
addText: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0.2,
|
||||
ctaBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 18,
|
||||
borderRadius: 12,
|
||||
gap: 5,
|
||||
},
|
||||
ctaText: {
|
||||
letterSpacing: 0.8,
|
||||
} as any,
|
||||
});
|
||||
|
||||
@@ -1,227 +1,387 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import Animated, { FadeIn, FadeOut, Layout, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||
import { ChevronDown, Trash2, Info } from 'lucide-react-native';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
FadeInDown,
|
||||
Layout,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, Gavel, Tag } from 'lucide-react-native';
|
||||
import { InsideHeader } from '../../Component/Index';
|
||||
import colors from '../../Constant/Color';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
|
||||
const PurchaseList = () => {
|
||||
const Navigation: any = useNavigation()
|
||||
const Dispatch = useDispatch()
|
||||
const Navigation: any = useNavigation();
|
||||
const Dispatch = useDispatch();
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
||||
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
|
||||
|
||||
const toggleExpand = (index: number) => {
|
||||
setExpandedIndex(prev => (prev === index ? null : index));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) {
|
||||
Navigation.goBack();
|
||||
}
|
||||
}, [SelectedPurchaseList]);
|
||||
|
||||
|
||||
const styles = makeStyles(colors, spacing, shadows);
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
<InsideHeader title="Purchase" showArrow />
|
||||
<InsideHeader title="Purchase Auctions" showArrow />
|
||||
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.heading}>Purchase List</Text>
|
||||
{/* <Info size={20} color="#0F172A" /> */}
|
||||
{/* Summary Strip */}
|
||||
<View style={styles.summaryStrip}>
|
||||
<View style={styles.summaryIcon}>
|
||||
<ShoppingBag size={16} color={colors.primary} />
|
||||
</View>
|
||||
<AppText variant="caption" color={colors.textSecondary}>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' } as any}>
|
||||
{SelectedPurchaseList.length}
|
||||
</AppText>
|
||||
{' '}auction{SelectedPurchaseList.length !== 1 ? 's' : ''} selected for purchase
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
{/* Cards */}
|
||||
{SelectedPurchaseList.map((item: any, index: any) => {
|
||||
const expanded = expandedIndex === index;
|
||||
return (
|
||||
<Animated.View
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.container}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{SelectedPurchaseList.map((item: any, index: number) => (
|
||||
<PurchaseListItem
|
||||
key={item.auctionId}
|
||||
layout={Layout.springify()}
|
||||
style={styles.card}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => toggleExpand(index)}
|
||||
>
|
||||
<CardHeader
|
||||
title={item.auctionOfWork}
|
||||
expanded={expanded}
|
||||
item={item}
|
||||
index={index}
|
||||
expandedIndex={expandedIndex}
|
||||
toggleExpand={toggleExpand}
|
||||
colors={colors}
|
||||
styles={styles}
|
||||
Dispatch={Dispatch}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Text style={styles.subTitle}>
|
||||
Auction No: {item.auctionNumber}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.subTitle}>
|
||||
Status:{' '}
|
||||
<Text
|
||||
style={{
|
||||
color: item.status ? '#16A34A' : '#DC2626',
|
||||
fontWeight: '700',
|
||||
}}
|
||||
>
|
||||
{item.status ? 'Open' : 'Closed'}
|
||||
</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{expanded && (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(220)}
|
||||
exiting={FadeOut.duration(180)}
|
||||
layout={Layout.springify()}
|
||||
style={styles.details}
|
||||
>
|
||||
{renderRow('Auction Mode', item.auctionMode)}
|
||||
{renderRow('Pattern', item.auctionPattern)}
|
||||
{renderRow('Fees', `₹ ${item.auctionFees}`)}
|
||||
{renderRow('Department', item.deptName)}
|
||||
{renderRow('Purchase End', item.purchaseEnd)}
|
||||
|
||||
<TouchableOpacity style={styles.removeButton} onPress={() => { Dispatch(togglePurchaseAuction(item)) }}>
|
||||
<Trash2 size={18} color="#fff" />
|
||||
<Text style={styles.removeText}>Remove</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Animated.View>
|
||||
);
|
||||
})}
|
||||
<View style={{ height: 90 }} />
|
||||
</ScrollView>
|
||||
|
||||
{/* Footer */}
|
||||
<TouchableOpacity disabled={Boolean(!SelectedPurchaseList.length)} style={[styles.proceedButton, { backgroundColor: Boolean(!SelectedPurchaseList.length) ? colors.lightText : colors.primary }]} onPress={() => { Navigation.navigate(NavigationString.PurchaseAuctionList) }}>
|
||||
<Text style={styles.proceedText}>Proceed to Purchase</Text>
|
||||
{/* Floating Proceed Button */}
|
||||
<View style={styles.footerContainer}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
disabled={!SelectedPurchaseList.length}
|
||||
style={[
|
||||
styles.proceedButton,
|
||||
{
|
||||
backgroundColor: SelectedPurchaseList.length
|
||||
? colors.buttonPrimary
|
||||
: colors.secondary,
|
||||
},
|
||||
]}
|
||||
onPress={() => Navigation.navigate(NavigationString.PurchaseAuctionList)}
|
||||
>
|
||||
<AppText variant="button" color={colors.buttonText} style={styles.proceedText as any}>
|
||||
Proceed to Purchase
|
||||
</AppText>
|
||||
<View style={styles.countBadge}>
|
||||
<AppText variant="caption" color={colors.buttonPrimary} style={{ fontWeight: '800' } as any}>
|
||||
{SelectedPurchaseList.length}
|
||||
</AppText>
|
||||
</View>
|
||||
<ChevronRight size={20} color={colors.buttonText} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseList;
|
||||
|
||||
const CardHeader = ({ title, expanded }: {
|
||||
title: string;
|
||||
expanded: boolean;
|
||||
}) => {
|
||||
const rotation = useSharedValue(expanded ? 1 : 0);
|
||||
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
|
||||
const arrowStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${rotation.value * 180}deg` }],
|
||||
}));
|
||||
/* ─── Sub-components ─── */
|
||||
|
||||
const PurchaseListItem = ({ item, index, expandedIndex, toggleExpand, colors, styles, Dispatch }: any) => {
|
||||
const expanded = expandedIndex === index;
|
||||
const isActive = item.status;
|
||||
const chevronStyle = useChevronStyle(expanded);
|
||||
|
||||
return (
|
||||
<View style={styles.cardHeader}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Animated.View style={arrowStyle}>
|
||||
<ChevronDown size={22} color={colors.black} />
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(index * 80).springify()}
|
||||
layout={Layout.springify()}
|
||||
style={[styles.card, expanded && styles.cardExpanded]}
|
||||
>
|
||||
{/* Left Accent Bar */}
|
||||
<View style={[styles.accentBar, { backgroundColor: isActive ? colors.success : colors.primary }]} />
|
||||
|
||||
<View style={styles.cardInner}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.85}
|
||||
onPress={() => toggleExpand(index)}
|
||||
>
|
||||
{/* Badge Row */}
|
||||
<View style={styles.badgeRow}>
|
||||
<View style={styles.auctionBadge}>
|
||||
<Gavel size={11} color={colors.primary} strokeWidth={2} />
|
||||
<AppText variant="caption" color={colors.primary} style={styles.badgeText as any}>
|
||||
{item.auctionNumber}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Title + Chevron */}
|
||||
<View style={styles.titleRow}>
|
||||
<AppText variant="h4" style={styles.title as any} numberOfLines={2}>
|
||||
{item.auctionOfWork ?? item.auctionNumber}
|
||||
</AppText>
|
||||
<Animated.View style={chevronStyle}>
|
||||
<ChevronDown size={20} color={colors.textSecondary} />
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* Dept Tag */}
|
||||
<View style={styles.deptRow}>
|
||||
<Tag size={12} color={colors.textSecondary} />
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.deptText as any}>
|
||||
{item.deptName || 'Department'}
|
||||
</AppText>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Expanded Section */}
|
||||
{expanded && (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(220)}
|
||||
exiting={FadeOut.duration(160)}
|
||||
layout={Layout.springify()}
|
||||
style={styles.detailsContainer}
|
||||
>
|
||||
<View style={styles.divider} />
|
||||
|
||||
<DetailRow label="Auction Mode" value={item.auctionMode} colors={colors} />
|
||||
<DetailRow label="Pattern" value={item.auctionPattern} colors={colors} />
|
||||
<DetailRow label="Auction Fees" value={`₹ ${item.auctionFees}`} colors={colors} valueColor={colors.primary} />
|
||||
<DetailRow label="Department" value={item.deptName} colors={colors} />
|
||||
<DetailRow label="Purchase End" value={item.purchaseEnd} colors={colors} />
|
||||
|
||||
{/* Remove Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.removeButton}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => Dispatch(togglePurchaseAuction(item))}
|
||||
>
|
||||
<Trash2 size={16} color="#fff" />
|
||||
<AppText variant="button" color="#fff" style={styles.removeText as any}>
|
||||
Remove
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRow = (label: string, value: string) => (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<Text style={styles.value}>{value}</Text>
|
||||
const DetailRow = ({
|
||||
label,
|
||||
value,
|
||||
colors,
|
||||
valueColor,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
colors: any;
|
||||
valueColor?: string;
|
||||
}) => (
|
||||
<View style={detailRowStyles.row}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={detailRowStyles.label as any}>
|
||||
{label}
|
||||
</AppText>
|
||||
<AppText variant="caption" color={valueColor || colors.text} style={detailRowStyles.value as any}>
|
||||
{value ?? '—'}
|
||||
</AppText>
|
||||
</View>
|
||||
);
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F8FAFC',
|
||||
},
|
||||
container: {
|
||||
padding: 16,
|
||||
paddingBottom: 110,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 14,
|
||||
},
|
||||
heading: {
|
||||
fontSize: 22,
|
||||
fontWeight: '800',
|
||||
color: colors.black,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 18,
|
||||
padding: 16,
|
||||
marginBottom: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
cardHeader: {
|
||||
const detailRowStyles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#0F172A',
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
subTitle: {
|
||||
fontSize: 14,
|
||||
color: '#64748B',
|
||||
marginTop: 6,
|
||||
},
|
||||
details: {
|
||||
marginTop: 16,
|
||||
paddingTop: 14,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E5E7EB',
|
||||
},
|
||||
row: {
|
||||
marginVertical: 6,
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: 'rgba(0,0,0,0.06)',
|
||||
},
|
||||
label: {
|
||||
fontSize: 13,
|
||||
color: '#64748B',
|
||||
},
|
||||
flex: 1,
|
||||
fontWeight: '500',
|
||||
} as any,
|
||||
value: {
|
||||
fontSize: 15,
|
||||
flex: 1,
|
||||
textAlign: 'right',
|
||||
fontWeight: '600',
|
||||
color: '#0F172A',
|
||||
} as any,
|
||||
});
|
||||
|
||||
/* ─── Chevron animated style hook ─── */
|
||||
function useChevronStyle(expanded: boolean) {
|
||||
const rotation = useSharedValue(expanded ? 1 : 0);
|
||||
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
|
||||
return useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${rotation.value * 180}deg` }],
|
||||
}));
|
||||
}
|
||||
|
||||
/* ─── Styles factory ─── */
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any) =>
|
||||
StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
summaryStrip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.surface,
|
||||
marginHorizontal: spacing.m,
|
||||
marginTop: spacing.m,
|
||||
marginBottom: spacing.s,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: spacing.s + 2,
|
||||
borderRadius: 14,
|
||||
gap: spacing.s,
|
||||
...shadows.light,
|
||||
},
|
||||
summaryIcon: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
backgroundColor: `${colors.primary}15`,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
container: {
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingTop: spacing.s,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 20,
|
||||
marginBottom: spacing.m,
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
...shadows.medium,
|
||||
},
|
||||
cardExpanded: {
|
||||
...shadows.strong,
|
||||
},
|
||||
accentBar: {
|
||||
width: 5,
|
||||
borderTopLeftRadius: 20,
|
||||
borderBottomLeftRadius: 20,
|
||||
},
|
||||
cardInner: {
|
||||
flex: 1,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: spacing.m,
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
auctionBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: `${colors.primary}15`,
|
||||
paddingHorizontal: spacing.s,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 8,
|
||||
gap: 4,
|
||||
},
|
||||
badgeText: {
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.2,
|
||||
} as any,
|
||||
titleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
gap: spacing.s,
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
fontWeight: '700',
|
||||
} as any,
|
||||
deptRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
marginTop: spacing.xs + 2,
|
||||
},
|
||||
deptText: {
|
||||
letterSpacing: 0.3,
|
||||
} as any,
|
||||
detailsContainer: {
|
||||
marginTop: spacing.s,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: colors.border,
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
removeButton: {
|
||||
marginTop: 18,
|
||||
backgroundColor: colors.primary,
|
||||
marginTop: spacing.m,
|
||||
backgroundColor: colors.error,
|
||||
borderRadius: 14,
|
||||
paddingVertical: 12,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
gap: spacing.s,
|
||||
},
|
||||
removeText: {
|
||||
color: '#fff',
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1,
|
||||
} as any,
|
||||
footerContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: spacing.m,
|
||||
right: spacing.m,
|
||||
},
|
||||
proceedButton: {
|
||||
position: 'absolute',
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 18,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 16,
|
||||
borderRadius: 20,
|
||||
gap: spacing.s,
|
||||
...shadows.strong,
|
||||
},
|
||||
proceedText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 1.5,
|
||||
} as any,
|
||||
countBadge: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
minWidth: 24,
|
||||
height: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { memo, useCallback, useRef, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native';
|
||||
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
||||
import colors from '../../../Constant/Color';
|
||||
import colors_const from '../../../Constant/Color';
|
||||
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
|
||||
import MainStyle from '../../../Styles/MainStyle';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
import { Loader } from '../../../Component/Index';
|
||||
import { useSelector } from 'react-redux';
|
||||
import axios from 'axios';
|
||||
@@ -11,6 +13,8 @@ import Variables from '../../../Constant/Variable';
|
||||
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
|
||||
|
||||
const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
|
||||
const { colors } = useTheme();
|
||||
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
||||
const Token = useSelector((state: any) => state.token.token)
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
const [value, setValue] = useState(`${item.bidAmount}`);
|
||||
@@ -100,7 +104,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
!showDetail ? <Animated.View
|
||||
layout={Layout.springify()}
|
||||
entering={FadeInUp.delay(100)}
|
||||
style={[styles.card, styles.leadingCard, { borderColor: item.me ? '#22C55E' : 'red', backgroundColor: item.me ? "#F0FDF4" : '#fdf0f0ff' }]}
|
||||
style={[styles.card, item.me ? styles.leadingCard : undefined, { borderColor: item.me ? colors.success : colors.error, backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10` }]}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<TouchableOpacity
|
||||
@@ -109,28 +113,28 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
style={styles.cardHeader}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.productName}>{item.productName}</Text>
|
||||
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
|
||||
{/* auctionStartValueFigure */}
|
||||
<Text style={styles.itemIdText}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
||||
Initial Amt : {item?.auctionStartValueFigure}
|
||||
</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
|
||||
<View style={[MainStyle.flexBetween, { gap: 10 }]}>
|
||||
<View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}>
|
||||
<Text style={[styles.badgeText, item.me ? styles.meText : styles.liveText]}>
|
||||
<AppText variant="caption" bold style={[styles.badgeText as any, item.me ? styles.meText : styles.liveText]}>
|
||||
{item.me ? 'LEADING' : 'LIVE'}
|
||||
</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<Animated.Text style={[styles.arrow, arrowStyle]}>
|
||||
<Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
|
||||
▼
|
||||
</Animated.Text>
|
||||
</View>
|
||||
<Text style={styles.itemIdText}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
||||
{SelectedAuction?.auctionNumber}
|
||||
</Text>
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
|
||||
</TouchableOpacity>
|
||||
@@ -138,15 +142,15 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
{/* PRICE INFO */}
|
||||
<View style={styles.priceRow}>
|
||||
<View>
|
||||
<Text style={styles.label}>Current Bid</Text>
|
||||
<Text style={styles.mainPrice}>₹{item?.bidAmount || item.auctionStartValueFigure}</Text>
|
||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
|
||||
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item?.bidAmount || item.auctionStartValueFigure}</AppText>
|
||||
</View>
|
||||
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<Text style={styles.label}>Increment</Text>
|
||||
<Text style={styles.incrementText}>
|
||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Increment</AppText>
|
||||
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
|
||||
+₹{item.bidVariationValue}
|
||||
</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -154,7 +158,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
<Animated.View style={[styles.collapsibleContainer, (!allBoolean.rebid && item.contractorId == undefined) ? bodyStyle : bodyStyle2]}>
|
||||
<View style={styles.divider} />
|
||||
|
||||
<Text style={styles.bidLabel}>Set Your Bid</Text>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
|
||||
|
||||
{!allBoolean.oneTimeBid && <View style={styles.stepperContainer}>
|
||||
<TouchableOpacity
|
||||
@@ -162,29 +166,29 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
disabled={isMin}
|
||||
style={[styles.stepBtn, isMin && styles.disabledBtn]}
|
||||
>
|
||||
<Text style={styles.stepText}>−</Text>
|
||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.inputBox}>
|
||||
<Text style={styles.currency}>₹</Text>
|
||||
<Text style={styles.bidValueText}>{bidValue}</Text>
|
||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
|
||||
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity onPress={onIncrease} style={styles.stepBtn}>
|
||||
<Text style={styles.stepText}>+</Text>
|
||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
|
||||
</TouchableOpacity>
|
||||
</View>}
|
||||
{
|
||||
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
|
||||
<Text style={styles.currency2}>₹</Text>
|
||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
|
||||
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={handleChange}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="0.00"
|
||||
style={styles.input}
|
||||
placeholderTextColor="#94A3B8"
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
@@ -194,7 +198,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
||||
>
|
||||
{
|
||||
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text>
|
||||
allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
|
||||
}
|
||||
</TouchableOpacity> : null}
|
||||
</Animated.View>
|
||||
@@ -209,11 +213,11 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
||||
|
||||
export default memo(BiddingCard);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const makeStyles = (colors: any) => StyleSheet.create({
|
||||
inputBox2: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F1F5F9',
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 12,
|
||||
height: 45,
|
||||
paddingHorizontal: 12,
|
||||
@@ -221,39 +225,23 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 20
|
||||
},
|
||||
currency2: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#64748B',
|
||||
marginRight: 4,
|
||||
},
|
||||
} as any,
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#0F172A',
|
||||
width: '100%'
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#FFF',
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
overflow: 'hidden',
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 10,
|
||||
},
|
||||
android: { elevation: 2 },
|
||||
}),
|
||||
},
|
||||
leadingCard: {
|
||||
borderColor: '#22C55E',
|
||||
borderWidth: 2,
|
||||
backgroundColor: '#F0FDF4',
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
@@ -261,27 +249,24 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
|
||||
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
|
||||
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
|
||||
productName: {} as any,
|
||||
itemIdText: { marginTop: 2 } as any,
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
liveBadge: { backgroundColor: '#FEE2E2' },
|
||||
meBadge: { backgroundColor: '#BBF7D0' },
|
||||
badgeText: { fontSize: 10, fontWeight: '900' },
|
||||
liveText: { color: '#EF4444' },
|
||||
meText: { color: '#15803D' },
|
||||
arrow: { fontSize: 10, color: '#94A3B8' },
|
||||
liveBadge: { backgroundColor: `${colors.error}20` },
|
||||
meBadge: { backgroundColor: `${colors.success}20` },
|
||||
badgeText: { fontSize: 10 } as any,
|
||||
liveText: { color: colors.error },
|
||||
meText: { color: colors.success },
|
||||
arrow: { fontSize: 10 },
|
||||
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
|
||||
label: {
|
||||
fontSize: 11,
|
||||
color: '#94A3B8',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: '600',
|
||||
},
|
||||
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
|
||||
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
|
||||
} as any,
|
||||
mainPrice: {} as any,
|
||||
incrementText: {} as any,
|
||||
collapsibleContainer: { overflow: 'hidden' },
|
||||
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
|
||||
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
|
||||
divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
|
||||
bidLabel: { marginBottom: 10 } as any,
|
||||
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
|
||||
stepBtn: {
|
||||
width: 45,
|
||||
@@ -291,19 +276,19 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
disabledBtn: { backgroundColor: '#CBD5E1' },
|
||||
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
|
||||
disabledBtn: { backgroundColor: colors.border },
|
||||
stepText: {} as any,
|
||||
inputBox: {
|
||||
flex: 1,
|
||||
height: 45,
|
||||
backgroundColor: '#F1F5F9',
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
|
||||
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
|
||||
currency: { marginRight: 4 } as any,
|
||||
bidValueText: {} as any,
|
||||
submitBtn: {
|
||||
height: 50,
|
||||
backgroundColor: colors.primary,
|
||||
@@ -311,5 +296,5 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
|
||||
submitBtnText: {} as any,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||
import { ShieldCheck, X } from 'lucide-react-native';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
bidAmount: number;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ConfirmBidModal = ({ visible, onClose, onConfirm, bidAmount, isLoading }: Props) => {
|
||||
const { colors, shadows } = useTheme();
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
|
||||
|
||||
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
|
||||
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
|
||||
<ShieldCheck size={32} color={colors.primary} strokeWidth={2} />
|
||||
</View>
|
||||
|
||||
<AppText variant="h3" color={colors.text} bold centered style={styles.title}>
|
||||
Confirm Your Bid
|
||||
</AppText>
|
||||
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle}>
|
||||
Are you sure you want to place this bid? This action cannot be undone.
|
||||
</AppText>
|
||||
|
||||
<View style={[styles.amountBox, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}30` }]}>
|
||||
<AppText variant="caption" color={colors.primary} bold>
|
||||
BID AMOUNT
|
||||
</AppText>
|
||||
<AppText variant="h1" color={colors.primary} bold>
|
||||
₹{bidAmount?.toLocaleString('en-IN') || 0}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<View style={styles.btnRow}>
|
||||
<TouchableOpacity style={[styles.btn, styles.cancelBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
|
||||
<AppText variant="button" color={colors.text} bold>Cancel</AppText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.btn, styles.confirmBtn, { backgroundColor: colors.primary }]} onPress={onConfirm} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<AppText variant="button" color="#fff" bold>Submit Bid</AppText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmBidModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 24,
|
||||
zIndex: 9999,
|
||||
elevation: 9999,
|
||||
},
|
||||
modalCard: {
|
||||
width: '100%',
|
||||
borderRadius: 24,
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
closeBtn: {
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconBox: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
title: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
marginBottom: 24,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
amountBox: {
|
||||
width: '100%',
|
||||
paddingVertical: 16,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 24,
|
||||
gap: 4,
|
||||
},
|
||||
btnRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
width: '100%',
|
||||
},
|
||||
btn: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
cancelBtn: {},
|
||||
confirmBtn: {},
|
||||
});
|
||||
@@ -1,105 +1,209 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import Animated, { FadeInUp } from 'react-native-reanimated';
|
||||
import colors from '../../../Constant/Color';
|
||||
import Animated, { FadeInDown, FadeInUp, ZoomIn } from 'react-native-reanimated';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
import {
|
||||
ShieldCheck,
|
||||
CheckCircle2,
|
||||
FileText,
|
||||
Gavel,
|
||||
ArrowRight,
|
||||
} from 'lucide-react-native';
|
||||
|
||||
const STEPS = [
|
||||
{ icon: FileText, label: 'Documents Submitted', done: true },
|
||||
{ icon: ShieldCheck, label: 'EMD Verified', done: true },
|
||||
{ icon: Gavel, label: 'Ready to Bid', done: true },
|
||||
];
|
||||
|
||||
const EmdComplete = ({ onNext }: { onNext?: () => void }) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LottieView
|
||||
source={require('../../../lottie/confetti.json')}
|
||||
autoPlay
|
||||
loop={true}
|
||||
style={styles.confetti}
|
||||
/>
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
|
||||
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
|
||||
source={require('../../../lottie/Success.json')}
|
||||
autoPlay
|
||||
loop={true}
|
||||
style={styles.success}
|
||||
loop={false}
|
||||
style={styles.lottie}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.Text entering={FadeInUp.delay(400).duration(700)} style={styles.title}>
|
||||
EMD Complete
|
||||
</Animated.Text>
|
||||
{/* Title */}
|
||||
<Animated.View entering={FadeInUp.delay(400).springify()} style={styles.titleBlock}>
|
||||
<AppText variant="h2" color={colors.text} bold centered>
|
||||
EMD Submitted!
|
||||
</AppText>
|
||||
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle as any}>
|
||||
Your Earnest Money Deposit has been{'\n'}successfully recorded.
|
||||
</AppText>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.Text entering={FadeInUp.delay(700).duration(700)} style={styles.subText} >
|
||||
Your submission was successful
|
||||
</Animated.Text>
|
||||
{/* ── Step Completion Chips ── */}
|
||||
<Animated.View entering={FadeInDown.delay(600).springify()} style={[styles.stepsCard,{backgroundColor: colors.surface, shadowColor: colors.shadow}]}>
|
||||
{STEPS.map((step, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<View style={styles.stepRow}>
|
||||
<View style={[styles.stepIconBox, { backgroundColor: `${colors.success}18` }]}>
|
||||
<step.icon size={16} color={colors.success} strokeWidth={2} />
|
||||
</View>
|
||||
<AppText variant="bodySmall" color={colors.text} semiBold style={styles.stepLabel as any}>
|
||||
{step.label}
|
||||
</AppText>
|
||||
<CheckCircle2 size={18} color={colors.success} strokeWidth={2.5} />
|
||||
</View>
|
||||
{index < STEPS.length - 1 && (
|
||||
<View style={[styles.stepConnector, { backgroundColor: `${colors.success}30` }]} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View entering={FadeInUp.delay(900).duration(700)} style={styles.nextWrapper} >
|
||||
<TouchableOpacity activeOpacity={0.85} style={styles.nextBtn} onPress={onNext} >
|
||||
<Text style={styles.nextText}>Next</Text>
|
||||
{/* ── Info badge ── */}
|
||||
<Animated.View
|
||||
entering={FadeInUp.delay(900).springify()}
|
||||
style={[styles.infoBadge, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}30` }]}
|
||||
>
|
||||
<AppText variant="caption" color={colors.primary} semiBold centered>
|
||||
🎉 You are now eligible to participate in the bidding process
|
||||
</AppText>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* ── Bottom CTA ── */}
|
||||
<Animated.View entering={FadeInUp.delay(1100).springify()} style={styles.bottomArea}>
|
||||
<TouchableOpacity
|
||||
style={[styles.ctaBtn, { backgroundColor: colors.primary, ...shadows.medium }]}
|
||||
onPress={onNext}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<AppText variant="button" color="#fff" bold style={styles.ctaText as any}>
|
||||
Continue to Bidding
|
||||
</AppText>
|
||||
<ArrowRight size={18} color="#fff" strokeWidth={2.5} />
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmdComplete;
|
||||
|
||||
/* ---------------- Styles ---------------- */
|
||||
/* ─── Styles ─── */
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
root: {
|
||||
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',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
confetti: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
lottie: {
|
||||
width: 130,
|
||||
height: 130,
|
||||
},
|
||||
|
||||
success: {
|
||||
width: 220,
|
||||
height: 220,
|
||||
marginTop: -100
|
||||
/* Title */
|
||||
titleBlock: {
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
|
||||
title: {
|
||||
marginTop: 16,
|
||||
fontSize: 24,
|
||||
fontWeight: '900',
|
||||
color: '#0F172A',
|
||||
letterSpacing: 0.3,
|
||||
} as any,
|
||||
subtitle: {
|
||||
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,
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { LogOut, X } from 'lucide-react-native';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const GoBackModal = ({ visible, onClose, onConfirm }: Props) => {
|
||||
const { colors, shadows } = useTheme();
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
|
||||
|
||||
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose}>
|
||||
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={[styles.iconBox, { backgroundColor: `${colors.error}15` }]}>
|
||||
<LogOut size={32} color={colors.error} strokeWidth={2} />
|
||||
</View>
|
||||
|
||||
<AppText variant="h3" color={colors.text} bold centered style={styles.title as any}>
|
||||
Leave Bidding?
|
||||
</AppText>
|
||||
<AppText variant="body" color={colors.textSecondary} centered style={styles.subtitle as any}>
|
||||
Are you sure you want to go back to the Dashboard? You can always return to this panel later.
|
||||
</AppText>
|
||||
|
||||
<View style={styles.btnRow}>
|
||||
<TouchableOpacity style={[styles.btn, styles.cancelBtn, { backgroundColor: colors.background }]} onPress={onClose}>
|
||||
<AppText variant="button" color={colors.text} bold>Stay</AppText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.btn, styles.confirmBtn, { backgroundColor: colors.error }]} onPress={onConfirm}>
|
||||
<AppText variant="button" color="#fff" bold>Leave</AppText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoBackModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 24,
|
||||
zIndex: 9999,
|
||||
elevation: 9999,
|
||||
},
|
||||
modalCard: {
|
||||
width: '100%',
|
||||
borderRadius: 24,
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
closeBtn: {
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconBox: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
title: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
marginBottom: 24,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
btnRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
width: '100%',
|
||||
},
|
||||
btn: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
cancelBtn: {},
|
||||
confirmBtn: {},
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { memo, useCallback, useRef, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput } from 'react-native';
|
||||
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
||||
import colors from '../../../Constant/Color';
|
||||
import colors_const from '../../../Constant/Color';
|
||||
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
|
||||
import MainStyle from '../../../Styles/MainStyle';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
import axios, { all } from 'axios';
|
||||
import Variables from '../../../Constant/Variable';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -11,6 +13,8 @@ import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
|
||||
import { Loader } from '../../../Component/Index';
|
||||
|
||||
const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
|
||||
const { colors } = useTheme();
|
||||
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
||||
const Token = useSelector((state: any) => state.token.token)
|
||||
const [allState, setAllState] = useState({ isLoading: false })
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
@@ -108,9 +112,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
entering={FadeInUp.delay(100)}
|
||||
style={[
|
||||
styles.card,
|
||||
item.me ? styles.leadingCard : undefined,
|
||||
{
|
||||
borderColor: item.me ? '#22C55E' : '#EF4444',
|
||||
backgroundColor: item.me ? '#F0FDF4' : '#FDF0F0',
|
||||
borderColor: item.me ? colors.success : colors.error,
|
||||
backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -121,10 +126,10 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
style={styles.cardHeader}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.productName}>{item.productName}</Text>
|
||||
<Text style={styles.itemIdText}>
|
||||
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
||||
Initial Amt : ₹{item.auctionStartValue}
|
||||
</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
|
||||
@@ -135,38 +140,40 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
item.me ? styles.meBadge : styles.liveBadge,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
<AppText
|
||||
variant="caption"
|
||||
bold
|
||||
style={[
|
||||
styles.badgeText,
|
||||
styles.badgeText as any,
|
||||
item.me ? styles.meText : styles.liveText,
|
||||
]}
|
||||
>
|
||||
{item.me ? 'LEADING' : 'LIVE'}
|
||||
</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<Animated.Text style={[styles.arrow, arrowStyle]}>
|
||||
<Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
|
||||
▼
|
||||
</Animated.Text>
|
||||
</View>
|
||||
<Text style={styles.itemIdText}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
||||
{SelectedAuction?.auctionNumber}
|
||||
</Text>
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* PRICE INFO */}
|
||||
<View style={styles.priceRow}>
|
||||
<View>
|
||||
<Text style={styles.label}>Current Bid</Text>
|
||||
<Text style={styles.mainPrice}>₹{item.bidAmount || item.auctionStartValue}</Text>
|
||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
|
||||
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item.bidAmount || item.auctionStartValue}</AppText>
|
||||
</View>
|
||||
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<Text style={styles.label}>Decrement</Text>
|
||||
<Text style={styles.incrementText}>
|
||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Decrement</AppText>
|
||||
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
|
||||
-₹{item.emdAmount}
|
||||
</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -175,7 +182,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
<Animated.View style={[styles.collapsibleContainer, bodyStyle]}>
|
||||
<View style={styles.divider} />
|
||||
|
||||
<Text style={styles.bidLabel}>Set Your Bid</Text>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
|
||||
|
||||
<View style={styles.stepperContainer}>
|
||||
{/* DECREASE */}
|
||||
@@ -187,25 +194,25 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
!canDecrease && styles.disabledBtn,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.stepText}>−</Text>
|
||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
|
||||
</TouchableOpacity>}
|
||||
|
||||
{/* VALUE */}
|
||||
{!allBoolean.oneTimeBid && <View style={styles.inputBox}>
|
||||
<Text style={styles.currency}>₹</Text>
|
||||
<Text style={styles.bidValueText}>{bidValue}</Text>
|
||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
|
||||
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
|
||||
</View>}
|
||||
{
|
||||
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
|
||||
<Text style={styles.currency2}>₹</Text>
|
||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
|
||||
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={handleChange}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="0.00"
|
||||
style={styles.input}
|
||||
placeholderTextColor="#94A3B8"
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
@@ -219,7 +226,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
!canIncrease && styles.disabledBtn,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.stepText}>+</Text>
|
||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
|
||||
</TouchableOpacity>}
|
||||
</View>
|
||||
|
||||
@@ -228,7 +235,7 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
||||
>
|
||||
{
|
||||
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text>
|
||||
allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -265,50 +272,34 @@ const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid
|
||||
|
||||
export default ReverseBiddingCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const makeStyles = (colors: any) => StyleSheet.create({
|
||||
inputBox2: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F1F5F9',
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 12,
|
||||
height: 45,
|
||||
paddingHorizontal: 12,
|
||||
width: '100%'
|
||||
},
|
||||
currency2: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#64748B',
|
||||
marginRight: 4,
|
||||
},
|
||||
} as any,
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#0F172A',
|
||||
width: '100%'
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#FFF',
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
overflow: 'hidden',
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 10,
|
||||
},
|
||||
android: { elevation: 2 },
|
||||
}),
|
||||
},
|
||||
leadingCard: {
|
||||
borderColor: '#22C55E',
|
||||
borderWidth: 2,
|
||||
backgroundColor: '#F0FDF4',
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
@@ -316,27 +307,24 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
|
||||
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
|
||||
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
|
||||
productName: {} as any,
|
||||
itemIdText: { marginTop: 2 } as any,
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
liveBadge: { backgroundColor: '#FEE2E2' },
|
||||
meBadge: { backgroundColor: '#BBF7D0' },
|
||||
badgeText: { fontSize: 10, fontWeight: '900' },
|
||||
liveText: { color: '#EF4444' },
|
||||
meText: { color: '#15803D' },
|
||||
arrow: { fontSize: 10, color: '#94A3B8' },
|
||||
liveBadge: { backgroundColor: `${colors.error}20` },
|
||||
meBadge: { backgroundColor: `${colors.success}20` },
|
||||
badgeText: { fontSize: 10 } as any,
|
||||
liveText: { color: colors.error },
|
||||
meText: { color: colors.success },
|
||||
arrow: { fontSize: 10 },
|
||||
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
|
||||
label: {
|
||||
fontSize: 11,
|
||||
color: '#94A3B8',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: '600',
|
||||
},
|
||||
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
|
||||
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
|
||||
} as any,
|
||||
mainPrice: {} as any,
|
||||
incrementText: {} as any,
|
||||
collapsibleContainer: { overflow: 'hidden' },
|
||||
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
|
||||
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
|
||||
divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
|
||||
bidLabel: { marginBottom: 10 } as any,
|
||||
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
|
||||
stepBtn: {
|
||||
width: 45,
|
||||
@@ -346,19 +334,19 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
disabledBtn: { backgroundColor: '#CBD5E1' },
|
||||
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
|
||||
disabledBtn: { backgroundColor: colors.border },
|
||||
stepText: {} as any,
|
||||
inputBox: {
|
||||
flex: 1,
|
||||
height: 45,
|
||||
backgroundColor: '#F1F5F9',
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
|
||||
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
|
||||
currency: { marginRight: 4 } as any,
|
||||
bidValueText: {} as any,
|
||||
submitBtn: {
|
||||
height: 50,
|
||||
backgroundColor: colors.primary,
|
||||
@@ -366,5 +354,5 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
|
||||
submitBtnText: {} as any,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView } from 'react-native'
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { Modal, View, StyleSheet, TextInput, TouchableOpacity, ScrollView, Platform } from 'react-native'
|
||||
import { Formik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { pick } from '@react-native-documents/picker'
|
||||
@@ -7,16 +7,20 @@ import { useSelector } from 'react-redux'
|
||||
import { apiCall } from '../../../Axios/Axios'
|
||||
import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/ApiCall/ApiUrls'
|
||||
import { Props } from '../Type/BidType'
|
||||
import colors from '../../../Constant/Color'
|
||||
import { Loader } from '../../../Component/Index'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import NavigationString from '../../../Constant/NavigationString'
|
||||
import { useTheme } from '../../../Hooks/useTheme'
|
||||
import AppText from '../../../components/AppText'
|
||||
import { Upload, FileText, X } from 'lucide-react-native'
|
||||
|
||||
const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }) => {
|
||||
if (!template) return null
|
||||
|
||||
const SelectedAuction = useSelector((state: any) => state.auction.auction)
|
||||
const Navigation: any = useNavigation()
|
||||
const { colors, spacing, typography } = useTheme()
|
||||
const styles = useMemo(() => makeStyles(colors, spacing, typography), [colors, spacing, typography])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const initialValues = template?.labels?.reduce((acc: any, label: any) => {
|
||||
acc[label.labelName] = ''
|
||||
@@ -65,10 +69,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
if (label.labelType === '3') {
|
||||
return (
|
||||
<View key={label.labelName} style={styles.field}>
|
||||
<Text style={styles.label}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.label as any}>
|
||||
{label.labelName}
|
||||
{label.mandatory && <Text style={styles.required}> *</Text>}
|
||||
</Text>
|
||||
{label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
|
||||
</AppText>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
@@ -79,18 +83,25 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
pickFile(label.labelName, setFieldValue)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.fileText,
|
||||
value?.name && styles.fileSelected,
|
||||
]}
|
||||
>
|
||||
{value?.name || 'Upload PDF'}
|
||||
</Text>
|
||||
{value?.name ? (
|
||||
<View style={styles.fileSelectedRow}>
|
||||
<FileText size={14} color={colors.success} strokeWidth={2} />
|
||||
<AppText variant="caption" color={colors.text} bold style={{ flex: 1 } as any} numberOfLines={1}>
|
||||
{value.name}
|
||||
</AppText>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.fileSelectedRow}>
|
||||
<Upload size={14} color={colors.textSecondary} strokeWidth={2} />
|
||||
<AppText variant="caption" color={colors.textSecondary}>
|
||||
Upload PDF
|
||||
</AppText>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{showError && (
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
<AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
@@ -98,10 +109,10 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
|
||||
return (
|
||||
<View key={label.labelName} style={styles.field}>
|
||||
<Text style={styles.label}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.label as any}>
|
||||
{label.labelName}
|
||||
{label.mandatory && <Text style={styles.required}> *</Text>}
|
||||
</Text>
|
||||
{label.mandatory && <AppText variant="caption" color={colors.error}> *</AppText>}
|
||||
</AppText>
|
||||
|
||||
<TextInput
|
||||
style={[
|
||||
@@ -109,7 +120,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
showError && styles.errorBorder,
|
||||
]}
|
||||
placeholder={`Enter ${label.labelName}`}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={value}
|
||||
onChangeText={(text) =>
|
||||
setFieldValue(label.labelName, text)
|
||||
@@ -122,7 +133,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
/>
|
||||
|
||||
{showError && (
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
<AppText variant="caption" color={colors.error} style={styles.errorText as any}>{error}</AppText>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
@@ -210,26 +221,26 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<Text style={styles.headerTitle}>Technical Documents</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
<AppText variant="h4" color={colors.text}>Technical Documents</AppText>
|
||||
<AppText variant="caption" color={colors.textSecondary}>
|
||||
Fill the form, Click save when you're done.
|
||||
</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity onPress={onClose} style={styles.closeBtn}>
|
||||
<Text style={styles.closeText}>✕</Text>
|
||||
<X size={20} color={colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.templateHeading}>Template</Text>
|
||||
<AppText variant="h3" color={colors.text} centered style={{ marginBottom: 16 } as any}>Template</AppText>
|
||||
|
||||
<View style={styles.templateBox}>
|
||||
<Text style={styles.templateName}>
|
||||
<AppText variant="bodySmall" color={colors.text} bold style={{ marginBottom: 12 } as any}>
|
||||
{template.templateName}
|
||||
</Text>
|
||||
</AppText>
|
||||
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
@@ -264,12 +275,12 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
{loading ? (
|
||||
<Loader
|
||||
size="small"
|
||||
color={colors.white}
|
||||
color="#fff"
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.submitText}>
|
||||
<AppText variant="button" color="#fff" bold style={styles.submitText as any}>
|
||||
Submit
|
||||
</Text>
|
||||
</AppText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -287,10 +298,11 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
||||
|
||||
export default TemplateFormModal
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const makeStyles = (colors: any, spacing: any, typography: any) =>
|
||||
StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F9FAFB',
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
@@ -298,118 +310,86 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.surface,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
borderColor: colors.border,
|
||||
},
|
||||
headerLeft: {
|
||||
flex: 1,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
},
|
||||
headerSubtitle: {
|
||||
fontSize: 12,
|
||||
color: '#6B7280',
|
||||
marginTop: 2,
|
||||
},
|
||||
closeBtn: {
|
||||
padding: 6,
|
||||
},
|
||||
closeText: {
|
||||
fontSize: 18,
|
||||
color: '#6B7280',
|
||||
padding: 8,
|
||||
backgroundColor: colors.overlay,
|
||||
borderRadius: 10,
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderColor: '#E5E7EB',
|
||||
paddingVertical: 20
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 18,
|
||||
marginTop: spacing.m,
|
||||
padding: spacing.m,
|
||||
...Platform.select({
|
||||
ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 12 },
|
||||
android: { elevation: 3 },
|
||||
}),
|
||||
},
|
||||
templateHeading: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
|
||||
templateBox: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
borderRadius: 10,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
},
|
||||
templateName: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
marginBottom: 12,
|
||||
},
|
||||
field: {
|
||||
marginBottom: 14,
|
||||
},
|
||||
label: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 6,
|
||||
},
|
||||
required: {
|
||||
color: '#EF4444',
|
||||
},
|
||||
} as any,
|
||||
input: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.background,
|
||||
borderWidth: 1.2,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
fontSize: 14,
|
||||
color: '#111827',
|
||||
color: colors.text,
|
||||
},
|
||||
filePicker: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1.2,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
},
|
||||
fileText: {
|
||||
color: '#6B7280',
|
||||
fontSize: 14,
|
||||
},
|
||||
fileSelected: {
|
||||
color: '#111827',
|
||||
fontWeight: '600',
|
||||
fileSelectedRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
errorText: {
|
||||
color: '#EF4444',
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
} as any,
|
||||
errorBorder: {
|
||||
borderColor: '#EF4444',
|
||||
borderColor: colors.error,
|
||||
},
|
||||
submitRow: {
|
||||
marginTop: 20,
|
||||
alignItems: 'flex-end',
|
||||
height: 50
|
||||
height: 50,
|
||||
},
|
||||
submitBtn: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 22,
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 14,
|
||||
minWidth: 120,
|
||||
alignItems: 'center',
|
||||
},
|
||||
submitText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '700',
|
||||
fontSize: 14,
|
||||
},
|
||||
letterSpacing: 0.5,
|
||||
} as any,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,52 +1,74 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { View, FlatList, StyleSheet, Platform, Alert, BackHandler, Text } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Alert,
|
||||
BackHandler,
|
||||
} from 'react-native';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
import { InsideHeader } from '../../Component/Index';
|
||||
import colors from '../../Constant/Color';
|
||||
import { useWebSocketService } from '../../socket/Socket';
|
||||
import Variables from '../../Constant/Variable';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { apiCall } from '../../Axios/Axios';
|
||||
import { AUCTION_ONE_TIME_BID, FORWARD_AUCTION_ITEM, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECTED_AUCTION_ITEM } from '../../Services/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 ConfirmBidModal from './Component/ConfirmBidModal';
|
||||
import GoBackModal from './Component/GoBackModal';
|
||||
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import CountdownTimer from '../../Component/Counter/CountdownTimer';
|
||||
import FontsFamily from '../../Constant/FontsFamily';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
import { Gavel, Building2, Clock4 } from 'lucide-react-native';
|
||||
|
||||
const StartBidding = () => {
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const SelectedAuction = useSelector((state: any) => state.auction.auction);
|
||||
const loginUser = useSelector((state: any) => state.token.token)
|
||||
const Navigation: any = useNavigation()
|
||||
const loginUser = useSelector((state: any) => state.token.token);
|
||||
const Navigation: any = useNavigation();
|
||||
|
||||
const [items, setItems] = useState<any>([]);
|
||||
const [bidValues, setBidValues] = useState<Record<number, number>>({});
|
||||
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, bidPayload: {}, oneTimeBid: false, rebid: false, reload: false })
|
||||
const [allBoolean, setAllBoolean] = useState({
|
||||
showSuccess: false,
|
||||
showDialog: false,
|
||||
showBackDialog: false,
|
||||
bidPayload: {} as any,
|
||||
oneTimeBid: false,
|
||||
rebid: false,
|
||||
reload: false,
|
||||
});
|
||||
|
||||
const { subscribe, createRoom, placeBid } = useWebSocketService(
|
||||
Variables.webSocketUrl,
|
||||
() => console.log('Connected!'),
|
||||
(error) => console.log('WebSocket Error:', error),
|
||||
(error: any) => console.log('WebSocket Error:', error),
|
||||
loginUser.token,
|
||||
);
|
||||
|
||||
const getAuction = async () => {
|
||||
const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId });
|
||||
if (selectedRes.statusCode == 200 && selectedRes.data.contractorId.length) {
|
||||
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
|
||||
}
|
||||
if (selectedRes.statusCode === 200 && selectedRes.data.contractorId.length) {
|
||||
setAllBoolean(prev => ({ ...prev, oneTimeBid: true }));
|
||||
}
|
||||
};
|
||||
|
||||
const getOneTimeBidValue = async () => {
|
||||
try {
|
||||
const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId });
|
||||
if (selectedRes.statusCode == 200) {
|
||||
return selectedRes.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
return selectedRes.statusCode === 200 ? selectedRes.data : [];
|
||||
} catch { return []; }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
const getAllAcutionItems = async () => {
|
||||
try {
|
||||
const payload = { auctionNumber: SelectedAuction.auctionNumber };
|
||||
@@ -58,87 +80,40 @@ const StartBidding = () => {
|
||||
}
|
||||
|
||||
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
|
||||
const uniqueArray: number[] = Array.from(new Set(selectedRes?.data || []));
|
||||
const filteredItems = auctionItem.filter(item => uniqueArray.includes(item.id));
|
||||
|
||||
const uniqueArray: number[] = Array.from(
|
||||
new Set(selectedRes?.data || [])
|
||||
);
|
||||
|
||||
const filteredItems = auctionItem.filter(item =>
|
||||
uniqueArray.includes(item.id)
|
||||
);
|
||||
|
||||
if (SelectedAuction.auctionCategories == 'Live') {
|
||||
if (SelectedAuction.auctionCategories === 'Live') {
|
||||
const result = await createRoom(SelectedAuction.auctionNumber);
|
||||
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, data => {
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data: any) => {
|
||||
console.log('🔥 BID UPDATE:', data);
|
||||
});
|
||||
const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>();
|
||||
|
||||
(result || []).forEach((r: any) => {
|
||||
bidAmountMap2.set(r.auctionItemId, {
|
||||
bidAmount: r.bidAmount,
|
||||
contractorId: r.contractorId,
|
||||
bidAmountMap2.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId });
|
||||
});
|
||||
});
|
||||
// Check One Time bid enable butoon
|
||||
if (SelectedAuction.auctionCategories != 'Live') {
|
||||
const myBid = result.find(
|
||||
(bid: any) => bid.contractorId === loginUser.contractorId
|
||||
);
|
||||
|
||||
if (myBid) {
|
||||
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
|
||||
}
|
||||
}
|
||||
|
||||
const bidAmountMap = new Map<number, number>();
|
||||
|
||||
(result || []).forEach((r: any) => {
|
||||
bidAmountMap.set(r.auctionItemId, r.bidAmount);
|
||||
});
|
||||
|
||||
const updatedFilteredItems = filteredItems.map(item => {
|
||||
const bidData = bidAmountMap2.get(item.id);
|
||||
return {
|
||||
...item,
|
||||
bidAmount: bidData?.bidAmount ?? item.auctionStartValueFigure,
|
||||
bidAvilable: bidData?.bidAmount ?? 0,
|
||||
me: bidData?.contractorId == loginUser.contractorId ? true : false
|
||||
me: bidData?.contractorId === loginUser.contractorId,
|
||||
};
|
||||
});
|
||||
|
||||
const initialBids: Record<number, number> = {};
|
||||
updatedFilteredItems.forEach(item => {
|
||||
initialBids[item.id] = item.bidAmount;
|
||||
});
|
||||
updatedFilteredItems.forEach(item => { initialBids[item.id] = item.bidAmount; });
|
||||
setBidValues(initialBids);
|
||||
setItems(updatedFilteredItems);
|
||||
} else {
|
||||
let result = []
|
||||
if (SelectedAuction.rebidAllowed) {
|
||||
setAllBoolean((prev: any) => ({ ...prev, rebid: true }))
|
||||
}
|
||||
|
||||
if (SelectedAuction.auctionCategories != 'Live') {
|
||||
result = await getOneTimeBidValue()
|
||||
}
|
||||
if (SelectedAuction.rebidAllowed) setAllBoolean(prev => ({ ...prev, rebid: true }));
|
||||
let result: any[] = [];
|
||||
if (SelectedAuction.auctionCategories !== 'Live') result = await getOneTimeBidValue();
|
||||
const bidMap = new Map<number, any>();
|
||||
|
||||
result.forEach((bid: any) => {
|
||||
bidMap.set(bid.auctionItemId, bid);
|
||||
});
|
||||
result.forEach((bid: any) => bidMap.set(bid.auctionItemId, bid));
|
||||
const mergedData = auctionItem.map((item: any) => {
|
||||
const bid = bidMap.get(item.id);
|
||||
|
||||
if (!bid) {
|
||||
return {
|
||||
...item,
|
||||
bidAmount: item.auctionStartValueFigure,
|
||||
contractorId: item.contractorId,
|
||||
};
|
||||
}
|
||||
|
||||
if (!bid) return { ...item, bidAmount: item.auctionStartValueFigure, contractorId: item.contractorId };
|
||||
return {
|
||||
...item,
|
||||
bidAmount: bid.bidAmount,
|
||||
@@ -150,56 +125,36 @@ const StartBidding = () => {
|
||||
};
|
||||
});
|
||||
const initialBids: Record<number, number> = {};
|
||||
mergedData.forEach(item => {
|
||||
initialBids[item.id] = item.bidAmount;
|
||||
});
|
||||
setBidValues(initialBids);
|
||||
setItems(mergedData)
|
||||
mergedData.forEach(item => { initialBids[item.id] = item.bidAmount; });
|
||||
setBidValues(initialBids);
|
||||
setItems(mergedData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching auction items:', error);
|
||||
}
|
||||
};
|
||||
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message) => {
|
||||
|
||||
const result = message[message.length - 1]
|
||||
|
||||
const updatedBidItem = (prev: any[]) =>
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message: any) => {
|
||||
const result = message[message.length - 1];
|
||||
setItems((prev: any[]) =>
|
||||
prev.map(item =>
|
||||
item.id === result.auctionItemId
|
||||
? { ...item, bidAmount: result.bidAmount, me: result.contractorId == loginUser.contractorId ? true : false }
|
||||
: item
|
||||
? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
|
||||
: item,
|
||||
),
|
||||
);
|
||||
|
||||
setItems(updatedBidItem);
|
||||
|
||||
setBidValues(prev => ({
|
||||
...prev,
|
||||
[result.auctionItemId]: result.bidAmount,
|
||||
}));
|
||||
try {
|
||||
|
||||
} catch (err) {
|
||||
console.log('JSON parse error:', err);
|
||||
}
|
||||
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (SelectedAuction.auctionCategories == 'One-time') {
|
||||
getAuction()
|
||||
}
|
||||
if (SelectedAuction.auctionCategories === 'One-time') getAuction();
|
||||
getAllAcutionItems();
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data) => { });
|
||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, () => {});
|
||||
}, [allBoolean.reload]);
|
||||
|
||||
useEffect(() => {
|
||||
const initial: Record<number, number> = {};
|
||||
items.forEach((item: any) => {
|
||||
initial[item.id] = item.bidAmount + item.bidVariationValue;
|
||||
});
|
||||
items.forEach((item: any) => { initial[item.id] = item.bidAmount + item.bidVariationValue; });
|
||||
setBidValues(initial);
|
||||
}, []);
|
||||
|
||||
@@ -216,51 +171,14 @@ const StartBidding = () => {
|
||||
fullName: loginUser.fullName,
|
||||
roomId: SelectedAuction.auctionNumber,
|
||||
};
|
||||
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, }));
|
||||
Alert.alert(
|
||||
'Confirm Bid',
|
||||
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Submit',
|
||||
onPress: () => {
|
||||
|
||||
placeBidSubmit(payload);
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
setAllBoolean(prev => ({ ...prev, bidPayload: payload, showDialog: true }));
|
||||
};
|
||||
|
||||
const placeBidSubmit = async (data: any) => {
|
||||
setAllBoolean(prev => ({ ...prev, showDialog: false }));
|
||||
try {
|
||||
const result: any = await placeBid(data);
|
||||
|
||||
if (result?.status && result.data) {
|
||||
const updatedBidItem = (prev: any[]) =>
|
||||
prev.map(item =>
|
||||
item.id === result.data.auctionItemId
|
||||
? { ...item, bidAmount: result.data.bidAmount, me: result.data?.contractorId == loginUser.contractorId ? true : false }
|
||||
: item
|
||||
);
|
||||
|
||||
// setItems(updatedBidItem);
|
||||
|
||||
|
||||
// setBidValues((prev: any) => ({
|
||||
// ...prev,
|
||||
// [result.data.auctionItemId]: {
|
||||
// ...prev[result.data.auctionItemId],
|
||||
// bidAmount: result.data.bidAmount,
|
||||
// },
|
||||
// }));
|
||||
getAllAcutionItems()
|
||||
}
|
||||
if (result?.status && result.data) getAllAcutionItems();
|
||||
} catch (error) {
|
||||
console.error('Error placing bid:', error);
|
||||
}
|
||||
@@ -269,55 +187,75 @@ const StartBidding = () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const onBackPress = () => {
|
||||
Alert.alert(
|
||||
'Confirm',
|
||||
'Do you want to go back to Dashboard?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Yes',
|
||||
onPress: () => {
|
||||
Navigation.dispatch(
|
||||
CommonActions.reset({
|
||||
index: 0,
|
||||
routes: [{ name: NavigationString.Home }],
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
|
||||
return true; // block default back action
|
||||
setAllBoolean(prev => ({ ...prev, showBackDialog: true }));
|
||||
return true;
|
||||
};
|
||||
|
||||
const subscription = BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
onBackPress
|
||||
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||
return () => sub.remove();
|
||||
}, [Navigation]),
|
||||
);
|
||||
|
||||
return () => subscription.remove();
|
||||
}, [Navigation])
|
||||
);
|
||||
const styles = makeStyles(colors, spacing, shadows);
|
||||
|
||||
const biddingEndDate =
|
||||
SelectedAuction?.auctionStatus === 'Amended'
|
||||
? SelectedAuction?.amendedkeyDates?.auctionBidding?.endDateTime
|
||||
: SelectedAuction?.keyDates?.auctionBidding?.endDateTime;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.root}>
|
||||
<InsideHeader title="Bidding Panel" showArrow />
|
||||
<View style={styles.container2}>
|
||||
<Text style={styles.title}>
|
||||
{SelectedAuction.deptName}
|
||||
</Text>
|
||||
|
||||
<View style={styles.timerWrapper}>
|
||||
{/* ── Top Info Band ── */}
|
||||
<Animated.View entering={FadeInDown.springify()} style={styles.infoBand}>
|
||||
{/* Department row */}
|
||||
<View style={styles.deptRow}>
|
||||
<View style={[styles.deptIcon, { backgroundColor: `${colors.primary}15` }]}>
|
||||
<Building2 size={16} color={colors.primary} strokeWidth={2} />
|
||||
</View>
|
||||
<AppText variant="bodySmall" color={colors.text} bold numberOfLines={1} style={styles.deptName as any}>
|
||||
{SelectedAuction.deptName ?? 'Auction Department'}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
{/* Timer card */}
|
||||
<View style={[styles.timerCard, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}25` }]}>
|
||||
<View style={styles.timerLabelRow}>
|
||||
<Clock4 size={13} color={colors.primary} strokeWidth={2} />
|
||||
<AppText variant="caption" color={colors.primary} bold style={styles.timerLabel as any}>
|
||||
BIDDING ENDS IN
|
||||
</AppText>
|
||||
</View>
|
||||
<CountdownTimer
|
||||
endDate={SelectedAuction.keyDates.auctionBidding.endDateTime}
|
||||
myStyle={{ fontSize: 30 }}
|
||||
size={30}
|
||||
endDate={biddingEndDate}
|
||||
myStyle={{ fontSize: 28, fontWeight: '900', color: colors.primary, letterSpacing: 1 }}
|
||||
size={28}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Auction chip */}
|
||||
<View style={styles.chipRow}>
|
||||
<View style={[styles.chip, { backgroundColor: `${colors.primary}12` }]}>
|
||||
<Gavel size={12} color={colors.primary} strokeWidth={2} />
|
||||
<AppText variant="caption" color={colors.primary} bold style={styles.chipText as any}>
|
||||
{SelectedAuction.auctionNumber}
|
||||
</AppText>
|
||||
</View>
|
||||
<View style={[styles.chip, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? `${colors.success}15` : `${colors.textSecondary}12` }]}>
|
||||
<View style={[styles.liveDot, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary }]} />
|
||||
<AppText
|
||||
variant="caption"
|
||||
color={SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary}
|
||||
bold
|
||||
style={styles.chipText as any}
|
||||
>
|
||||
{SelectedAuction.auctionCategories ?? 'Auction'}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
{/* ── Items List ── */}
|
||||
<FlatList
|
||||
data={items}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
@@ -327,15 +265,17 @@ const StartBidding = () => {
|
||||
bidValue={bidValues[item.id] || 0}
|
||||
onIncrease={() => handleUpdate(item.id, item.bidVariationValue)}
|
||||
onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)}
|
||||
onPlaceBid={() => { submitBid(item) }}
|
||||
onPlaceBid={() => submitBid(item)}
|
||||
SelectedAuction={SelectedAuction}
|
||||
allBoolean={allBoolean}
|
||||
setAllBoolean={setAllBoolean}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 300 }}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
|
||||
{/* ── Success overlay ── */}
|
||||
{allBoolean.showSuccess && (
|
||||
<View style={styles.overlay}>
|
||||
<LottieView
|
||||
@@ -346,66 +286,117 @@ const StartBidding = () => {
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ConfirmBidModal
|
||||
visible={allBoolean.showDialog}
|
||||
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
|
||||
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
|
||||
bidAmount={allBoolean.bidPayload?.bidAmount}
|
||||
/>
|
||||
|
||||
<GoBackModal
|
||||
visible={allBoolean.showBackDialog}
|
||||
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
|
||||
onConfirm={() => {
|
||||
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
|
||||
Navigation.dispatch(
|
||||
CommonActions.reset({
|
||||
index: 0,
|
||||
routes: [{ name: NavigationString.Home }],
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartBidding;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.white },
|
||||
container2: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 8,
|
||||
/* ─── Styles factory ─── */
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any) =>
|
||||
StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontFamily: FontsFamily.poppinsSemiBold,
|
||||
color: colors.black,
|
||||
marginBottom: 6,
|
||||
// textAlign:'center'
|
||||
|
||||
/* Info band */
|
||||
infoBand: {
|
||||
backgroundColor: colors.surface,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: spacing.m,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.border,
|
||||
gap: spacing.s,
|
||||
...shadows.light,
|
||||
},
|
||||
timerWrapper: {
|
||||
deptRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.s,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#FFF',
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
overflow: 'hidden',
|
||||
...Platform.select({ ios: { shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, android: { elevation: 2 } }),
|
||||
deptIcon: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 9,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
deptName: {
|
||||
flex: 1,
|
||||
} as any,
|
||||
|
||||
/* Timer */
|
||||
timerCard: {
|
||||
borderRadius: 16,
|
||||
borderWidth: 1.2,
|
||||
paddingVertical: spacing.m,
|
||||
paddingHorizontal: spacing.m,
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
timerLabelRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
timerLabel: {
|
||||
letterSpacing: 1.2,
|
||||
} as any,
|
||||
|
||||
/* Chips */
|
||||
chipRow: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.s,
|
||||
},
|
||||
chip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: spacing.s + 4,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 20,
|
||||
},
|
||||
chipText: {
|
||||
letterSpacing: 0.3,
|
||||
} as any,
|
||||
liveDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
},
|
||||
|
||||
listContent: {
|
||||
padding: spacing.m,
|
||||
paddingBottom: 120,
|
||||
},
|
||||
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: colors.overlay,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 99,
|
||||
},
|
||||
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 },
|
||||
});
|
||||
@@ -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 NavigationString from '../../Constant/NavigationString';
|
||||
import CountdownTimer from '../../Component/Counter/CountdownTimer';
|
||||
import FontsFamily from '../../Constant/FontsFamily';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
import ReverseBiddingCard from './Component/ReverseBiddingCard';
|
||||
import ConfirmBidModal from './Component/ConfirmBidModal';
|
||||
import GoBackModal from './Component/GoBackModal';
|
||||
|
||||
const StartReverseBidding = () => {
|
||||
const { colors } = useTheme();
|
||||
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
||||
const SelectedAuction = useSelector((state: any) => state.auction.auction);
|
||||
const loginUser = useSelector((state: any) => state.token.token)
|
||||
const Navigation: any = useNavigation()
|
||||
const [items, setItems] = useState<any>([]);
|
||||
const [bidValues, setBidValues] = useState<Record<number, number>>({});
|
||||
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, bidPayload: {}, oneTimeBid: false, rebid: false, reload: false })
|
||||
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, showBackDialog: false, bidPayload: {} as any, oneTimeBid: false, rebid: false, reload: false })
|
||||
const { subscribe, createRoom, placeBid } = useWebSocketService(
|
||||
Variables.webSocketUrl,
|
||||
() => console.log('Connected!'),
|
||||
@@ -216,27 +221,11 @@ const StartReverseBidding = () => {
|
||||
fullName: loginUser.fullName,
|
||||
roomId: SelectedAuction.auctionNumber,
|
||||
};
|
||||
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, }));
|
||||
Alert.alert(
|
||||
'Confirm Bid',
|
||||
`Are you sure you want to place a bid of ₹${payload.bidAmount}?`,
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Submit',
|
||||
onPress: () => {
|
||||
placeBidSubmit(payload);
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, showDialog: true }));
|
||||
};
|
||||
|
||||
const placeBidSubmit = async (data: any) => {
|
||||
setAllBoolean((prev: any) => ({ ...prev, showDialog: false }));
|
||||
try {
|
||||
const result: any = await placeBid(data);
|
||||
|
||||
@@ -258,26 +247,7 @@ const StartReverseBidding = () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const onBackPress = () => {
|
||||
Alert.alert(
|
||||
'Confirm',
|
||||
'Do you want to go back to Dashboard?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Yes',
|
||||
onPress: () => {
|
||||
Navigation.dispatch(
|
||||
CommonActions.reset({
|
||||
index: 0,
|
||||
routes: [{ name: NavigationString.Home }],
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
|
||||
setAllBoolean((prev: any) => ({ ...prev, showBackDialog: true }));
|
||||
return true; // block default back action
|
||||
};
|
||||
|
||||
@@ -294,9 +264,9 @@ const StartReverseBidding = () => {
|
||||
<View style={styles.container}>
|
||||
<InsideHeader title="Bidding Panel" showArrow />
|
||||
<View style={styles.container2}>
|
||||
<Text style={styles.title}>
|
||||
<AppText variant="h3" color={colors.text} bold style={styles.title as any}>
|
||||
{SelectedAuction.deptName}
|
||||
</Text>
|
||||
</AppText>
|
||||
|
||||
<View style={styles.timerWrapper}>
|
||||
<CountdownTimer
|
||||
@@ -335,66 +305,45 @@ const StartReverseBidding = () => {
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ConfirmBidModal
|
||||
visible={allBoolean.showDialog}
|
||||
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
|
||||
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
|
||||
bidAmount={allBoolean.bidPayload?.bidAmount}
|
||||
/>
|
||||
|
||||
<GoBackModal
|
||||
visible={allBoolean.showBackDialog}
|
||||
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
|
||||
onConfirm={() => {
|
||||
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
|
||||
Navigation.dispatch(
|
||||
CommonActions.reset({
|
||||
index: 0,
|
||||
routes: [{ name: NavigationString.Home }],
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartReverseBidding;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.white },
|
||||
const makeStyles = (colors: any) => StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.background },
|
||||
container2: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontFamily: FontsFamily.poppinsSemiBold,
|
||||
color: colors.black,
|
||||
marginBottom: 6,
|
||||
// textAlign:'center'
|
||||
},
|
||||
} as any,
|
||||
timerWrapper: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#FFF',
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
overflow: 'hidden',
|
||||
...Platform.select({ ios: { shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, android: { elevation: 2 } }),
|
||||
},
|
||||
leadingCard: { borderColor: '#22C55E', borderWidth: 2, backgroundColor: '#F0FDF4' },
|
||||
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' },
|
||||
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||
productName: { fontSize: 18, fontWeight: 'bold', color: '#1E293B' },
|
||||
itemIdText: { fontSize: 12, color: '#64748B', marginTop: 2 },
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
liveBadge: { backgroundColor: '#FEE2E2' },
|
||||
meBadge: { backgroundColor: '#BBF7D0' },
|
||||
badgeText: { fontSize: 10, fontWeight: '900' },
|
||||
liveText: { color: '#EF4444' },
|
||||
meText: { color: '#15803D' },
|
||||
arrow: { fontSize: 10, color: '#94A3B8' },
|
||||
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
|
||||
label: { fontSize: 11, color: '#94A3B8', textTransform: 'uppercase', fontWeight: '600' },
|
||||
mainPrice: { fontSize: 22, fontWeight: '900', color: '#0F172A' },
|
||||
incrementText: { fontSize: 16, fontWeight: '700', color: '#3B82F6' },
|
||||
collapsibleContainer: { overflow: 'hidden' },
|
||||
divider: { height: 1, backgroundColor: '#E2E8F0', marginVertical: 10 },
|
||||
bidLabel: { fontSize: 13, fontWeight: '600', color: '#475569', marginBottom: 10 },
|
||||
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
|
||||
stepBtn: { width: 45, height: 45, backgroundColor: colors.primary, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
disabledBtn: { backgroundColor: '#CBD5E1' },
|
||||
stepText: { color: '#FFF', fontSize: 20, fontWeight: 'bold' },
|
||||
inputBox: { flex: 1, height: 45, backgroundColor: '#F1F5F9', borderRadius: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
|
||||
currency: { fontSize: 16, fontWeight: 'bold', color: '#64748B', marginRight: 4 },
|
||||
bidValueText: { fontSize: 18, fontWeight: '800', color: '#0F172A' },
|
||||
submitBtn: { height: 50, backgroundColor: colors.primary, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
|
||||
submitBtnText: { color: '#FFF', fontSize: 16, fontWeight: 'bold' },
|
||||
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(255,255,255,0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 99 },
|
||||
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.overlay, justifyContent: 'center', alignItems: 'center', zIndex: 99 },
|
||||
});
|
||||
@@ -1,226 +1,134 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { View, Text, StyleSheet, Dimensions, FlatList, ActivityIndicator, Alert } from 'react-native';
|
||||
import Animated, { useSharedValue, useAnimatedStyle, withDelay, withTiming } from 'react-native-reanimated';
|
||||
import { Layers, Clock, ShoppingCart, X, FileText } from 'lucide-react-native';
|
||||
import { Header } from '../../Component/Index';
|
||||
import colors from '../../Constant/Color';
|
||||
import { View, StyleSheet, Dimensions, FlatList, ActivityIndicator, Alert, ScrollView, ImageBackground, TouchableOpacity, Platform } from 'react-native';
|
||||
import Animated, { FadeInDown, FadeInUp } from 'react-native-reanimated';
|
||||
import { Layers, Clock, ShoppingCart, X, FileText, ArrowUpRight, Menu, Radio } from 'lucide-react-native';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import { useDashboard, useESetuAdharVarification, useTenderDashboard } from '../../Services/ApiCall/Hooks';
|
||||
import { useSelector } from 'react-redux';
|
||||
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog';
|
||||
import { generateTransactionId } from '../../Helper/uniqueId';
|
||||
import { getToken } from '../../Redux/Slices/Token';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
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 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 { data, isLoading } = useDashboard();
|
||||
const { data: tenderData, isLoading: tenderLoading } = useTenderDashboard();
|
||||
const { colors, spacing, typography, shadows } = useTheme();
|
||||
|
||||
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(
|
||||
useCallback(() => {
|
||||
setIsNavigating(false);
|
||||
|
||||
return () => {
|
||||
// cleanup if needed
|
||||
};
|
||||
}, [])
|
||||
if (userType === 'auction') refetchDashboard();
|
||||
else refetchTender();
|
||||
}, [userType])
|
||||
);
|
||||
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(() => {
|
||||
if (user?.userType === "auction") {
|
||||
setDASHBOARD_CARDS([
|
||||
{
|
||||
title: 'Total Auctions',
|
||||
value: dashboard?.myAuctionCount ?? 0,
|
||||
desc: 'Auctions purchased by you',
|
||||
icon: Layers,
|
||||
color: '#2563EB',
|
||||
},
|
||||
{
|
||||
title: 'Live Auctions',
|
||||
value: dashboard?.liveAuctionCount ?? 0,
|
||||
desc: 'Currently live',
|
||||
icon: Clock,
|
||||
color: '#0284C7',
|
||||
},
|
||||
{
|
||||
title: 'Participated Auctions',
|
||||
value: dashboard?.currentBiddingCount ?? 0,
|
||||
desc: 'Participated in auction',
|
||||
icon: ShoppingCart,
|
||||
color: '#7C3AED',
|
||||
},
|
||||
{
|
||||
title: 'Cancelled Auctions',
|
||||
value: dashboard?.cancelledAuctionCount ?? 0,
|
||||
desc: 'Cancelled by department',
|
||||
icon: X,
|
||||
color: '#DC2626',
|
||||
},
|
||||
]);
|
||||
} 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',
|
||||
},
|
||||
]);
|
||||
if (userType === 'auction' && dashboardData) {
|
||||
console.log(dashboardData.data, "lkkllklklklklklklklklk")
|
||||
setStats(dashboardData.data || {});
|
||||
} else if (userType === 'tender' && tenderData) {
|
||||
console.log(tenderData.data, "bonds")
|
||||
setStats(tenderData.data || {});
|
||||
}
|
||||
},[tenderData,dashboard])
|
||||
}, [dashboardData, tenderData, userType]);
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
<Header title="Dashboard" />
|
||||
|
||||
<ConfirmDialog
|
||||
visible={showDialog}
|
||||
title="Aadhaar Verification Required"
|
||||
message="To access features, please complete your Aadhaar verification."
|
||||
confirmText={isPending || isNavigating ? "Processing..." : "Proceed"}
|
||||
cancelText="Cancel"
|
||||
onConfirm={handleProceedVerification}
|
||||
onClose={() => !isPending && !isNavigating && setShowDialog(false)}
|
||||
loading={isPending || isNavigating}
|
||||
/>
|
||||
|
||||
{isNavigating && (
|
||||
<View style={styles.navigationOverlay}>
|
||||
<ActivityIndicator size="large" color={colors.primary || '#2563EB'} />
|
||||
<Text style={styles.loadingText}>Loading verification...</Text>
|
||||
const renderGlassCard = (title: string, value: string | number, icon: React.ReactNode, index: number) => (
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(600).delay(index * 100)}
|
||||
style={[styles.glassCard, {
|
||||
backgroundColor: colors.glass,
|
||||
borderColor: colors.overlay,
|
||||
}]}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: colors.overlay }]}>
|
||||
{icon}
|
||||
</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 (
|
||||
<StatCard
|
||||
item={item}
|
||||
index={index}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* Large Typography Header */}
|
||||
<Animated.View entering={FadeInDown.duration(800)} style={styles.headerContainer}>
|
||||
<View>
|
||||
<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 >
|
||||
);
|
||||
};
|
||||
@@ -228,81 +136,56 @@ if (user?.userType === "auction") {
|
||||
export default Home;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
list: {
|
||||
padding: 16,
|
||||
paddingBottom: 24,
|
||||
scrollContent: {
|
||||
paddingTop: 60,
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 100, // Space for floating tab bar
|
||||
},
|
||||
row: {
|
||||
headerContainer: {
|
||||
marginBottom: 40,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
alignItems: 'flex-start'
|
||||
},
|
||||
card: {
|
||||
width: CARD_WIDTH,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
sectionContainer: {
|
||||
marginBottom: 32,
|
||||
},
|
||||
gridContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
glassCard: {
|
||||
width: (width - 48) / 2, // 2 column grid
|
||||
padding: 16,
|
||||
borderRadius: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
marginBottom: 16,
|
||||
// Glassmorphism creates a subtle depth without heavy shadows
|
||||
},
|
||||
header: {
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
flex: 1,
|
||||
paddingRight: 8,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: 'row',
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 12,
|
||||
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',
|
||||
alignItems: 'center',
|
||||
zIndex: 999,
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 16,
|
||||
cardValue: {
|
||||
fontSize: 28,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
marginBottom: 4,
|
||||
},
|
||||
cardLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
CommonActions.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'Login' }], // Adjust to your login screen route name
|
||||
routes: [{ name: 'Login' }],
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const startSuccessCountdown = () => {
|
||||
@@ -57,7 +61,7 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
|
||||
|
||||
successTimeoutRef.current = setTimeout(() => {
|
||||
clearInterval(countdownInterval);
|
||||
navigateToLogin();
|
||||
handleNavigationBack();
|
||||
}, SUCCESS_DELAY_MS);
|
||||
};
|
||||
|
||||
@@ -126,7 +130,17 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
|
||||
onLoadStart={handleLoadStart}
|
||||
onLoadEnd={handleLoadEnd}
|
||||
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 />}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useNavigation } from '@react-navigation/native';
|
||||
import colors from '../../Constant/Color';
|
||||
import { showToast } from '../../Constant/ShowDailog';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
|
||||
|
||||
interface ForgotModalProps {
|
||||
visible: boolean;
|
||||
@@ -58,7 +59,7 @@ const ForgotModal: React.FC<ForgotModalProps> = ({
|
||||
try {
|
||||
// Make API call to send OTP for forgot password
|
||||
const response = await fetch(
|
||||
`https://procurelinc.in/EProcurementSB/eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
|
||||
`${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -263,7 +263,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
||||
|
||||
try {
|
||||
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,
|
||||
{
|
||||
headers: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
@@ -15,8 +14,11 @@ import {
|
||||
TextInput,
|
||||
KeyboardAvoidingView,
|
||||
Alert,
|
||||
StatusBar,
|
||||
Image,
|
||||
} 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 {
|
||||
AlertCircle,
|
||||
UserCircle,
|
||||
@@ -51,15 +53,18 @@ import {
|
||||
Edit,
|
||||
X,
|
||||
Save,
|
||||
ChevronRight,
|
||||
LogOut
|
||||
} from 'lucide-react-native';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useContractorProfile, useUpdateUserProfile, useESetuAdharVarification } from '../../Services/ApiCall/Hooks';
|
||||
import colors from '../../Constant/Color';
|
||||
import { useSelector } from 'react-redux';
|
||||
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog';
|
||||
import { generateTransactionId } from '../../Helper/uniqueId';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
import ImagePath from '../../Constant/ImagePath';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
@@ -165,8 +170,11 @@ const hasSectionData = (values: any[]): boolean => {
|
||||
|
||||
// Main Component
|
||||
const ProfileTab: React.FC = () => {
|
||||
const { colors, spacing, shadows, isDarkMode } = useTheme();
|
||||
const styles = useMemo(() => createStyles(colors), [colors]);
|
||||
const { data, refetch, isLoading, error } = useContractorProfile();
|
||||
const user = useSelector((state: any) => state.token?.token);
|
||||
console.log("User: ", user)
|
||||
const Navigation: any = useNavigation();
|
||||
const { mutateAsync: eSetuAdharVarification, isPending } = useESetuAdharVarification();
|
||||
|
||||
@@ -182,7 +190,7 @@ const ProfileTab: React.FC = () => {
|
||||
const displayMobile = isAuctionUser ? user?.mobileNo : data?.mobileNo;
|
||||
|
||||
const handleAadhaarPress = () => {
|
||||
if (user?.esetuVerified === 'Not Verified') {
|
||||
if (user?.esetuVerified === 'Not Verified' || user?.esetuVerified === null) {
|
||||
setShowDialog(true);
|
||||
}
|
||||
};
|
||||
@@ -202,7 +210,7 @@ const ProfileTab: React.FC = () => {
|
||||
udf2: "77.34",
|
||||
udf3: "string",
|
||||
udf4: "string",
|
||||
responseUrl: "https://dev.procurelinc.in/"
|
||||
responseUrl: "http://dev.192.168.6.194:8090/"
|
||||
};
|
||||
|
||||
const res = await eSetuAdharVarification(payload);
|
||||
@@ -221,7 +229,7 @@ const ProfileTab: React.FC = () => {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={styles.loadingText}>Loading profile...</Text>
|
||||
<AppText style={styles.loadingText}>Loading profile...</AppText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -230,9 +238,9 @@ const ProfileTab: React.FC = () => {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<AlertCircle size={64} color={colors.error} />
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
<TouchableOpacity style={styles.retryButton} onPress={refetch}>
|
||||
<Text style={styles.retryButtonText}>Retry</Text>
|
||||
<AppText style={styles.screenErrorText}>{error instanceof Error ? error.message : String(error)}</AppText>
|
||||
<TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
|
||||
<AppText style={styles.retryButtonText}>Retry</AppText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
@@ -243,7 +251,7 @@ const ProfileTab: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
style={[styles.container, { backgroundColor: colors.background }]}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
@@ -254,11 +262,6 @@ const ProfileTab: React.FC = () => {
|
||||
/>
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
updateCellsBatchingPeriod={50}
|
||||
initialNumToRender={8}
|
||||
windowSize={10}
|
||||
>
|
||||
{/* Header with Gradient */}
|
||||
<ProfileHeader data={data} displayName={displayName || data.fullName} />
|
||||
@@ -352,7 +355,7 @@ const ProfileTab: React.FC = () => {
|
||||
label="Expiry Date"
|
||||
value={data.expiryDate}
|
||||
IconComponent={CalendarClock}
|
||||
valueColor={colors.warning}
|
||||
valueColor={colors.text}
|
||||
/>
|
||||
</InfoSection>
|
||||
)}
|
||||
@@ -401,6 +404,8 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
|
||||
data,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
const styles = useMemo(() => createStyles(colors), [colors]);
|
||||
const [formData, setFormData] = useState<UpdateProfilePayload>({
|
||||
firstName: data.firstName || '',
|
||||
middleName: data.middleName || '',
|
||||
@@ -484,83 +489,83 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
|
||||
style={styles.modalOverlay}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={[styles.modalContent, { backgroundColor: colors.background }]}>
|
||||
<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}>
|
||||
<X size={24} color={colors.darkText} />
|
||||
<X size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.inputLabel}>First Name *</Text>
|
||||
<AppText variant="bodySmall" style={styles.inputLabel}>First Name *</AppText>
|
||||
<TextInput
|
||||
style={[styles.input, errors.firstName && styles.inputError]}
|
||||
value={formData.firstName}
|
||||
onChangeText={(text) => setFormData({ ...formData, firstName: text })}
|
||||
placeholder="Enter first name"
|
||||
placeholderTextColor={colors.grayText}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<Text style={styles.errorText}>{errors.firstName}</Text>
|
||||
<AppText variant="caption" style={styles.errorText}>{errors.firstName}</AppText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.inputLabel}>Middle Name</Text>
|
||||
<AppText variant="bodySmall" style={styles.inputLabel}>Middle Name</AppText>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={formData.middleName}
|
||||
onChangeText={(text) => setFormData({ ...formData, middleName: text })}
|
||||
placeholder="Enter middle name"
|
||||
placeholderTextColor={colors.grayText}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.inputLabel}>Last Name *</Text>
|
||||
<AppText variant="bodySmall" style={styles.inputLabel}>Last Name *</AppText>
|
||||
<TextInput
|
||||
style={[styles.input, errors.lastName && styles.inputError]}
|
||||
value={formData.lastName}
|
||||
onChangeText={(text) => setFormData({ ...formData, lastName: text })}
|
||||
placeholder="Enter last name"
|
||||
placeholderTextColor={colors.grayText}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<Text style={styles.errorText}>{errors.lastName}</Text>
|
||||
<AppText variant="caption" style={styles.errorText}>{errors.lastName}</AppText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.inputLabel}>Email *</Text>
|
||||
<AppText variant="bodySmall" style={styles.inputLabel}>Email *</AppText>
|
||||
<TextInput
|
||||
style={[styles.input, errors.emailId && styles.inputError]}
|
||||
value={formData.emailId}
|
||||
onChangeText={(text) => setFormData({ ...formData, emailId: text })}
|
||||
placeholder="Enter email"
|
||||
placeholderTextColor={colors.grayText}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
{errors.emailId && (
|
||||
<Text style={styles.errorText}>{errors.emailId}</Text>
|
||||
<AppText variant="caption" style={styles.errorText}>{errors.emailId}</AppText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.inputLabel}>Mobile Number *</Text>
|
||||
<AppText variant="bodySmall" style={styles.inputLabel}>Mobile Number *</AppText>
|
||||
<TextInput
|
||||
style={[styles.input, errors.mobileNo && styles.inputError]}
|
||||
value={formData.mobileNo}
|
||||
onChangeText={(text) => setFormData({ ...formData, mobileNo: text })}
|
||||
placeholder="Enter mobile number"
|
||||
placeholderTextColor={colors.grayText}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
keyboardType="phone-pad"
|
||||
maxLength={10}
|
||||
/>
|
||||
{errors.mobileNo && (
|
||||
<Text style={styles.errorText}>{errors.mobileNo}</Text>
|
||||
<AppText variant="caption" style={styles.errorText}>{errors.mobileNo}</AppText>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
@@ -571,7 +576,7 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
|
||||
onPress={onClose}
|
||||
disabled={updateProfileMutation.isPending}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
<AppText variant="button" style={styles.cancelButtonText}>Cancel</AppText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
@@ -580,11 +585,11 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
|
||||
disabled={updateProfileMutation.isPending}
|
||||
>
|
||||
{updateProfileMutation.isPending ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
) : (
|
||||
<>
|
||||
<Save size={18} color={colors.white} />
|
||||
<Text style={styles.saveButtonText}>Save</Text>
|
||||
<Save size={18} color={colors.buttonText} />
|
||||
<AppText variant="button" style={styles.saveButtonText}>Save</AppText>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@@ -597,6 +602,9 @@ const EditProfileModal: React.FC<EditProfileModalProps> = ({
|
||||
|
||||
// Profile Header Component - OPTIMIZED
|
||||
const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayName }) => {
|
||||
const { colors } = useTheme();
|
||||
const styles = useMemo(() => createStyles(colors), [colors]);
|
||||
|
||||
const getInitials = (name: string): string => {
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
const fallbackName = `${data.firstName || ''} ${data.lastName || ''}`.trim();
|
||||
@@ -630,46 +638,44 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayN
|
||||
|
||||
return (
|
||||
<View>
|
||||
<LinearGradient
|
||||
colors={[colors.primary, colors.primaryLight]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
<View
|
||||
style={styles.headerGradient}
|
||||
>
|
||||
<View style={styles.avatarContainer}>
|
||||
<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 style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}>
|
||||
<View style={styles.statusDot} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.headerName}>
|
||||
<AppText variant="h2" style={styles.headerName}>
|
||||
{displayName || 'Unknown User'}
|
||||
</Text>
|
||||
</AppText>
|
||||
|
||||
<View style={styles.roleBadge}>
|
||||
<ShieldCheck size={16} color={colors.white} />
|
||||
<Text style={styles.roleText}>{data.role}</Text>
|
||||
<ShieldCheck size={16} color={colors.text} />
|
||||
<AppText variant="caption" style={styles.roleText}>{data.role}</AppText>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
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 user = useSelector((state: any) => state.token?.token);
|
||||
const { colors } = useTheme();
|
||||
|
||||
const stats: StatItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
Icon: CheckCircle,
|
||||
label: 'Aadhaar',
|
||||
value: user?.esetuVerified,
|
||||
value: user?.esetuVerified || 'Pending',
|
||||
color: user?.esetuVerified === 'Not Verified' ? colors.error : colors.success,
|
||||
onPress: onAadhaarPress,
|
||||
},
|
||||
@@ -677,169 +683,103 @@ const QuickStats: React.FC<QuickStatsProps> = React.memo(({ data, onAadhaarPress
|
||||
Icon: ShieldCheck,
|
||||
label: '2FA',
|
||||
value: data.twoFactorAuthEnabled ? 'Enabled' : 'Disabled',
|
||||
color: data.twoFactorAuthEnabled ? colors.success : colors.grayText,
|
||||
},
|
||||
{
|
||||
Icon: FileText,
|
||||
label: 'Registration',
|
||||
value: data.registrationStatus,
|
||||
color: colors.info,
|
||||
color: data.twoFactorAuthEnabled ? colors.success : colors.textSecondary,
|
||||
},
|
||||
// ...
|
||||
],
|
||||
[data.twoFactorAuthEnabled, data.registrationStatus, user?.esetuVerified, onAadhaarPress]
|
||||
[data.twoFactorAuthEnabled, user?.esetuVerified, onAadhaarPress, colors]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.statsContainer}>
|
||||
{stats.map((stat) => {
|
||||
const CardContent = (
|
||||
<View style={styles.statCard}>
|
||||
<stat.Icon size={28} color={stat.color} />
|
||||
<Text style={styles.statValue} numberOfLines={2}>
|
||||
{stat.value}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>{stat.label}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (stat.onPress) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={stat.label}
|
||||
activeOpacity={0.7}
|
||||
onPress={stat.onPress}
|
||||
style={{ flex: 1 }}
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 24 }}>
|
||||
{stats.map((stat, index) => {
|
||||
const content = (
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(600).delay(index * 100)}
|
||||
style={{
|
||||
flex: 1, backgroundColor: colors.glass, borderColor: colors.overlay, borderWidth: 1,
|
||||
borderRadius: 20, padding: 12, alignItems: 'center', marginHorizontal: 4,
|
||||
minHeight: 100, justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{CardContent}
|
||||
</TouchableOpacity>
|
||||
<View style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: stat.color + '10', alignItems: 'center', justifyContent: 'center', marginBottom: 8 }}>
|
||||
<stat.Icon size={20} color={stat.color} />
|
||||
</View>
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
||||
QuickStats.displayName = 'QuickStats';
|
||||
|
||||
// Info Section Component - OPTIMIZED
|
||||
// Info Section Component - Glass Card Container
|
||||
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 (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={styles.sectionHeaderLeft}>
|
||||
<View style={styles.sectionIconContainer}>
|
||||
<IconComponent size={24} color={colors.primary} />
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(600).delay(delay)}
|
||||
style={{
|
||||
backgroundColor: colors.glass,
|
||||
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>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
<AppText variant="h4" color={colors.text}>{title}</AppText>
|
||||
</View>
|
||||
{showEdit && onEdit && (
|
||||
<TouchableOpacity style={styles.editButton} onPress={onEdit}>
|
||||
<Edit size={20} color={colors.primary} />
|
||||
<TouchableOpacity onPress={onEdit} style={{ padding: 4 }}>
|
||||
<Edit size={18} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.sectionContent}>{children}</View>
|
||||
</View>
|
||||
<View>{children}</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InfoSection.displayName = 'InfoSection';
|
||||
|
||||
// Info Row Component - OPTIMIZED
|
||||
// Info Row Component
|
||||
const InfoRow: React.FC<InfoRowProps> = React.memo(
|
||||
({ label, value, IconComponent, valueColor }) => {
|
||||
if (!isValidValue(value)) {
|
||||
return null;
|
||||
}
|
||||
const { colors } = useTheme();
|
||||
|
||||
if (!isValidValue(value)) return null;
|
||||
|
||||
return (
|
||||
<View style={styles.infoRow}>
|
||||
<View style={styles.infoRowTouchable}>
|
||||
<View style={styles.infoRowContent}>
|
||||
<View style={styles.infoIconContainer}>
|
||||
<IconComponent size={18} color={colors.primary} />
|
||||
</View>
|
||||
<View style={styles.infoTextContainer}>
|
||||
<Text style={styles.infoLabel}>{label}</Text>
|
||||
<Text
|
||||
style={[styles.infoValue, valueColor && { color: valueColor }]}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', marginBottom: 16, alignItems: 'flex-start' }}>
|
||||
<IconComponent size={16} color={colors.textSecondary} style={{ marginTop: 4, marginRight: 12 }} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 2 }}>{label}</AppText>
|
||||
<AppText variant='bodySmall' color={valueColor || colors.text}>{value}</AppText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InfoRow.displayName = 'InfoRow';
|
||||
|
||||
// Styles
|
||||
interface Styles {
|
||||
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>({
|
||||
const createStyles = (colors: any) => StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.lightGray,
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
contentContainer: {
|
||||
paddingBottom: 20,
|
||||
@@ -848,16 +788,16 @@ const styles = StyleSheet.create<Styles>({
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.background,
|
||||
padding: 20,
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
color: colors.grayText,
|
||||
color: colors.textSecondary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
errorText: {
|
||||
screenErrorText: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
color: colors.error,
|
||||
@@ -886,13 +826,6 @@ const styles = StyleSheet.create<Styles>({
|
||||
paddingTop: Platform.OS === 'ios' ? 60 : 40,
|
||||
paddingBottom: 30,
|
||||
alignItems: 'center',
|
||||
borderBottomLeftRadius: 32,
|
||||
borderBottomRightRadius: 32,
|
||||
elevation: 8,
|
||||
shadowColor: colors.shadow,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
},
|
||||
avatarContainer: {
|
||||
position: 'relative',
|
||||
@@ -902,11 +835,11 @@ const styles = StyleSheet.create<Styles>({
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.card,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 4,
|
||||
borderColor: colors.white,
|
||||
borderColor: colors.card,
|
||||
elevation: 4,
|
||||
shadowColor: colors.shadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
@@ -926,7 +859,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
borderWidth: 3,
|
||||
borderColor: colors.white,
|
||||
borderColor: colors.card, // Updated
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
@@ -939,7 +872,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
headerName: {
|
||||
fontSize: 26,
|
||||
fontWeight: '700',
|
||||
color: colors.white,
|
||||
color: colors.text, // Updated
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 20,
|
||||
@@ -947,14 +880,14 @@ const styles = StyleSheet.create<Styles>({
|
||||
roleBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||
backgroundColor: colors.primary + '20', // Updated opacity approach
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
gap: 6,
|
||||
},
|
||||
roleText: {
|
||||
color: colors.white,
|
||||
color: colors.primary, // Updated
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
@@ -968,7 +901,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
},
|
||||
statCard: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
@@ -981,20 +914,20 @@ const styles = StyleSheet.create<Styles>({
|
||||
statValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: colors.darkText,
|
||||
color: colors.text,
|
||||
marginTop: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
color: colors.grayText,
|
||||
color: colors.textSecondary,
|
||||
marginTop: 4,
|
||||
},
|
||||
|
||||
section: {
|
||||
marginHorizontal: 16,
|
||||
marginTop: 24,
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
elevation: 2,
|
||||
@@ -1021,7 +954,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.primaryHighlight,
|
||||
backgroundColor: colors.primary + '10', // Updated
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
@@ -1029,7 +962,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: colors.darkText,
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
},
|
||||
sectionContent: {
|
||||
@@ -1038,7 +971,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
editButton: {
|
||||
padding: 8,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.primaryHighlight,
|
||||
backgroundColor: colors.primary + '10', // Updated
|
||||
},
|
||||
|
||||
infoRow: {
|
||||
@@ -1047,7 +980,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
},
|
||||
infoRowTouchable: {
|
||||
padding: 14,
|
||||
backgroundColor: colors.lightestGray,
|
||||
backgroundColor: colors.background, // Updated
|
||||
borderRadius: 12,
|
||||
},
|
||||
infoRowContent: {
|
||||
@@ -1058,7 +991,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.card, // Updated
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
@@ -1070,13 +1003,13 @@ const styles = StyleSheet.create<Styles>({
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 13,
|
||||
color: colors.grayText,
|
||||
color: colors.textSecondary,
|
||||
fontWeight: '500',
|
||||
lineHeight: 18,
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 15,
|
||||
color: colors.darkText,
|
||||
color: colors.text,
|
||||
fontWeight: '600',
|
||||
lineHeight: 21,
|
||||
},
|
||||
@@ -1087,11 +1020,11 @@ const styles = StyleSheet.create<Styles>({
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.white,
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
maxHeight: '90%',
|
||||
paddingTop: 20,
|
||||
borderRadius: 30
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
@@ -1105,7 +1038,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
modalTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: colors.darkText,
|
||||
color: colors.text,
|
||||
},
|
||||
closeButton: {
|
||||
padding: 4,
|
||||
@@ -1120,7 +1053,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
inputLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: colors.darkText,
|
||||
color: colors.text,
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
@@ -1129,8 +1062,8 @@ const styles = StyleSheet.create<Styles>({
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
fontSize: 15,
|
||||
color: colors.darkText,
|
||||
backgroundColor: colors.white,
|
||||
color: colors.text,
|
||||
backgroundColor: colors.card, // Updated
|
||||
},
|
||||
inputError: {
|
||||
borderColor: colors.error,
|
||||
@@ -1157,7 +1090,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
gap: 8,
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: colors.lightestGray,
|
||||
backgroundColor: colors.background, // Updated
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: colors.primary,
|
||||
@@ -1165,7 +1098,7 @@ const styles = StyleSheet.create<Styles>({
|
||||
cancelButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.darkText,
|
||||
color: colors.text,
|
||||
},
|
||||
saveButtonText: {
|
||||
fontSize: 16,
|
||||
|
||||
@@ -3,23 +3,22 @@ import {
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import React, { use, useState } from 'react';
|
||||
import { useNavigation, useRoute, NavigationProp, RouteProp } from '@react-navigation/native';
|
||||
import colors from '../../Constant/Color';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
|
||||
import MyInput from '../../Component/MyInput';
|
||||
import MyInput from '../../components/MyInput';
|
||||
import { showToast } from '../../Constant/ShowDailog';
|
||||
import { PARTNER_API_ENDPOINT } from '../../Services/ApiCall/ApiUrls';
|
||||
import { usePartnerShipSubmit } from '../../Services/ApiCall/Hooks';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Navigation } from 'lucide-react-native';
|
||||
import { NavigationString } from '../../Constant/AllImports';
|
||||
|
||||
import Animated, { FadeInUp } from 'react-native-reanimated';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
import { Trash2, UserPlus } from 'lucide-react-native';
|
||||
|
||||
type Partner = {
|
||||
partnerName: string;
|
||||
@@ -32,11 +31,12 @@ type Navigation = {
|
||||
replace: (route: string) => void;
|
||||
};
|
||||
type PartnershipParams = {
|
||||
otherId: string | number
|
||||
otherId: string | number;
|
||||
};
|
||||
|
||||
const Partnership = () => {
|
||||
const route = useRoute<RouteProp<{ params: PartnershipParams }, 'params'>>();
|
||||
|
||||
const { colors, spacing, typography, shadows } = useTheme();
|
||||
const navigation = useNavigation() as Navigation;
|
||||
const [partners, setPartners] = useState<Partner[]>([
|
||||
{
|
||||
@@ -49,9 +49,11 @@ const Partnership = () => {
|
||||
]);
|
||||
|
||||
const [focusedField, setFocusedField] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { mutateAsync: partnerShipSubmit } = usePartnerShipSubmit();
|
||||
const insets = useSafeAreaInsets();
|
||||
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
|
||||
|
||||
const handleChange = (index: number, name: keyof Partner, value: string) => {
|
||||
const updated = [...partners];
|
||||
updated[index][name] = value;
|
||||
@@ -90,9 +92,8 @@ const Partnership = () => {
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
try {
|
||||
// Convert percentage strings to numbers and validate
|
||||
const payload = partners.map((p, i) => {
|
||||
const percentage = Number(p.percentage);
|
||||
|
||||
@@ -105,13 +106,12 @@ const Partnership = () => {
|
||||
ptnrAadharNo: p.aadharNumber,
|
||||
ptnrMobNo: p.mobileNumber,
|
||||
ptnrName: p.partnerName,
|
||||
ptnrPop: percentage, // Ensure this is a number
|
||||
ptnrPop: percentage,
|
||||
ptnrPANNo: p.partnerPan,
|
||||
ptnrNo: i,
|
||||
};
|
||||
});
|
||||
|
||||
// Validate total percentage equals 100%
|
||||
const totalPercentage = payload.reduce((sum, p) => sum + p.ptnrPop, 0);
|
||||
if (totalPercentage !== 100) {
|
||||
showToast(`Total percentage must equal 100% (currently ${totalPercentage}%)`);
|
||||
@@ -123,153 +123,179 @@ const Partnership = () => {
|
||||
if (response?.statusCode === 200) {
|
||||
showToast(response.message || 'Submitted successfully!');
|
||||
navigation.replace(NavigationString.Home);
|
||||
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ Submission failed:', error.message);
|
||||
showToast(error.message || 'Submission failed. Please check partner details.');
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.heading, { paddingTop: insets.top }]}>Partnership Details</Text>
|
||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<View style={[styles.header, { paddingTop: insets.top + spacing.s }]}>
|
||||
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
|
||||
Partnership Details
|
||||
</AppText>
|
||||
<AppText variant="caption" color={colors.textSecondary}>
|
||||
Add all partners and their share percentages
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={40}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{partners.map((partner, index) => (
|
||||
<View style={{ marginBottom: 20 }} key={index}>
|
||||
<Text style={styles.partnerTitle}>Partner {index + 1}</Text>
|
||||
<Animated.View
|
||||
key={index}
|
||||
entering={FadeInUp.delay(index * 80).springify()}
|
||||
style={[styles.card, shadows.medium]}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
|
||||
Partner {index + 1}
|
||||
</AppText>
|
||||
{partners.length > 1 && (
|
||||
<TouchableOpacity onPress={() => handleRemove(index)} activeOpacity={0.7}>
|
||||
<Trash2 size={18} color={colors.error} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<MyInput
|
||||
icon="user"
|
||||
placeholder="Name of Partner *"
|
||||
label="Partner Name"
|
||||
placeholder="Enter Partner Name"
|
||||
value={partner.partnerName}
|
||||
onChangeText={text => handleChange(index, 'partnerName', text)}
|
||||
onFocus={() => setFocusedField(`partnerName-${index}`)}
|
||||
onBlur={() => setFocusedField('')}
|
||||
focused={focusedField === `partnerName-${index}`}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="percent"
|
||||
placeholder="Partnership Percentage *"
|
||||
label="Share Percentage"
|
||||
placeholder="Enter Percentage"
|
||||
value={partner.percentage}
|
||||
onChangeText={text => handleChange(index, 'percentage', text)}
|
||||
onFocus={() => setFocusedField(`percentage-${index}`)}
|
||||
onBlur={() => setFocusedField('')}
|
||||
focused={focusedField === `percentage-${index}`}
|
||||
keyboardType="numeric"
|
||||
maxlength={3}
|
||||
maxLength={3}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="id-card"
|
||||
placeholder="PAN of Partner *"
|
||||
label="PAN of Partner"
|
||||
placeholder="Enter PAN Number"
|
||||
value={partner.partnerPan}
|
||||
onChangeText={text => handleChange(index, 'partnerPan', text)}
|
||||
onFocus={() => setFocusedField(`partnerPan-${index}`)}
|
||||
onBlur={() => setFocusedField('')}
|
||||
focused={focusedField === `partnerPan-${index}`}
|
||||
autoCapitalize="characters"
|
||||
maxlength={10}
|
||||
|
||||
maxLength={10}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="credit-card"
|
||||
placeholder="Aadhar Number *"
|
||||
label="Aadhaar Number"
|
||||
placeholder="Enter 12-digit Aadhaar"
|
||||
value={partner.aadharNumber}
|
||||
onChangeText={text => handleChange(index, 'aadharNumber', text)}
|
||||
onFocus={() => setFocusedField(`aadharNumber-${index}`)}
|
||||
onBlur={() => setFocusedField('')}
|
||||
focused={focusedField === `aadharNumber-${index}`}
|
||||
keyboardType="numeric"
|
||||
maxlength={12}
|
||||
maxLength={12}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="phone"
|
||||
placeholder="Mobile Number *"
|
||||
label="Mobile Number"
|
||||
placeholder="Enter Mobile Number"
|
||||
value={partner.mobileNumber}
|
||||
onChangeText={text => handleChange(index, 'mobileNumber', text)}
|
||||
onFocus={() => setFocusedField(`mobileNumber-${index}`)}
|
||||
onBlur={() => setFocusedField('')}
|
||||
focused={focusedField === `mobileNumber-${index}`}
|
||||
keyboardType="phone-pad"
|
||||
maxlength={10}
|
||||
maxLength={10}
|
||||
/>
|
||||
|
||||
{partners.length > 1 && (
|
||||
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
|
||||
<Text style={styles.deleteText}>Delete Partner</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
))}
|
||||
|
||||
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}>
|
||||
<Text style={styles.addText}>+ Add Partner</Text>
|
||||
<TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
|
||||
<UserPlus size={16} color={colors.buttonText} />
|
||||
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
|
||||
Add Partner
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
|
||||
<PrimaryButton title={loading ? "Loading..." : "Submit" }onPress={handleSubmit} />
|
||||
<View style={styles.footer}>
|
||||
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Partnership;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
heading: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 10,
|
||||
marginTop: 10,
|
||||
textAlign: 'center',
|
||||
|
||||
},
|
||||
partnerTitle: {
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
marginBottom: 10,
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
|
||||
header: {
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingBottom: spacing.m,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
scrollContainer: {
|
||||
// backgroundColor: color.background,
|
||||
flexGrow: 1,
|
||||
padding: spacing.m,
|
||||
paddingBottom: 30,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.s,
|
||||
paddingBottom: spacing.xs,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
addButton: {
|
||||
backgroundColor: colors.primary,
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
borderRadius: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
width: 140,
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 12,
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
addText: {
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: '#FF5C5C',
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
width: 140,
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
deleteText: {
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
footer: {
|
||||
padding: spacing.m,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, SafeAreaView, ScrollView, BackHandler, ToastAndroid, Button, Alert } from 'react-native';
|
||||
// import AntDesign from 'react-native-vector-icons/AntDesign';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, ScrollView, BackHandler, ToastAndroid, Alert } from 'react-native';
|
||||
import { ArrowLeft } from 'lucide-react-native';
|
||||
import { NavigationProp, RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
// import { sendAadhaarOtp, verifyAadhaarOtp, verifyGstNumber, verifyPanHandlerRes } from '../../utils/helper/ragisterationApiCalls';
|
||||
|
||||
// import { getAllMsMeOrg, getCityDropdown, getClassDropdown, getMsmeDropdown, getOrgDropdown, getSocialDropdown, getStateDropdown } from '../../utils/helper/dropdownApis';
|
||||
import { ApiResponse, DocumentsType, FormDataType, OrgType, UserData } from './types/types';
|
||||
import { pick } from '@react-native-documents/picker';
|
||||
// import { REJ_STAGE_TWO } from '../../utils/services/apiUrl';
|
||||
// import LoaderModel from '../../component/LoaderModal';
|
||||
|
||||
import axios from 'axios';
|
||||
import StageOne from './ragistrationStages/StageOne';
|
||||
import StageTwo from './ragistrationStages/StageTwo';
|
||||
import StageZero from './ragistrationStages/StageZero';
|
||||
import StageThree from './ragistrationStages/StageThree';
|
||||
import StageFour from './ragistrationStages/StageFour';
|
||||
import colors from '../../Constant/Color';
|
||||
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
|
||||
import StepProgressBar from '../../Component/StepProgressBar';
|
||||
import { formatDateInput } from '../../Helper/formatDateInput';
|
||||
@@ -26,6 +18,9 @@ import { showToast } from '../../Constant/ShowDailog';
|
||||
import { useCityDropdown, useContractorStageTwoSubmit, usefetchMasterData, useSendAadhaarOtp, useVerifyAadhaarOtp, useVerifyGstNumber, useVerifyPan } from '../../Services/ApiCall/Hooks';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
|
||||
type RouteParams = {
|
||||
userData?: {
|
||||
@@ -38,6 +33,7 @@ type RouteParams = {
|
||||
};
|
||||
|
||||
const Registration: React.FC = () => {
|
||||
const { colors, spacing, typography, shadows } = useTheme();
|
||||
const navigation = useNavigation<any>();
|
||||
const { params } = useRoute<RouteProp<Record<string, RouteParams>, string>>();
|
||||
const userData = params as RouteParams;
|
||||
@@ -102,6 +98,9 @@ const Registration: React.FC = () => {
|
||||
const { mutateAsync: verifyGstNumber } = useVerifyGstNumber();
|
||||
const { mutateAsync: contractorStageTwoSubmit } = useContractorStageTwoSubmit();
|
||||
const { data: masterData } = usefetchMasterData();
|
||||
|
||||
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!masterData?.data) return;
|
||||
const contractorClasses = masterData.data.contractorClasses || [];
|
||||
@@ -138,22 +137,13 @@ useEffect(() => {
|
||||
|
||||
|
||||
const { mutate: cityDropdownMutate } = useCityDropdown();
|
||||
const { data: MasterData}=usefetchMasterData()
|
||||
const { data: MasterData } = usefetchMasterData();
|
||||
const user = useSelector((state: any) => state.token.token);
|
||||
const steps = ['', '', '', '', ''];
|
||||
const dropdownStyle = {
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
height: 50,
|
||||
backgroundColor: '#fff',
|
||||
marginBottom: 10,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserData = async (user: any) => {
|
||||
try {
|
||||
|
||||
if (user) {
|
||||
const parsedUser = user;
|
||||
setFormData(prev => ({
|
||||
@@ -180,25 +170,11 @@ useEffect(() => {
|
||||
gstin = gstin.toUpperCase();
|
||||
|
||||
const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
|
||||
const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;
|
||||
|
||||
if (!panRegex.test(pan)) {
|
||||
return { valid: false, message: "Invalid PAN format. It should be like 'ABCDE1234F'." };
|
||||
}
|
||||
|
||||
// if (!gstinRegex.test(gstin)) {
|
||||
// return { valid: false, message: "Invalid GSTIN format. It should be like '22ABCDE1234F1Z5'." };
|
||||
// }
|
||||
|
||||
// const gstinPAN = gstin.slice(2, 12); // Extract PAN part from GSTIN
|
||||
|
||||
// if (pan !== gstinPAN) {
|
||||
// return {
|
||||
// valid: false,
|
||||
// message: "PAN does not match the PAN embedded in GSTIN.",
|
||||
// };
|
||||
// }
|
||||
|
||||
return { valid: true, message: 'PAN and GSTIN are correctly linked.' };
|
||||
};
|
||||
|
||||
@@ -207,7 +183,7 @@ useEffect(() => {
|
||||
};
|
||||
|
||||
const verifyPanHandler = async () => {
|
||||
setPanVerifying(true); // Start loading
|
||||
setPanVerifying(true);
|
||||
const payload = {
|
||||
dob: formData?.dateOfIncorp.trim(),
|
||||
name: formData?.orgPanName.trim(),
|
||||
@@ -220,7 +196,7 @@ useEffect(() => {
|
||||
} else {
|
||||
setPanVerified(false);
|
||||
}
|
||||
setPanVerifying(false); // Stop loading
|
||||
setPanVerifying(false);
|
||||
};
|
||||
|
||||
const sendAdharOTPHandler = async (text: string) => {
|
||||
@@ -333,11 +309,10 @@ useEffect(() => {
|
||||
fileData.append('pancardfile', documents.panCard as any);
|
||||
fileData.append('regfile', documents.registrationCertificate as any);
|
||||
|
||||
|
||||
const res = await contractorStageTwoSubmit(fileData);
|
||||
|
||||
if (res.status === 200) {
|
||||
if (res.statusCode === 200) {
|
||||
if (res) {
|
||||
|
||||
showToast('Form submitted successfully!');
|
||||
|
||||
const orgType = selectedOrg;
|
||||
@@ -357,7 +332,6 @@ useEffect(() => {
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
|
||||
} else {
|
||||
console.error('Unexpected error', error);
|
||||
}
|
||||
@@ -369,7 +343,6 @@ useEffect(() => {
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchCity = async (stateId: number) => {
|
||||
cityDropdownMutate(stateId, {
|
||||
onSuccess: (data) => {
|
||||
@@ -380,13 +353,10 @@ useEffect(() => {
|
||||
console.error('Error in city fetch:', error);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
console.log('formdata : ', formData);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (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 (
|
||||
<View style={{ flex: 1, backgroundColor: colors.white }}>
|
||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
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 }}>
|
||||
{/* ═══ Back Button ═══ */}
|
||||
<View style={styles.headerRow}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (stage > 0) {
|
||||
@@ -518,12 +504,39 @@ useEffect(() => {
|
||||
}
|
||||
}}
|
||||
style={styles.backBtn}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{/* Replaced AntDesign with Lucide ArrowLeft */}
|
||||
<ArrowLeft size={25} color={colors.primary} />
|
||||
<ArrowLeft size={22} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
|
||||
Registration
|
||||
</AppText>
|
||||
<View style={{ width: 36 }} />
|
||||
</View>
|
||||
|
||||
{/* ═══ Progress Bar ═══ */}
|
||||
<StepProgressBar currentStep={stage} steps={steps} />
|
||||
|
||||
{/* ═══ Stage Title ═══
|
||||
<Animated.View
|
||||
key={`title-${stage}`}
|
||||
entering={FadeInUp.duration(300)}
|
||||
style={styles.stageTitleContainer}
|
||||
>
|
||||
<AppText variant="h3" color={colors.text} style={styles.stageTitle}>
|
||||
{stageTitles[stage]}
|
||||
</AppText>
|
||||
<AppText variant="caption" color={colors.textSecondary}>
|
||||
Step {stage + 1} of {steps.length}
|
||||
</AppText>
|
||||
</Animated.View> */}
|
||||
|
||||
{/* ═══ Stage Content ═══ */}
|
||||
<Animated.View
|
||||
key={`stage-${stage}`}
|
||||
entering={FadeInUp.duration(350).delay(100)}
|
||||
style={[styles.stageCard, shadows.medium]}
|
||||
>
|
||||
{stage === 0 && (
|
||||
<StageZero
|
||||
formData={formData}
|
||||
@@ -549,7 +562,7 @@ useEffect(() => {
|
||||
allMsmeOrgs={allMsmeOrgs}
|
||||
allSocialCategories={allSocialCategories}
|
||||
allMSME={allMSME}
|
||||
dropdownStyle={dropdownStyle}
|
||||
dropdownStyle={{}}
|
||||
setSelectedOrg={setSelectedOrg}
|
||||
selectedOrg={selectedOrg}
|
||||
/>
|
||||
@@ -594,8 +607,9 @@ useEffect(() => {
|
||||
states={states}
|
||||
cities={cities}
|
||||
fetchCity={() => {
|
||||
fetchCity(Number(formData.state))}}
|
||||
dropdownStyle={dropdownStyle}
|
||||
fetchCity(Number(formData.state));
|
||||
}}
|
||||
dropdownStyle={{}}
|
||||
styles={styles}
|
||||
/>
|
||||
)}
|
||||
@@ -609,10 +623,14 @@ useEffect(() => {
|
||||
selectedOrg={selectedOrg}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* ═══ Next / Submit Button ═══ */}
|
||||
<Animated.View entering={FadeInDown.duration(300).delay(200)}>
|
||||
<PrimaryButton
|
||||
title={stage === 4 ? (loading ? 'Loading...' : 'Submit') : 'Next'}
|
||||
style={{ backgroundColor: colors.primary }}
|
||||
onPress={() => {
|
||||
if (validateStage(stage)) {
|
||||
switch (stage) {
|
||||
@@ -657,6 +675,7 @@ useEffect(() => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
@@ -665,96 +684,96 @@ useEffect(() => {
|
||||
|
||||
export default Registration;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// ── Styles ────────────────────────────────────────────────────────────
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal:14
|
||||
paddingHorizontal: spacing.m,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: spacing.s,
|
||||
},
|
||||
backBtn: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.surface,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
stageTitleContainer: {
|
||||
marginBottom: spacing.m,
|
||||
paddingHorizontal: spacing.xs,
|
||||
},
|
||||
stageTitle: {
|
||||
fontWeight: '800',
|
||||
marginBottom: 2,
|
||||
},
|
||||
stageCard: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 16,
|
||||
padding: spacing.m,
|
||||
},
|
||||
// These styles will be passed to child stages that still use `styles` prop
|
||||
label: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
marginVertical: 10,
|
||||
color: colors.black,
|
||||
fontSize: typography.size.s,
|
||||
fontWeight: '700',
|
||||
color: colors.primary,
|
||||
marginBottom: spacing.s,
|
||||
letterSpacing: 0.3,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 10,
|
||||
},
|
||||
backBtn: {
|
||||
padding: 5,
|
||||
width: 50,
|
||||
marginBottom: 5,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 24,
|
||||
fontSize: typography.size.l,
|
||||
fontWeight: '700',
|
||||
color: colors.black,
|
||||
marginBottom: 20,
|
||||
marginTop: 10,
|
||||
color: colors.text,
|
||||
marginBottom: spacing.m,
|
||||
textAlign: 'center',
|
||||
},
|
||||
uploadGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
gap: spacing.s,
|
||||
},
|
||||
uploadCard: {
|
||||
width: '48%',
|
||||
backgroundColor: '#fdfdfd',
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 16,
|
||||
padding: spacing.m,
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 6,
|
||||
elevation: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderColor: colors.border,
|
||||
},
|
||||
iconCircle: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: 23,
|
||||
backgroundColor: colors.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 10,
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 15,
|
||||
fontSize: typography.size.s,
|
||||
fontWeight: '600',
|
||||
color: colors.black,
|
||||
color: colors.text,
|
||||
marginBottom: 4,
|
||||
textAlign: 'center',
|
||||
},
|
||||
cardHint: {
|
||||
fontSize: 12,
|
||||
color: '#888',
|
||||
fontSize: typography.size.xs,
|
||||
color: colors.textSecondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
dropdown: {
|
||||
height: 55,
|
||||
borderColor: '#e0e0e0',
|
||||
borderWidth: 1,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
marginTop: 8,
|
||||
marginBottom: 14,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
selectedTextStyle: {
|
||||
fontSize: 14,
|
||||
color: colors.black,
|
||||
},
|
||||
placeholderStyle: {
|
||||
fontSize: 14,
|
||||
color: colors.black,
|
||||
},
|
||||
itemTextStyle: {
|
||||
fontSize: 14,
|
||||
color: colors.black,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,24 +3,21 @@ import {
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
|
||||
|
||||
|
||||
import colors from '../../Constant/Color';
|
||||
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
|
||||
import MyInput from '../../Component/MyInput';
|
||||
import MyInput from '../../components/MyInput';
|
||||
import { showToast } from '../../Constant/ShowDailog';
|
||||
import { STAGETHREE } from '../../Services/ApiCall/ApiUrls';
|
||||
import { useContractorStageThreeSubmit } from '../../Services/ApiCall/Hooks';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import Animated, { FadeInUp } from 'react-native-reanimated';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
import { Trash2, Building2 } from 'lucide-react-native';
|
||||
|
||||
type Company = {
|
||||
director: string;
|
||||
@@ -32,13 +29,16 @@ type Company = {
|
||||
|
||||
type StageThreeRouteParams = {
|
||||
orgType: string;
|
||||
otherId: string | number
|
||||
otherId: string | number;
|
||||
};
|
||||
|
||||
const StageThreeScreen = () => {
|
||||
const route = useRoute<RouteProp<{ params: StageThreeRouteParams }, 'params'>>();
|
||||
const navigation = useNavigation<any>();
|
||||
const 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[]>([
|
||||
{
|
||||
director: '',
|
||||
@@ -72,7 +72,6 @@ const StageThreeScreen = () => {
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
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 (
|
||||
<View style={{ flex: 1 ,paddingHorizontal:10 }}>
|
||||
<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={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<View style={[styles.header, { paddingTop: insets.top + spacing.s }]}>
|
||||
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
|
||||
{orgLabel} Organization
|
||||
</AppText>
|
||||
<AppText variant="caption" color={colors.textSecondary}>
|
||||
Add directors / shareholders for your organization
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={40}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{company.map((partner, index) => (
|
||||
<View style={{ marginBottom: 20 }} key={index}>
|
||||
<Text style={styles.partnerTitle}>Company {index + 1}</Text>
|
||||
<Animated.View
|
||||
key={index}
|
||||
entering={FadeInUp.delay(index * 80).springify()}
|
||||
style={[styles.card, shadows.medium]}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
|
||||
Director {index + 1}
|
||||
</AppText>
|
||||
{company.length > 1 && (
|
||||
<TouchableOpacity onPress={() => handleRemove(index)} activeOpacity={0.7}>
|
||||
<Trash2 size={18} color={colors.error} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<MyInput
|
||||
icon="user"
|
||||
placeholder="Name of Director *"
|
||||
label="Director Name"
|
||||
placeholder="Enter Name of Director"
|
||||
value={partner.director}
|
||||
onChangeText={(text: any) => handleChange(index, 'director', text)}
|
||||
onFocus={() => setFocusedField(`director-${index}`)}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === `director-${index}`}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="id-card"
|
||||
placeholder="DIN Number in Firm *"
|
||||
label="DIN Number"
|
||||
placeholder="Enter DIN Number"
|
||||
value={partner.DINNumber}
|
||||
onChangeText={(text: any) => handleChange(index, 'DINNumber', text)}
|
||||
onFocus={() => setFocusedField(`DINNumber-${index}`)}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === `DINNumber-${index}`}
|
||||
keyboardType="numeric"
|
||||
maxlength={8}
|
||||
maxLength={8}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="id-card"
|
||||
placeholder="PAN of Director *"
|
||||
label="PAN of Director"
|
||||
placeholder="Enter PAN Number"
|
||||
value={partner.panNumber}
|
||||
onChangeText={(text: any) => handleChange(index, 'panNumber', text)}
|
||||
onFocus={() => setFocusedField(`panNumber-${index}`)}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === `panNumber-${index}`}
|
||||
maxlength={12}
|
||||
maxLength={12}
|
||||
autoCapitalize="characters"
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="credit-card"
|
||||
placeholder="Aadhar Number *"
|
||||
label="Aadhaar Number"
|
||||
placeholder="Enter 12-digit Aadhaar"
|
||||
value={partner.aadharNumber}
|
||||
onChangeText={(text: any) => handleChange(index, 'aadharNumber', text)}
|
||||
onFocus={() => setFocusedField(`aadharNumber-${index}`)}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === `aadharNumber-${index}`}
|
||||
keyboardType="numeric"
|
||||
maxlength={12}
|
||||
maxLength={12}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="phone"
|
||||
placeholder="Mobile Number *"
|
||||
label="Mobile Number"
|
||||
placeholder="Enter Mobile Number"
|
||||
value={partner.mobileNumber}
|
||||
onChangeText={(text: any) => handleChange(index, 'mobileNumber', text)}
|
||||
onFocus={() => setFocusedField(`mobileNumber-${index}`)}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === `mobileNumber-${index}`}
|
||||
keyboardType="phone-pad"
|
||||
maxlength={10}
|
||||
maxLength={10}
|
||||
/>
|
||||
|
||||
{company.length > 1 && (
|
||||
<TouchableOpacity style={styles.deleteButton} onPress={() => handleRemove(index)}>
|
||||
<Text style={styles.deleteText}>Delete Company</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
))}
|
||||
|
||||
<TouchableOpacity style={styles.addButton} onPress={handleAddMore}>
|
||||
<Text style={styles.addText}>+ Add Company</Text>
|
||||
<TouchableOpacity style={styles.addButton} onPress={handleAddMore} activeOpacity={0.8}>
|
||||
<Building2 size={16} color={colors.buttonText} />
|
||||
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700', marginLeft: 8 }}>
|
||||
Add Director
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default StageThreeScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
heading: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 10,
|
||||
marginTop: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
partnerTitle: {
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
marginBottom: 10,
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
|
||||
header: {
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingBottom: spacing.m,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
scrollContainer: {
|
||||
|
||||
flexGrow: 1,
|
||||
padding: spacing.m,
|
||||
paddingBottom: 30,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.s,
|
||||
paddingBottom: spacing.xs,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
addButton: {
|
||||
backgroundColor: colors.primary,
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
borderRadius: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
width: 140,
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 12,
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
addText: {
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: '#FF5C5C',
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
width: 140,
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
deleteText: {
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
footer: {
|
||||
padding: spacing.m,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { View, TouchableOpacity } from 'react-native';
|
||||
import {
|
||||
IdCard,
|
||||
FileText,
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
ShieldCheck,
|
||||
Building2,
|
||||
ScrollText,
|
||||
} from 'lucide-react-native'; // lucide icons
|
||||
CheckCircle,
|
||||
Upload,
|
||||
} from 'lucide-react-native';
|
||||
import Animated, { FadeInUp } from 'react-native-reanimated';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
|
||||
interface DocumentItem {
|
||||
key: string;
|
||||
@@ -30,13 +35,15 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
|
||||
formData,
|
||||
selectedOrg,
|
||||
}) => {
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
|
||||
const documentItems: DocumentItem[] = [
|
||||
{ key: 'panCard', label: 'PAN Card', icon: 'pan' },
|
||||
{ key: 'aadharCard', label: 'Aadhar Card', icon: 'aadhar' },
|
||||
{ key: 'gstnCertificate', label: 'GSTN Certificate', icon: 'gst' },
|
||||
{ key: 'constractorClass', label: 'Contractor Class File', icon: 'contractor' },
|
||||
{ key: 'constractorClass', label: 'Contractor Class', icon: 'contractor' },
|
||||
{ key: 'msmefile', label: 'MSME Organization', icon: 'msme' },
|
||||
{ key: 'registrationCertificate', label: 'Registration Certificate', icon: 'registration' },
|
||||
{ key: 'registrationCertificate', label: 'Registration Cert.', icon: 'registration' },
|
||||
];
|
||||
|
||||
const shouldRenderItem = (itemKey: string): boolean => {
|
||||
@@ -52,54 +59,84 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // pan + aadhar + rest (unless filtered above)
|
||||
return true;
|
||||
};
|
||||
|
||||
const renderIcon = (iconType: DocumentItem['icon']) => {
|
||||
const commonProps = { size: 26, color: '#fff', strokeWidth: 2 };
|
||||
const commonProps = { size: 24, color: colors.buttonText, strokeWidth: 2 };
|
||||
|
||||
switch (iconType) {
|
||||
case 'pan':
|
||||
return <IdCard {...commonProps} />; // PAN → ID card feel [web:19]
|
||||
return <IdCard {...commonProps} />;
|
||||
case 'aadhar':
|
||||
return <IdCard {...commonProps} />; // Same icon, different label
|
||||
return <IdCard {...commonProps} />;
|
||||
case 'gst':
|
||||
return <FileBadge {...commonProps} />; // File with badge [web:20]
|
||||
return <FileBadge {...commonProps} />;
|
||||
case 'contractor':
|
||||
return <ShieldCheck {...commonProps} />; // Safety / verification feel
|
||||
return <ShieldCheck {...commonProps} />;
|
||||
case 'msme':
|
||||
return <Building2 {...commonProps} />; // Organization / company
|
||||
return <Building2 {...commonProps} />;
|
||||
case 'registration':
|
||||
return <ScrollText {...commonProps} />; // Certificate / document scroll
|
||||
return <ScrollText {...commonProps} />;
|
||||
default:
|
||||
return <FileText {...commonProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
let animIndex = 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.sectionTitle}>Upload Required Documents</Text>
|
||||
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700', textAlign: 'center', marginBottom: spacing.m }}>
|
||||
Upload Documents
|
||||
</AppText>
|
||||
|
||||
<View style={styles.uploadGrid}>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
{documentItems.map(item => {
|
||||
if (!shouldRenderItem(item.key)) return null;
|
||||
const isUploaded = !!documents[item.key];
|
||||
const currentIndex = animIndex++;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<Animated.View
|
||||
key={item.key}
|
||||
style={styles.uploadCard}
|
||||
onPress={() => handleDocumentPick(item.key)}
|
||||
entering={FadeInUp.delay(80 * currentIndex).springify()}
|
||||
style={{ width: '50%', padding: spacing.xs }}
|
||||
>
|
||||
<View style={styles.iconCircle}>{renderIcon(item.icon)}</View>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.uploadCard,
|
||||
{ width: '100%' },
|
||||
{
|
||||
borderColor: isUploaded ? colors.primary : colors.border,
|
||||
backgroundColor: isUploaded ? `${colors.primary}08` : colors.surface,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleDocumentPick(item.key)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.iconCircle, isUploaded && { backgroundColor: colors.primary }]}>
|
||||
{isUploaded ? (
|
||||
<CheckCircle size={24} color={colors.buttonText} strokeWidth={2.5} />
|
||||
) : (
|
||||
renderIcon(item.icon)
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text style={styles.cardTitle}>{item.label}</Text>
|
||||
<AppText variant="bodySmall" color={colors.text} style={{ fontWeight: '600', textAlign: 'center', marginBottom: 4 }}>
|
||||
{item.label}
|
||||
</AppText>
|
||||
|
||||
<Text style={styles.cardHint}>
|
||||
{documents[item.key]
|
||||
? `${documents[item.key]?.name} ✅`
|
||||
<AppText variant="caption" color={isUploaded ? colors.primary : colors.textSecondary} style={{ textAlign: 'center', fontSize: 10 }}>
|
||||
{isUploaded
|
||||
? `${documents[item.key]?.name?.slice(0, 18)}...`
|
||||
: 'Tap to upload PDF'}
|
||||
</Text>
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Text, View, StyleSheet } from 'react-native';
|
||||
|
||||
import DatePicker from 'react-native-date-picker'
|
||||
import MyInput from '../../../Component/MyInput';
|
||||
import { View } from 'react-native';
|
||||
import DatePicker from 'react-native-date-picker';
|
||||
import MyInput from '../../../components/MyInput';
|
||||
import MyDropdown from '../../../Component/Dropdown/MyDropdown';
|
||||
import colors from '../../../Constant/Color';
|
||||
import { DropdownOption } from '../types/types';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
|
||||
interface Props {
|
||||
formData: {
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
orgType: string;
|
||||
registrationDetail: string | number;
|
||||
socialCategory: string;
|
||||
expiryDate: string
|
||||
expiryDate: string;
|
||||
};
|
||||
handleChange: (key: string, value: string) => void;
|
||||
focusedField: string | null;
|
||||
@@ -26,7 +26,7 @@ interface Props {
|
||||
allMSME: DropdownOption[];
|
||||
dropdownStyle: object;
|
||||
setSelectedOrg: (val: number) => void;
|
||||
selectedOrg: string | number
|
||||
selectedOrg: string | number;
|
||||
}
|
||||
|
||||
const StageOne: React.FC<Props> = ({
|
||||
@@ -40,23 +40,24 @@ const StageOne: React.FC<Props> = ({
|
||||
allSocialCategories,
|
||||
dropdownStyle,
|
||||
setSelectedOrg,
|
||||
selectedOrg
|
||||
selectedOrg,
|
||||
}) => {
|
||||
console.log("masterdata",allOrganizations,allMsmeOrgs,allSocialCategories,contractorClassItems,selectedOrg !== 8)
|
||||
|
||||
const [selectedAllMSME, setSelectedAllMSME] = useState()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [date, setDate] = useState(new Date())
|
||||
const { colors, spacing } = useTheme();
|
||||
const [selectedAllMSME, setSelectedAllMSME] = useState();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [date, setDate] = useState(new Date());
|
||||
|
||||
const handleDateConfirm = (selectedDate: Date) => {
|
||||
setOpen(false);
|
||||
setDate(selectedDate);
|
||||
handleChange('expiryDate', selectedDate.toISOString().split('T')[0]); // Format as YYYY-MM-DD
|
||||
handleChange('expiryDate', selectedDate.toISOString().split('T')[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text style={styles.label}>Registration & Type Details</Text>
|
||||
|
||||
<AppText variant="caption" color={colors.primary} style={{ fontWeight: '700', letterSpacing: 0.3, textTransform: 'uppercase', marginBottom: spacing.s }}>
|
||||
Registration & Type Details
|
||||
</AppText>
|
||||
|
||||
<MyDropdown
|
||||
label="Type of Organization"
|
||||
@@ -69,24 +70,26 @@ const StageOne: React.FC<Props> = ({
|
||||
setSelectedOrg(item.value);
|
||||
handleChange('orgType', item.value);
|
||||
}}
|
||||
style={dropdownStyle}
|
||||
onFocus={() => setFocusedField('orgType')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focusedField={focusedField === 'orgType'}
|
||||
/>
|
||||
{
|
||||
selectedOrg !== 8 &&
|
||||
|
||||
{selectedOrg !== 8 && (
|
||||
<MyInput
|
||||
icon="file"
|
||||
placeholder="Registration Number *"
|
||||
label="Registration Number"
|
||||
placeholder="Enter Registration Number"
|
||||
value={formData.registrationNumber}
|
||||
onChangeText={text => handleChange('registrationNumber', text)}
|
||||
onFocus={() => setFocusedField('registrationNumber')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'registrationNumber'}
|
||||
autoCapitalize='characters' />
|
||||
}
|
||||
{selectedOrg &&
|
||||
autoCapitalize="characters"
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedOrg && (
|
||||
<MyDropdown
|
||||
label="Contractor Class"
|
||||
data={contractorClassItems}
|
||||
@@ -94,7 +97,6 @@ const StageOne: React.FC<Props> = ({
|
||||
valueField="value"
|
||||
placeholder="Select Class"
|
||||
value={selectedOrg == 8 ? contractorClassItems[0]?.value : formData.contractorClass}
|
||||
style={dropdownStyle}
|
||||
disable={selectedOrg == 8 ? true : false}
|
||||
onChange={item => {
|
||||
handleChange('contractorClass', item.value);
|
||||
@@ -102,9 +104,10 @@ const StageOne: React.FC<Props> = ({
|
||||
onFocus={() => setFocusedField('contractorClass')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focusedField={focusedField === 'contractorClass'}
|
||||
/>}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedOrg !== 8 &&
|
||||
{selectedOrg !== 8 && (
|
||||
<MyDropdown
|
||||
label="Registration Detail (MSME/NSIC etc.)"
|
||||
data={allMsmeOrgs}
|
||||
@@ -113,32 +116,15 @@ const StageOne: React.FC<Props> = ({
|
||||
placeholder="Select Registration Type"
|
||||
value={formData.registrationDetail}
|
||||
onChange={item => {
|
||||
setSelectedAllMSME(item.value)
|
||||
console.log('first', item)
|
||||
handleChange('registrationDetail', item.value)
|
||||
setSelectedAllMSME(item.value);
|
||||
handleChange('registrationDetail', item.value);
|
||||
}}
|
||||
style={dropdownStyle}
|
||||
onFocus={() => setFocusedField('registrationDetail')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focusedField={focusedField === 'registrationDetail'}
|
||||
/>}
|
||||
{/* <MyDropdown
|
||||
label="MSME Details"
|
||||
data={allMSME}
|
||||
labelField="label"
|
||||
valueField="value"
|
||||
placeholder="Select MSME Type"
|
||||
value={''}
|
||||
onChange={item => {
|
||||
setSelectedAllMSME(item.value)
|
||||
handleChange('allMsme', item.value)
|
||||
}}
|
||||
style={dropdownStyle}
|
||||
/>
|
||||
)}
|
||||
|
||||
onFocus={() => setFocusedField('allMsme')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focusedField={focusedField === 'allMsme'}
|
||||
/> */}
|
||||
<MyDropdown
|
||||
label="Social Category"
|
||||
data={allSocialCategories}
|
||||
@@ -147,25 +133,21 @@ const StageOne: React.FC<Props> = ({
|
||||
placeholder="Select Social Category"
|
||||
value={formData.socialCategory}
|
||||
onChange={item => handleChange('socialCategory', item.value)}
|
||||
style={dropdownStyle}
|
||||
|
||||
onFocus={() => setFocusedField('socialCategory')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focusedField={focusedField === 'socialCategory'}
|
||||
/>
|
||||
{
|
||||
selectedAllMSME !== 3 && selectedOrg !== 8 &&
|
||||
<>
|
||||
|
||||
{selectedAllMSME !== 3 && selectedOrg !== 8 && (
|
||||
<>
|
||||
<MyInput
|
||||
icon="calendar-alt"
|
||||
placeholder="Expiry Date *"
|
||||
label="Expiry Date"
|
||||
placeholder="Select Expiry Date"
|
||||
value={formData.expiryDate}
|
||||
onChangeText={text => handleChange('expiryDate', text)}
|
||||
onFocus={() => setFocusedField('expiryDate')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'expiryDate'}
|
||||
rightIcon='wallet'
|
||||
editable={true}
|
||||
rightIconPress={() => setOpen(true)}
|
||||
/>
|
||||
@@ -175,22 +157,12 @@ const StageOne: React.FC<Props> = ({
|
||||
date={date}
|
||||
onConfirm={handleDateConfirm}
|
||||
onCancel={() => setOpen(false)}
|
||||
mode="date" // Ensure you're using date mode
|
||||
mode="date"
|
||||
/>
|
||||
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.black,
|
||||
marginBottom: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default StageOne;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import { DropdownOption } from '../types/types';
|
||||
import MyInput from '../../../Component/MyInput';
|
||||
import MyInput from '../../../components/MyInput';
|
||||
import MyDropdown from '../../../Component/Dropdown/MyDropdown';
|
||||
|
||||
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
|
||||
interface AddressDetailsSectionProps {
|
||||
formData: {
|
||||
@@ -21,7 +21,7 @@ interface AddressDetailsSectionProps {
|
||||
cities: DropdownOption[];
|
||||
fetchCity: (stateValue: number) => void;
|
||||
dropdownStyle?: object;
|
||||
styles?: any; // Optional styles prop for custom styling
|
||||
styles?: any;
|
||||
}
|
||||
|
||||
const StageThree: React.FC<AddressDetailsSectionProps> = ({
|
||||
@@ -32,54 +32,51 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
|
||||
states,
|
||||
cities,
|
||||
fetchCity,
|
||||
styles
|
||||
styles,
|
||||
}) => {
|
||||
const { colors, spacing } = useTheme();
|
||||
|
||||
const dropdownStyle = {
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
height: 50,
|
||||
backgroundColor: '#fff',
|
||||
marginBottom: 10,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.label}>Address Details</Text>
|
||||
<AppText variant="caption" color={colors.primary} style={styles.label}>
|
||||
Address Details
|
||||
</AppText>
|
||||
|
||||
<MyInput
|
||||
icon="home"
|
||||
placeholder="Enter Address *"
|
||||
label="Address"
|
||||
placeholder="Enter Full Address"
|
||||
value={formData.address}
|
||||
onChangeText={text => handleChange('address', text)}
|
||||
onFocus={() => setFocusedField('address')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'address'}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
maxlength={10}
|
||||
icon="phone"
|
||||
placeholder="Contact Number *"
|
||||
label="Contact Number"
|
||||
maxLength={10}
|
||||
placeholder="Enter Contact Number"
|
||||
value={formData.agentContact2}
|
||||
onChangeText={text => handleChange('agentContact2', text)}
|
||||
onFocus={() => setFocusedField('agentContact2')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'agentContact2'}
|
||||
keyboardType="numeric"
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="lock"
|
||||
placeholder="Pin Code *"
|
||||
label="Pin Code"
|
||||
placeholder="Enter 6-digit Pin Code"
|
||||
keyboardType="numeric"
|
||||
maxlength={6}
|
||||
maxLength={6}
|
||||
value={formData.pinCode}
|
||||
onChangeText={text => handleChange('pinCode', text)}
|
||||
onFocus={() => setFocusedField('pinCode')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'pinCode'}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyDropdown
|
||||
@@ -94,7 +91,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
|
||||
handleChange('city', '');
|
||||
fetchCity(item.value);
|
||||
}}
|
||||
style={dropdownStyle}
|
||||
onFocus={() => setFocusedField('state')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focusedField={focusedField === 'state'}
|
||||
@@ -108,7 +104,6 @@ const StageThree: React.FC<AddressDetailsSectionProps> = ({
|
||||
placeholder="Select City"
|
||||
value={formData.city}
|
||||
onChange={item => handleChange('city', item.value)}
|
||||
style={dropdownStyle}
|
||||
onFocus={() => setFocusedField('city')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focusedField={focusedField === 'city'}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
View,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import MyInput from '../../../Component/MyInput';
|
||||
|
||||
// Adjust the path to your custom input
|
||||
import MyInput from '../../../components/MyInput';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
|
||||
interface Props {
|
||||
formData: {
|
||||
@@ -21,12 +21,10 @@ interface Props {
|
||||
handleChange: (key: string, value: string) => void;
|
||||
focusedField: string | null;
|
||||
setFocusedField: (field: string | null | any) => void;
|
||||
// PAN
|
||||
panVerified: boolean;
|
||||
setPanVerified: (value: boolean) => void;
|
||||
verifyPanHandler: () => void;
|
||||
panVerifying: boolean;
|
||||
// Aadhaar
|
||||
aadhaarVerified: boolean;
|
||||
setAadhaarVerified: (value: boolean) => void;
|
||||
sendAdharOTPHandler: (aadhaarNumber: string) => void;
|
||||
@@ -34,13 +32,12 @@ interface Props {
|
||||
setadharotpsend: (value: boolean) => void;
|
||||
verifyAdharOtpHandler: () => void;
|
||||
aadhaarVerifying: boolean;
|
||||
// GST
|
||||
verifyGSTHandler: () => void;
|
||||
gstverifying: boolean;
|
||||
color: any;
|
||||
styles: any;
|
||||
formatDateInput: (text: string) => string;
|
||||
selectedOrg:string|number
|
||||
selectedOrg: string | number;
|
||||
}
|
||||
|
||||
const StageTwo: React.FC<Props> = ({
|
||||
@@ -64,61 +61,79 @@ const StageTwo: React.FC<Props> = ({
|
||||
color,
|
||||
styles,
|
||||
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 (
|
||||
<>
|
||||
{/* ---------- PAN Details ---------- */}
|
||||
<Text style={styles.label}>PAN Details</Text>
|
||||
{/* ── PAN Details ── */}
|
||||
<AppText variant="caption" color={colors.primary} style={styles.label}>
|
||||
PAN Details
|
||||
</AppText>
|
||||
|
||||
<MyInput
|
||||
icon="file"
|
||||
placeholder={selectedOrg == 8 ? "Pan Number *" : "Organization PAN Number *"}
|
||||
label={selectedOrg == 8 ? "PAN Number" : "Organization PAN Number"}
|
||||
placeholder={selectedOrg == 8 ? "Enter PAN Number" : "Enter Organization PAN"}
|
||||
value={formData.orgPanNumber}
|
||||
onChangeText={(text) => {
|
||||
setPanVerified(false);
|
||||
handleChange('orgPanNumber', text);
|
||||
}}
|
||||
isVarify={panVerified}
|
||||
isVerify={panVerified}
|
||||
onFocus={() => setFocusedField('pan')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'pan'}
|
||||
editable={!panVerified}
|
||||
autoCapitalize="characters"
|
||||
maxlength={10}
|
||||
maxLength={10}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="calendar"
|
||||
placeholder={selectedOrg == 8 ? "Date of Birth *" :"Date of Incorporation *"}
|
||||
label={selectedOrg == 8 ? "Date of Birth" : "Date of Incorporation"}
|
||||
placeholder="DD/MM/YYYY"
|
||||
value={formData.dateOfIncorp}
|
||||
onChangeText={(text) => {
|
||||
const formatted = formatDateInput(text);
|
||||
setPanVerified(false);
|
||||
handleChange('dateOfIncorp', formatted);
|
||||
}}
|
||||
isVarify={panVerified}
|
||||
isVerify={panVerified}
|
||||
onFocus={() => setFocusedField('dob')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'dob'}
|
||||
editable={!panVerified}
|
||||
keyboardType='numeric'
|
||||
keyboardType="numeric"
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="file-signature"
|
||||
placeholder="PAN Name *"
|
||||
label="PAN Name"
|
||||
placeholder="Enter Name as on PAN"
|
||||
value={formData.orgPanName}
|
||||
onChangeText={(text) => {
|
||||
setPanVerified(false);
|
||||
handleChange('orgPanName', text);
|
||||
}}
|
||||
isVarify={panVerified}
|
||||
isVerify={panVerified}
|
||||
onFocus={() => setFocusedField('panName')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'panName'}
|
||||
editable={!panVerified}
|
||||
autoCapitalize="characters"
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
{formData.orgPanNumber &&
|
||||
@@ -126,32 +141,69 @@ const StageTwo: React.FC<Props> = ({
|
||||
formData.orgPanName &&
|
||||
!panVerified && (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: color.primary,
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
width: 120,
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
style={verifyButtonStyle}
|
||||
disabled={panVerifying}
|
||||
onPress={verifyPanHandler}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={{ color: 'white', fontWeight: '600' }}>
|
||||
{panVerifying ? 'Verifying' : 'Verify PAN'}
|
||||
</Text>
|
||||
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
|
||||
{panVerifying ? 'Verifying...' : 'Verify PAN'}
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* ---------- Aadhaar Details ---------- */}
|
||||
<Text style={styles.label}>Aadhaar Details</Text>
|
||||
{panVerified && (
|
||||
<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
|
||||
icon="address-card"
|
||||
placeholder="Aadhaar Number *"
|
||||
label="Aadhaar Number"
|
||||
placeholder="Enter 12-digit Aadhaar Number"
|
||||
keyboardType="numeric"
|
||||
maxlength={12}
|
||||
maxLength={12}
|
||||
value={formData.aadhaarNumber}
|
||||
onChangeText={(text) => {
|
||||
handleChange('aadhaarNumber', text);
|
||||
@@ -159,31 +211,27 @@ const StageTwo: React.FC<Props> = ({
|
||||
setadharotpsend(false);
|
||||
handleChange('aadhaarOtp', '');
|
||||
}}
|
||||
isVarify={aadhaarVerified}
|
||||
isVerify={aadhaarVerified}
|
||||
onFocus={() => setFocusedField('adhar')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'adhar'}
|
||||
editable={!aadhaarVerified}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
{formData.aadhaarNumber.length === 12 && !adharotpsend && (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: color.primary,
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
width: 120,
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
style={verifyButtonStyle}
|
||||
onPress={() => sendAdharOTPHandler(formData.aadhaarNumber)}
|
||||
disabled={aadhaarVerifying}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
@@ -191,43 +239,78 @@ const StageTwo: React.FC<Props> = ({
|
||||
{adharotpsend && !aadhaarVerified && (
|
||||
<>
|
||||
<MyInput
|
||||
icon="lock"
|
||||
placeholder="Enter OTP *"
|
||||
label="OTP"
|
||||
placeholder="Enter 6-digit OTP"
|
||||
keyboardType="numeric"
|
||||
maxlength={6}
|
||||
maxLength={6}
|
||||
value={formData.aadhaarOtp}
|
||||
onChangeText={(text) => handleChange('aadhaarOtp', text)}
|
||||
onFocus={() => setFocusedField('adharOtp')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'adharOtp'}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: color.primary,
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
width: 120,
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
style={verifyButtonStyle}
|
||||
onPress={verifyAdharOtpHandler}
|
||||
disabled={aadhaarVerifying}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={{ color: 'white', fontWeight: '600' }}>
|
||||
{aadhaarVerifying ? 'Verifying...' : 'Verify'}
|
||||
</Text>
|
||||
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '700' }}>
|
||||
{aadhaarVerifying ? 'Verifying...' : 'Verify OTP'}
|
||||
</AppText>
|
||||
</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
|
||||
icon="file-invoice"
|
||||
placeholder="GSTN No"
|
||||
label="GSTN Number"
|
||||
placeholder="Enter GSTN Number"
|
||||
value={formData.gstnNo}
|
||||
onChangeText={(text) => handleChange('gstnNo', text)}
|
||||
onFocus={() => setFocusedField('gstNumber')}
|
||||
@@ -235,30 +318,10 @@ const StageTwo: React.FC<Props> = ({
|
||||
focused={focusedField === 'gstNumber'}
|
||||
autoCapitalize="characters"
|
||||
/>
|
||||
|
||||
{/* {formData?.gstnNo?.length === 15 && (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: color.primary,
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
width: 120,
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
onPress={verifyGSTHandler}
|
||||
>
|
||||
<Text style={{ color: 'white', fontWeight: '600' }}>
|
||||
{gstverifying ? 'Verifying...' : 'Verify GST'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)} */}
|
||||
</>}
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StageTwo;
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Text, View, StyleSheet } from 'react-native';
|
||||
import MyInput from '../../../Component/MyInput';
|
||||
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import MyInput from '../../../components/MyInput';
|
||||
import { useTheme } from '../../../Hooks/useTheme';
|
||||
import AppText from '../../../components/AppText';
|
||||
|
||||
|
||||
interface Props {
|
||||
formData: {
|
||||
|
||||
agentContact2: string;
|
||||
organizationName: string;
|
||||
officialWebsite: string;
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
email: string;
|
||||
firstName: string;
|
||||
middleName: string;
|
||||
lastName: string
|
||||
lastName: string;
|
||||
};
|
||||
handleChange: (key: string, value: string) => void;
|
||||
focusedField: string | null;
|
||||
@@ -29,27 +29,29 @@ const StageZero: React.FC<Props> = ({
|
||||
setFocusedField,
|
||||
styles,
|
||||
}) => {
|
||||
const { colors, spacing } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.label}>Personal & Contact Details</Text>
|
||||
{/* <View style={styles.row}> */}
|
||||
<AppText variant="caption" color={colors.primary} style={styles.label}>
|
||||
Personal & Contact Details
|
||||
</AppText>
|
||||
|
||||
<MyInput
|
||||
icon="user"
|
||||
placeholder="First Name *"
|
||||
label="First Name"
|
||||
placeholder="First Name"
|
||||
value={formData.firstName}
|
||||
onChangeText={(text: string) => handleChange('firstName', text)}
|
||||
onFocus={() => setFocusedField('firstname')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'firstname'}
|
||||
editable={false}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
<View style={{ width: 10 }} />
|
||||
{
|
||||
formData.middleName ?
|
||||
|
||||
{formData.middleName ? (
|
||||
<MyInput
|
||||
icon="user"
|
||||
label="Middle Name"
|
||||
placeholder="Middle Name"
|
||||
value={formData.middleName}
|
||||
onChangeText={(text: string) => handleChange('middleName', text)}
|
||||
@@ -57,24 +59,25 @@ const StageZero: React.FC<Props> = ({
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'middleName'}
|
||||
editable={false}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
: null}
|
||||
|
||||
) : null}
|
||||
|
||||
<MyInput
|
||||
icon="user"
|
||||
placeholder="Last Name *"
|
||||
label="Last Name"
|
||||
placeholder="Last Name"
|
||||
value={formData.lastName}
|
||||
onChangeText={(text: string) => handleChange('lastName', text)}
|
||||
onFocus={() => setFocusedField('lastName')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'lastName'}
|
||||
editable={false}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="envelope"
|
||||
placeholder="Email *"
|
||||
label="Email"
|
||||
placeholder="Email Address"
|
||||
keyboardType="email-address"
|
||||
value={formData.email}
|
||||
onChangeText={(text: string) => handleChange('email', text)}
|
||||
@@ -82,11 +85,12 @@ const StageZero: React.FC<Props> = ({
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'email'}
|
||||
editable={false}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="phone"
|
||||
placeholder="Mobile Number *"
|
||||
label="Mobile Number"
|
||||
placeholder="Mobile Number"
|
||||
keyboardType="phone-pad"
|
||||
value={formData.mobileNumber}
|
||||
onChangeText={(text: string) => handleChange('mobileNumber', text)}
|
||||
@@ -94,22 +98,24 @@ const StageZero: React.FC<Props> = ({
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'mobileNumber'}
|
||||
editable={false}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="building"
|
||||
placeholder="Organization Name *"
|
||||
label="Organization Name"
|
||||
placeholder="Enter Organization Name"
|
||||
value={formData.organizationName}
|
||||
onChangeText={(text: string) => handleChange('organizationName', text)}
|
||||
onFocus={() => setFocusedField('organizationName')}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
focused={focusedField === 'organizationName'}
|
||||
autoCapitalize="none"
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
icon="globe"
|
||||
placeholder="Official Website (optional)"
|
||||
label="Official Website"
|
||||
placeholder="Website (optional)"
|
||||
value={formData.officialWebsite}
|
||||
onChangeText={(text: string) => handleChange('officialWebsite', text)}
|
||||
onFocus={() => setFocusedField('officialWebsite')}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Alert,
|
||||
@@ -13,16 +11,18 @@ import {
|
||||
ScrollView,
|
||||
ToastAndroid,
|
||||
} from 'react-native';
|
||||
import colors from '../../Constant/Color';
|
||||
import { User, Phone, Mail, Lock, Eye, EyeOff, ArrowLeft, CheckSquare, Square } from 'lucide-react-native';
|
||||
import { User, Phone, Mail, Lock, CheckSquare, Square, Shield, UserPlus, Contact, Eye, EyeOff } from 'lucide-react-native';
|
||||
import { useGetDesignation, useRegisterContractor } from '../../Services/ApiCall/Hooks';
|
||||
import MyDropdown from '../../Component/Dropdown/MyDropdown';
|
||||
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
|
||||
import PrimaryButton from '../../components/PrimaryButton';
|
||||
import MyInput from '../../components/MyInput';
|
||||
import InsideHeader from '../../Component/Header/InsideHeader';
|
||||
import TermsAndConditionsModal from '../../Component/TermsAndConditionsModal/TermsAndConditionsModal';
|
||||
import AppText from '../../components/AppText';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Animated, { FadeInUp } from 'react-native-reanimated';
|
||||
import { InsideHeader } from '../../Component/Index';
|
||||
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
|
||||
export type ContractorFormData = {
|
||||
firstName: string;
|
||||
@@ -40,7 +40,7 @@ export type ContractorFormData = {
|
||||
|
||||
type DesignationOption = {
|
||||
label: string;
|
||||
value: string; // string id from API
|
||||
value: string;
|
||||
};
|
||||
|
||||
const SignupSchema = Yup.object().shape({
|
||||
@@ -48,7 +48,7 @@ const SignupSchema = Yup.object().shape({
|
||||
firstName: Yup.string().required('First name is required.'),
|
||||
middleName: Yup.string().optional(),
|
||||
lastName: Yup.string().required('Last name is required.'),
|
||||
designation: Yup.string().required('Please select a designation.'), // string
|
||||
designation: Yup.string().required('Please select a designation.'),
|
||||
mobileNumber: Yup.string()
|
||||
.matches(/^[6-9]\d{9}$/, 'Invalid mobile number.')
|
||||
.required('Mobile number is required.'),
|
||||
@@ -65,6 +65,7 @@ const SignupSchema = Yup.object().shape({
|
||||
});
|
||||
|
||||
const Signup = () => {
|
||||
const { colors, spacing, typography, shadows } = useTheme();
|
||||
const navigation = useNavigation<any>();
|
||||
const [designation, setDesignation] = useState<DesignationOption[]>([]);
|
||||
const { data, isLoading: isDesignationLoading } = useGetDesignation();
|
||||
@@ -74,6 +75,7 @@ const Signup = () => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
|
||||
|
||||
useEffect(() => {
|
||||
const list = (data as any)?.data;
|
||||
@@ -81,89 +83,15 @@ const Signup = () => {
|
||||
|
||||
const formattedDesignation: DesignationOption[] = list.map((item: any) => ({
|
||||
label: item.designationName,
|
||||
value: String(item.designationid), // e.g. "3"
|
||||
value: String(item.designationid),
|
||||
}));
|
||||
setDesignation(formattedDesignation);
|
||||
|
||||
}, [data]);
|
||||
|
||||
|
||||
const dropdownStyle = {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
height: 50,
|
||||
backgroundColor: colors.white,
|
||||
marginBottom: 10,
|
||||
};
|
||||
|
||||
const renderInputWithIcon = (
|
||||
icon: 'user' | 'phone' | 'mail',
|
||||
placeholder: string,
|
||||
value: string,
|
||||
onChangeText: (text: string) => void,
|
||||
onBlur: () => void,
|
||||
error?: string,
|
||||
touched?: boolean,
|
||||
props: object = {},
|
||||
) => {
|
||||
const IconComponent = icon === 'user' ? User : icon === 'phone' ? Phone : Mail;
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.inputWrapper}>
|
||||
<IconComponent size={18} color={colors.grayText} style={styles.icon} />
|
||||
<TextInput
|
||||
placeholder={placeholder}
|
||||
style={styles.input}
|
||||
placeholderTextColor={colors.placeholder}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
onBlur={onBlur}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
{touched && error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPasswordInput = (
|
||||
placeholder: string,
|
||||
value: string,
|
||||
onChangeText: (text: string) => void,
|
||||
onBlur: () => void,
|
||||
show: boolean,
|
||||
setShow: (show: boolean) => void,
|
||||
error?: string,
|
||||
touched?: boolean,
|
||||
) => (
|
||||
<>
|
||||
<View style={styles.inputWrapper}>
|
||||
<Lock size={18} color={colors.grayText} style={styles.icon} />
|
||||
<TextInput
|
||||
placeholder={placeholder}
|
||||
style={styles.input}
|
||||
placeholderTextColor={colors.placeholder}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
onBlur={onBlur}
|
||||
secureTextEntry={!show}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShow(!show)}>
|
||||
{show ? <Eye size={18} color={colors.grayText} /> : <EyeOff size={18} color={colors.grayText} />}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{touched && error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</>
|
||||
);
|
||||
|
||||
const handleSubmitForm = (values: any) => {
|
||||
console.log('values', values);
|
||||
|
||||
if (!isCheck) {
|
||||
Alert.alert('Check terms and condition');
|
||||
Alert.alert('Terms Required', 'Please accept the terms and conditions to proceed.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,7 +103,7 @@ const Signup = () => {
|
||||
mobNo: values.mobileNumber.trim(),
|
||||
emailAdd: values.email.trim(),
|
||||
contractorType: values.bidderType,
|
||||
designation: values.designation ? Number(values.designation) : 0, // convert string -> number
|
||||
designation: values.designation ? Number(values.designation) : 0,
|
||||
password: values.password.trim(),
|
||||
otherDesignation: null,
|
||||
conPassword: values.confirmPassword.trim(),
|
||||
@@ -197,21 +125,21 @@ const Signup = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SafeAreaView style={styles.container}>
|
||||
<InsideHeader title="Sign Up" showArrow />
|
||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<InsideHeader title="Create Account" showArrow />
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={40}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
|
||||
>
|
||||
<Formik
|
||||
initialValues={{
|
||||
bidderType: '',
|
||||
bidderType: 'Indian',
|
||||
firstName: '',
|
||||
middleName: '',
|
||||
lastName: '',
|
||||
designation: '', // string, not 0
|
||||
designation: '',
|
||||
mobileNumber: '',
|
||||
email: '',
|
||||
password: '',
|
||||
@@ -232,174 +160,239 @@ const Signup = () => {
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
bounces={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(80)}>
|
||||
<Text style={styles.label}>Bidder Type</Text>
|
||||
<View style={styles.toggleContainer}>
|
||||
{/* ═══ 1. Bidder Type Toggle ═══ */}
|
||||
<Animated.View entering={FadeInUp.duration(400).delay(100)} style={{ marginBottom: spacing.m }}>
|
||||
<View style={styles.sectionLabelRow}>
|
||||
<UserPlus size={14} color={colors.primary} />
|
||||
<AppText variant="caption" color={colors.primary} style={styles.sectionLabel}>
|
||||
Bidder Type
|
||||
</AppText>
|
||||
</View>
|
||||
<View style={[styles.toggleContainer, shadows.light]}>
|
||||
{['Indian', 'Foreign'].map(type => (
|
||||
<TouchableOpacity
|
||||
key={type}
|
||||
style={[
|
||||
styles.toggleButton,
|
||||
values.bidderType === type && styles.selectedToggle,
|
||||
values.bidderType === type && [styles.toggleButtonActive, { backgroundColor: colors.primary }],
|
||||
]}
|
||||
onPress={() => setFieldValue('bidderType', type)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.toggleText,
|
||||
values.bidderType === type && styles.selectedToggleText,
|
||||
]}
|
||||
<AppText
|
||||
variant="bodySmall"
|
||||
color={values.bidderType === type ? colors.buttonText : colors.textSecondary}
|
||||
style={{ fontWeight: values.bidderType === type ? '700' : '500' }}
|
||||
>
|
||||
{type}
|
||||
</Text>
|
||||
{type} Contractor
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
{touched.bidderType && errors.bidderType && (
|
||||
<Text style={styles.errorText}>{errors.bidderType}</Text>
|
||||
<AppText variant="caption" color={colors.error} style={styles.errorText}>
|
||||
{errors.bidderType}
|
||||
</AppText>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(120)}>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.halfInput}>
|
||||
{renderInputWithIcon(
|
||||
'user',
|
||||
'First Name *',
|
||||
values.firstName,
|
||||
handleChange('firstName'),
|
||||
() => handleBlur('firstName'),
|
||||
errors.firstName,
|
||||
touched.firstName,
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.halfInput}>
|
||||
{renderInputWithIcon(
|
||||
'user',
|
||||
'Middle Name',
|
||||
values.middleName,
|
||||
handleChange('middleName'),
|
||||
() => handleBlur('middleName'),
|
||||
errors.middleName,
|
||||
touched.middleName,
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ═══ 2. Personal Information ═══ */}
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(200)} style={[styles.card, shadows.medium]}>
|
||||
<View style={styles.cardHeader}>
|
||||
<User size={16} color={colors.primary} />
|
||||
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
|
||||
Personal Information
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
{renderInputWithIcon(
|
||||
'user',
|
||||
'Last Name *',
|
||||
values.lastName,
|
||||
handleChange('lastName'),
|
||||
() => handleBlur('lastName'),
|
||||
errors.lastName,
|
||||
touched.lastName,
|
||||
)}
|
||||
</Animated.View>
|
||||
<MyInput
|
||||
label="First Name"
|
||||
placeholder="Enter First Name"
|
||||
value={values.firstName}
|
||||
onChangeText={handleChange('firstName')}
|
||||
onBlur={handleBlur('firstName')}
|
||||
error={touched.firstName && errors.firstName ? errors.firstName : undefined}
|
||||
rightIcon={<User size={18} color={colors.textSecondary} />}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
label="Middle Name"
|
||||
placeholder="Enter Middle Name (Optional)"
|
||||
value={values.middleName}
|
||||
onChangeText={handleChange('middleName')}
|
||||
onBlur={handleBlur('middleName')}
|
||||
error={touched.middleName && errors.middleName ? errors.middleName : undefined}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
label="Last Name"
|
||||
placeholder="Enter Last Name"
|
||||
value={values.lastName}
|
||||
onChangeText={handleChange('lastName')}
|
||||
onBlur={handleBlur('lastName')}
|
||||
error={touched.lastName && errors.lastName ? errors.lastName : undefined}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(160)}>
|
||||
<Text style={styles.label}>Designation</Text>
|
||||
<MyDropdown
|
||||
label="Select Designation"
|
||||
label="Designation"
|
||||
labelField="label"
|
||||
valueField="value"
|
||||
data={designation}
|
||||
value={values.designation}
|
||||
onChange={(item: any) => {
|
||||
setFieldValue('designation', item.value);
|
||||
}}
|
||||
placeholder={isDesignationLoading ? 'Loading...' : 'Choose Designation'}
|
||||
style={dropdownStyle}
|
||||
onChange={(item: any) => { setFieldValue('designation', item.value); }}
|
||||
placeholder={isDesignationLoading ? 'Loading...' : 'Select Designation'}
|
||||
/>
|
||||
{touched.designation && errors.designation && (
|
||||
<Text style={styles.errorText}>{errors.designation}</Text>
|
||||
<AppText variant="caption" color={colors.error} style={styles.errorText}>
|
||||
{errors.designation}
|
||||
</AppText>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(200)}>
|
||||
{renderInputWithIcon(
|
||||
'phone',
|
||||
'Mobile Number *',
|
||||
values.mobileNumber,
|
||||
handleChange('mobileNumber'),
|
||||
() => handleBlur('mobileNumber'),
|
||||
errors.mobileNumber,
|
||||
touched.mobileNumber,
|
||||
{
|
||||
keyboardType: 'phone-pad',
|
||||
maxLength: 10,
|
||||
},
|
||||
)}
|
||||
|
||||
{renderInputWithIcon(
|
||||
'mail',
|
||||
'Email *',
|
||||
values.email,
|
||||
handleChange('email'),
|
||||
() => handleBlur('email'),
|
||||
errors.email,
|
||||
touched.email,
|
||||
{
|
||||
keyboardType: 'email-address',
|
||||
autoCapitalize: 'none',
|
||||
},
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(240)}>
|
||||
{renderPasswordInput(
|
||||
'Password *',
|
||||
values.password,
|
||||
handleChange('password'),
|
||||
() => handleBlur('password'),
|
||||
showPassword,
|
||||
setShowPassword,
|
||||
errors.password,
|
||||
touched.password,
|
||||
)}
|
||||
|
||||
{renderPasswordInput(
|
||||
'Confirm Password *',
|
||||
values.confirmPassword,
|
||||
handleChange('confirmPassword'),
|
||||
() => handleBlur('confirmPassword'),
|
||||
showConfirmPassword,
|
||||
setShowConfirmPassword,
|
||||
errors.confirmPassword,
|
||||
touched.confirmPassword,
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(280)}>
|
||||
<View style={styles.termsRow}>
|
||||
<TouchableOpacity onPress={() => setCheckModal(true)}>
|
||||
{isCheck ? (
|
||||
<CheckSquare size={20} color={colors.primary} />
|
||||
) : (
|
||||
<Square size={20} color={colors.black} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.termsText}>
|
||||
By continuing, I agree to the Terms and Conditions
|
||||
</Text>
|
||||
{/* ═══ 3. Contact Details ═══ */}
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(300)} style={[styles.card, shadows.medium]}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Contact size={16} color={colors.primary} />
|
||||
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
|
||||
Contact Details
|
||||
</AppText>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(320)} style={styles.buttonWrapper}>
|
||||
<PrimaryButton
|
||||
title={isRegistering ? 'Submitting...' : 'Submit'}
|
||||
disabled={isRegistering}
|
||||
onPress={() => handleSubmit()}
|
||||
<MyInput
|
||||
label="Mobile Number"
|
||||
placeholder="Enter Mobile Number"
|
||||
value={values.mobileNumber}
|
||||
onChangeText={handleChange('mobileNumber')}
|
||||
onBlur={handleBlur('mobileNumber')}
|
||||
error={touched.mobileNumber && errors.mobileNumber ? errors.mobileNumber : undefined}
|
||||
keyboardType="phone-pad"
|
||||
maxLength={10}
|
||||
rightIcon={<Phone size={18} color={colors.textSecondary} />}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
label="Email Address"
|
||||
placeholder="Enter Email Address"
|
||||
value={values.email}
|
||||
onChangeText={handleChange('email')}
|
||||
onBlur={handleBlur('email')}
|
||||
error={touched.email && errors.email ? errors.email : undefined}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
rightIcon={<Mail size={18} color={colors.textSecondary} />}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
{/* ═══ 4. Security ═══ */}
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(400)} style={[styles.card, shadows.medium]}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Shield size={16} color={colors.primary} />
|
||||
<AppText variant="bodySmall" color={colors.primary} style={styles.cardTitle}>
|
||||
Security
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<MyInput
|
||||
label="Password"
|
||||
placeholder="••••••••"
|
||||
value={values.password}
|
||||
onChangeText={handleChange('password')}
|
||||
onBlur={handleBlur('password')}
|
||||
secureTextEntry={!showPassword}
|
||||
error={touched.password && errors.password ? errors.password : undefined}
|
||||
rightIcon={
|
||||
showPassword
|
||||
? <Eye size={18} color={colors.textSecondary} />
|
||||
: <EyeOff size={18} color={colors.textSecondary} />
|
||||
}
|
||||
rightIconPress={() => setShowPassword(prev => !prev)}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
|
||||
<MyInput
|
||||
label="Confirm Password"
|
||||
placeholder="••••••••"
|
||||
value={values.confirmPassword}
|
||||
onChangeText={handleChange('confirmPassword')}
|
||||
onBlur={handleBlur('confirmPassword')}
|
||||
secureTextEntry={!showConfirmPassword}
|
||||
error={touched.confirmPassword && errors.confirmPassword ? errors.confirmPassword : undefined}
|
||||
rightIcon={
|
||||
showConfirmPassword
|
||||
? <Eye size={18} color={colors.textSecondary} />
|
||||
: <EyeOff size={18} color={colors.textSecondary} />
|
||||
}
|
||||
rightIconPress={() => setShowConfirmPassword(prev => !prev)}
|
||||
containerStyle={{ marginBottom: spacing.s }}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
{/* ═══ Terms & Conditions ═══ */}
|
||||
<Animated.View entering={FadeInUp.duration(450).delay(500)}>
|
||||
<TouchableOpacity
|
||||
style={[styles.termsCard, shadows.light]}
|
||||
onPress={() => setCheckModal(true)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isCheck ? (
|
||||
<CheckSquare size={22} color={colors.primary} strokeWidth={2.5} />
|
||||
) : (
|
||||
<Square size={22} color={colors.textSecondary} strokeWidth={2} />
|
||||
)}
|
||||
<AppText variant="bodySmall" color={colors.textSecondary} style={{ flex: 1 }}>
|
||||
I agree to the{' '}
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700', textDecorationLine: 'underline' }}>
|
||||
Terms and Conditions
|
||||
</AppText>
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
{/* ═══ Submit Button ═══ */}
|
||||
<Animated.View entering={FadeInDown.duration(400).delay(600)} style={styles.buttonWrapper}>
|
||||
<PrimaryButton
|
||||
title="Create Account"
|
||||
loading={isRegistering}
|
||||
onPress={handleSubmit}
|
||||
variant="primary"
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
{/* ═══ Login Link ═══ */}
|
||||
<Animated.View entering={FadeInUp.duration(400).delay(650)}>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.pop()}
|
||||
style={styles.loginRow}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.textSecondary}>
|
||||
Already have an account?{' '}
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
|
||||
Sign In
|
||||
</AppText>
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
{checkModal && (
|
||||
<TermsAndConditionsModal
|
||||
visible={checkModal}
|
||||
onClose={() => setCheckModal(false)}
|
||||
onChecked={() => setIsCheck(true)}
|
||||
onChecked={() => { setIsCheck(true); setCheckModal(false); }}
|
||||
/>
|
||||
)}
|
||||
</ScrollView>
|
||||
@@ -413,99 +406,85 @@ const Signup = () => {
|
||||
|
||||
export default Signup;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
|
||||
// ── Styles ────────────────────────────────────────────────────────────
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 24,
|
||||
justifyContent: 'flex-start',
|
||||
padding: spacing.m,
|
||||
paddingBottom: 30,
|
||||
},
|
||||
label: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
marginTop: 6,
|
||||
marginBottom: 4,
|
||||
color: colors.darkText,
|
||||
sectionLabelRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
marginBottom: 6,
|
||||
},
|
||||
sectionLabel: {
|
||||
fontWeight: '700',
|
||||
fontSize: typography.size.xs,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
toggleContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 4,
|
||||
borderRadius: 12,
|
||||
padding: 3,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
height: 46,
|
||||
},
|
||||
toggleButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 4,
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: 9,
|
||||
},
|
||||
selectedToggle: {
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
toggleButtonActive: {
|
||||
elevation: 2,
|
||||
},
|
||||
toggleText: {
|
||||
fontSize: 14,
|
||||
color: colors.grayText,
|
||||
card: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
selectedToggleText: {
|
||||
color: colors.white,
|
||||
fontWeight: '600',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 6,
|
||||
},
|
||||
halfInput: {
|
||||
width: '48%',
|
||||
},
|
||||
inputWrapper: {
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
paddingHorizontal: 10,
|
||||
marginTop: 8,
|
||||
height: 52,
|
||||
gap: 8,
|
||||
marginBottom: spacing.s,
|
||||
paddingBottom: spacing.xs,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
icon: {
|
||||
marginRight: 10,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: colors.darkText,
|
||||
paddingVertical: 0,
|
||||
cardTitle: {
|
||||
fontWeight: '700',
|
||||
fontSize: typography.size.s,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
errorText: {
|
||||
color: colors.error,
|
||||
fontSize: 12,
|
||||
marginTop: 3,
|
||||
marginLeft: 4,
|
||||
marginTop: 4,
|
||||
fontSize: typography.size.xs,
|
||||
fontWeight: '500',
|
||||
},
|
||||
termsRow: {
|
||||
termsCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 14,
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
termsText: {
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
color: colors.darkText,
|
||||
marginLeft: 8,
|
||||
gap: 10,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
marginBottom: spacing.m,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
buttonWrapper: {
|
||||
marginTop: 18,
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
loginRow: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 { useNavigation, useIsFocused } from '@react-navigation/native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useStore } from 'react-redux'; // For hydration check
|
||||
|
||||
import { showToast } from '../../Constant/ShowDailog';
|
||||
import { useRagistrationState } from '../../Services/ApiCall/Hooks';
|
||||
@@ -24,37 +23,13 @@ const Splash: React.FC = () => {
|
||||
const isFocused = useIsFocused();
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const reduxToken = useSelector((state: any) => state.token?.token);
|
||||
const store = useStore();
|
||||
const isPersistRehydrated = useSelector((state: any) => state?._persist?.rehydrated);
|
||||
const { mutateAsync: registrationStateMutate } = useRagistrationState();
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const hasSessionToken = Boolean(reduxToken && typeof reduxToken === 'object' && reduxToken.token);
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: any;
|
||||
const checkHydration = () => {
|
||||
const token = (store.getState() as any).token?.token;
|
||||
if (token !== undefined) {
|
||||
setIsHydrated(true);
|
||||
unsubscribe?.();
|
||||
}
|
||||
};
|
||||
|
||||
unsubscribe = store.subscribe(checkHydration);
|
||||
checkHydration(); // Check immediately
|
||||
|
||||
return () => unsubscribe?.();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const routeByOrgType = (params: { otherId: any; orgType: any }) => {
|
||||
|
||||
|
||||
|
||||
// const orgType = params.orgType;
|
||||
const orgType = "7" as string; // For testing purpose
|
||||
const routeByOrgType = useCallback((params: { otherId: any; orgType: any }) => {
|
||||
const orgType = String(params.orgType ?? '');
|
||||
if (orgType === '4' || orgType === '5' || orgType === '6') {
|
||||
|
||||
navigation.replace(NavigationString.StageThreeScreen, params);
|
||||
return;
|
||||
}
|
||||
@@ -65,18 +40,26 @@ const Splash: React.FC = () => {
|
||||
}
|
||||
|
||||
navigation.replace('Dashboard');
|
||||
};
|
||||
}, [navigation]);
|
||||
|
||||
const handleGetData = async () => {
|
||||
const handleGetData = useCallback(async () => {
|
||||
try {
|
||||
const res = await registrationStateMutate();
|
||||
|
||||
if ( res?.data?.message==="No Response From Server"&& res?.data?.success===false){
|
||||
console.log("splash", res);
|
||||
if (res?.status === false) {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
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) {
|
||||
navigation.replace('Registration');
|
||||
@@ -86,7 +69,6 @@ const Splash: React.FC = () => {
|
||||
if (res?.statusCode === 200 && res?.data) {
|
||||
const { registrationStatus, contOtherId, contOrganization } = res.data;
|
||||
const params = { otherId: contOtherId, orgType: contOrganization };
|
||||
|
||||
switch (registrationStatus) {
|
||||
case null:
|
||||
case undefined:
|
||||
@@ -115,18 +97,19 @@ const Splash: React.FC = () => {
|
||||
if (error?.response?.status === 404) {
|
||||
navigation.replace('Registration');
|
||||
} else {
|
||||
await AsyncStorage.removeItem('token');
|
||||
navigation.replace('Signin');
|
||||
await AsyncStorage.removeItem('persist:root');
|
||||
navigation.replace(NavigationString.Login);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [navigation, registrationStateMutate, routeByOrgType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocused || !isHydrated) return;
|
||||
if (!isFocused || !isPersistRehydrated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runSplash = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 500,
|
||||
@@ -134,7 +117,7 @@ const Splash: React.FC = () => {
|
||||
}).start();
|
||||
|
||||
|
||||
if (!reduxToken.token) {
|
||||
if (!hasSessionToken) {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: NavigationString.Login }],
|
||||
@@ -142,7 +125,7 @@ const Splash: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reduxToken.userType == 'tender') {
|
||||
if (reduxToken.userType === 'tender') {
|
||||
await handleGetData();
|
||||
} else {
|
||||
navigation.reset({
|
||||
@@ -154,18 +137,16 @@ const Splash: React.FC = () => {
|
||||
} catch (error) {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: NavigationString.Login || 'Signin' }],
|
||||
routes: [{ name: NavigationString.Login }],
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(runSplash, 200);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [isFocused, isHydrated]);
|
||||
}, [fadeAnim, handleGetData, hasSessionToken, isFocused, isPersistRehydrated, navigation, reduxToken?.userType]);
|
||||
|
||||
if (!isHydrated) {
|
||||
if (!isPersistRehydrated) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Animated.Image
|
||||
@@ -185,11 +166,6 @@ const Splash: React.FC = () => {
|
||||
style={[styles.logo, { opacity: fadeAnim }]}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
{isLoading && (
|
||||
<View style={{ marginTop: 20 }}>
|
||||
{/* Optional spinner */}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -353,6 +353,7 @@ const EnvelopeCExcelPreview = (props: any) => {
|
||||
deviceType: 'MOBILE',
|
||||
fileName: 'envelopeC_encrypted.xlsx',
|
||||
saveEnvelopesOfficersDetailsResDTO: officerEncryptedKeys,
|
||||
randomKeyGenerated: ENCRYPTION_KEY,
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,18 +1,19 @@
|
||||
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 { useNavigation } from '@react-navigation/native';
|
||||
import { useFetchLiveAuction, useFetchLiveTender } from '../../Services/ApiCall/Hooks';
|
||||
import { SearchInput } from '../../Component/Serach/SeacrhInput';
|
||||
import { Skelton } from '../../Component/Index';
|
||||
import colors from '../../Constant/Color';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
|
||||
import LiveTenderTile from './LiveTenderTile';
|
||||
import { useTheme } from '../../Hooks/useTheme'; // Using Theme
|
||||
|
||||
const LiveTender = () => {
|
||||
const navigation: any = useNavigation();
|
||||
const Dispatch = useDispatch()
|
||||
const { colors } = useTheme(); // Hook
|
||||
const user = useSelector((state: any) => state?.token.token);
|
||||
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchLiveTender();
|
||||
@@ -68,11 +69,20 @@ const LiveTender = () => {
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Skelton />;
|
||||
return <View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<Skelton />
|
||||
</View>;
|
||||
}
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<SearchInput onChange={() => { }} placeholder="Search...." />
|
||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
{/* 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 && (
|
||||
<Animated.View
|
||||
@@ -87,7 +97,7 @@ const LiveTender = () => {
|
||||
paddingHorizontal: 14,
|
||||
marginTop: 6,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.surface,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 8,
|
||||
@@ -102,7 +112,7 @@ const LiveTender = () => {
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: '#374151',
|
||||
color: colors.text,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
@@ -124,7 +134,7 @@ const LiveTender = () => {
|
||||
<RefreshControl
|
||||
refreshing={isRefetching}
|
||||
onRefresh={refetch}
|
||||
tintColor="transparent"
|
||||
tintColor={colors.primary}
|
||||
colors={[colors.primary]}
|
||||
/>
|
||||
}
|
||||
@@ -133,11 +143,23 @@ const LiveTender = () => {
|
||||
windowSize={7}
|
||||
removeClippedSubviews
|
||||
updateCellsBatchingPeriod={50}
|
||||
onRefresh={refetch}
|
||||
refreshing={isRefetching}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
blobContainer: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
zIndex: -1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
blob: {
|
||||
position: 'absolute',
|
||||
width: 300,
|
||||
height: 300,
|
||||
borderRadius: 150,
|
||||
},
|
||||
});
|
||||
|
||||
export default LiveTender;
|
||||
@@ -1,20 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
StatusBar,
|
||||
SafeAreaView,
|
||||
} from 'react-native';
|
||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
useFetchLiveTenderById,
|
||||
} from '../../Services/ApiCall/Hooks';
|
||||
import { InsideHeader } from '../../Component/Index';
|
||||
import colors from '../../Constant/Color';
|
||||
import ReactNativeBlobUtil from 'react-native-blob-util';
|
||||
import { GET_DOCID } from '../../Services/ApiCall/ApiUrls';
|
||||
import {
|
||||
@@ -27,20 +22,23 @@ import {
|
||||
} from 'lucide-react-native';
|
||||
import moment from 'moment';
|
||||
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}>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<Text style={styles.value}>{value || '-'}</Text>
|
||||
<AppText variant="caption" style={styles.label}>{label}</AppText>
|
||||
<AppText variant="bodySmall" style={styles.value}>{value || '-'}</AppText>
|
||||
</View>
|
||||
);
|
||||
|
||||
const Section = ({ title, icon: Icon, children }: any) => (
|
||||
const Section = ({ title, icon: Icon, children, styles, colors }: any) => (
|
||||
<View style={styles.sectionContainer}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Icon size={18} color={colors.primary} />
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
<AppText variant="body" style={styles.sectionTitle}>{title}</AppText>
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
@@ -48,11 +46,156 @@ const Section = ({ title, icon: Icon, children }: any) => (
|
||||
|
||||
const LiveTenderDetail = () => {
|
||||
const route = useRoute<any>();
|
||||
const { colors, spacing, shadows, isDarkMode } = useTheme();
|
||||
const navigation = useNavigation<any>();
|
||||
const tender = route?.params;
|
||||
const { data } = useFetchLiveTenderById(tender?.tenderId);
|
||||
const [downloadingId, setDownloadingId] = useState<string | number | null>(null);
|
||||
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;
|
||||
|
||||
const tenderData = data.data[0];
|
||||
@@ -77,7 +220,7 @@ const LiveTenderDetail = () => {
|
||||
}).fetch('GET', downloadUrl, {
|
||||
Accept: 'application/pdf',
|
||||
});
|
||||
const stat = await ReactNativeBlobUtil.fs.stat(path);
|
||||
// const stat = await ReactNativeBlobUtil.fs.stat(path);
|
||||
showToast('Download complete');
|
||||
setDownloadingId(null);
|
||||
} catch (error: any) {
|
||||
@@ -98,193 +241,214 @@ const LiveTenderDetail = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.safeArea}>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
<View style={[styles.safeArea, { backgroundColor: colors.background }]}>
|
||||
<View style={[styles.header, { marginTop: insets.top }]}>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backBtn}
|
||||
>
|
||||
<ArrowLeft size={24} color="#1e293b" />
|
||||
<ArrowLeft size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.headerTitle}>Tender Overview</Text>
|
||||
<Text style={styles.headerSub}>
|
||||
<AppText variant="body" style={styles.headerTitle}>Tender Overview</AppText>
|
||||
<AppText variant="caption" style={styles.headerSub}>
|
||||
Ref: {tenderData?.tenderNumber || '-'}
|
||||
</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
<View style={styles.liveBox}>
|
||||
<View
|
||||
style={[
|
||||
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>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
<Section title="Basic Details" icon={Layers}>
|
||||
<Section title="Basic Details" icon={Layers} styles={styles} colors={colors}>
|
||||
<View style={styles.card}>
|
||||
<InfoRow label="Tender ID" value={tenderData?.tenderId} />
|
||||
<InfoRow label="Tender Number" value={tenderData?.tenderNumber} />
|
||||
<InfoRow label="NIT Number" value={tenderData?.nitNumber} />
|
||||
<InfoRow label="NIT Date" value={tenderData?.nitDate} />
|
||||
<InfoRow label="Name of Work" value={tenderData?.nameOfWork} />
|
||||
<InfoRow label="Description" value={tenderData?.descriptionOfWork} />
|
||||
<InfoRow label="Tender Stage" value={tenderData?.tenderStage} />
|
||||
<InfoRow label="Tender Status" value={tenderData?.tenderStatus} />
|
||||
<InfoRow label="Tender ID" value={tenderData?.tenderId} styles={styles} />
|
||||
<InfoRow label="Tender Number" value={tenderData?.tenderNumber} styles={styles} />
|
||||
<InfoRow label="NIT Number" value={tenderData?.nitNumber} styles={styles} />
|
||||
<InfoRow label="NIT Date" value={tenderData?.nitDate} styles={styles} />
|
||||
<InfoRow label="Name of Work" value={tenderData?.nameOfWork} styles={styles} />
|
||||
<InfoRow label="Description" value={tenderData?.descriptionOfWork} styles={styles} />
|
||||
<InfoRow label="Tender Stage" value={tenderData?.tenderStage} styles={styles} />
|
||||
<InfoRow label="Tender Status" value={tenderData?.tenderStatus} styles={styles} />
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Category & Type" icon={Briefcase}>
|
||||
<Section title="Category & Type" icon={Briefcase} styles={styles} colors={colors}>
|
||||
<View style={styles.card}>
|
||||
<InfoRow label="Tender Category" value={tenderData?.tenderCategory} />
|
||||
<InfoRow label="Type of Tender" value={tenderData?.typeOfTender} />
|
||||
<InfoRow label="Selective Tender" value={tenderData?.selectiveTender} />
|
||||
<InfoRow label="Area of Work" value={tenderData?.areaOfWork} />
|
||||
<InfoRow label="Tender Category" value={tenderData?.tenderCategory} styles={styles} />
|
||||
<InfoRow label="Type of Tender" value={tenderData?.typeOfTender} styles={styles} />
|
||||
<InfoRow label="Selective Tender" value={tenderData?.selectiveTender} styles={styles} />
|
||||
<InfoRow label="Area of Work" value={tenderData?.areaOfWork} styles={styles} />
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Financial Details" icon={FileText}>
|
||||
<Section title="Financial Details" icon={FileText} styles={styles} colors={colors}>
|
||||
<View style={styles.card}>
|
||||
<InfoRow
|
||||
label="Estimated Value"
|
||||
value={tenderData?.estimateValueInFig ? `₹ ${tenderData.estimateValueInFig}` : '-'}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Estimated Value (Words)"
|
||||
value={tenderData?.estimateValueInWord}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Tender Fee"
|
||||
value={tenderData?.tenderFeeInFig ? `₹ ${tenderData.tenderFeeInFig}` : '-'}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="EMD"
|
||||
value={tenderData?.emdInFig ? `₹ ${tenderData.emdInFig}` : '-'}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow label="GST %" value={tenderData?.gst} />
|
||||
<InfoRow label="GST Amount" value={tenderData?.gstAmmount} />
|
||||
<InfoRow label="GST %" value={tenderData?.gst} styles={styles} />
|
||||
<InfoRow label="GST Amount" value={tenderData?.gstAmmount} styles={styles} />
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Work & Validity" icon={Briefcase}>
|
||||
<Section title="Work & Validity" icon={Briefcase} styles={styles} colors={colors}>
|
||||
<View style={styles.card}>
|
||||
<InfoRow
|
||||
label="Bid Validity Type"
|
||||
value={tenderData?.bidValidateType}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Bid Validity Period"
|
||||
value={tenderData?.bidValidatePeriod}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Work Completion Type"
|
||||
value={tenderData?.workCompletionType}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Work Completion Period"
|
||||
value={tenderData?.workCompletionPeriod}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow label="Rainy Season" value={tenderData?.rainySeason} />
|
||||
<InfoRow label="Rainy Season" value={tenderData?.rainySeason} styles={styles} />
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Key Dates" icon={Calendar}>
|
||||
<Section title="Key Dates" icon={Calendar} styles={styles} colors={colors}>
|
||||
<View style={styles.card}>
|
||||
<InfoRow
|
||||
label="Purchase Start"
|
||||
value={formatDateTime(
|
||||
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.startDateTime : tenderData?.keydates?.tenderPurchaseDate?.startDateTime
|
||||
)}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Purchase End"
|
||||
value={formatDateTime(
|
||||
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.endDateTime : tenderData?.keydates?.tenderPurchaseDate?.endDateTime
|
||||
)}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Bid Submission Start"
|
||||
value={formatDateTime(
|
||||
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.startDateTime : tenderData?.keydates?.bidSubmissionDate?.startDateTime
|
||||
)}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Bid Submission End"
|
||||
value={formatDateTime(
|
||||
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.endDateTime : tenderData?.keydates?.bidSubmissionDate?.endDateTime,
|
||||
)}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Opening Envelope A"
|
||||
value={formatDateTime(
|
||||
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEA : tenderData?.keydates?.openingEA
|
||||
)}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Opening Envelope B"
|
||||
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEB : tenderData?.keydates?.openingEB)}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Opening Envelope C"
|
||||
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEC : tenderData?.keydates?.openingEC)}
|
||||
styles={styles}
|
||||
/>
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Envelope Configuration" icon={Layers}>
|
||||
<Section title="Envelope Configuration" icon={Layers} styles={styles} colors={colors}>
|
||||
<View style={styles.card}>
|
||||
<InfoRow
|
||||
label="Envelope Type"
|
||||
value={tenderData?.envelopeType}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Envelope A Enabled"
|
||||
value={tenderData?.envelopeA ? 'Yes' : 'No'}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Envelope B Enabled"
|
||||
value={tenderData?.envelopeB ? 'Yes' : 'No'}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Envelope C Enabled"
|
||||
value={tenderData?.envelopeC ? 'Yes' : 'No'}
|
||||
styles={styles}
|
||||
/>
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Department & Location" icon={Layers}>
|
||||
<Section title="Department & Location" icon={Layers} styles={styles} colors={colors}>
|
||||
<View style={styles.card}>
|
||||
<InfoRow label="Department" value={tenderData?.deptName} />
|
||||
<InfoRow label="Circle" value={tenderData?.circle} />
|
||||
<InfoRow label="Department" value={tenderData?.deptName} styles={styles} />
|
||||
<InfoRow label="Circle" value={tenderData?.circle} styles={styles} />
|
||||
<InfoRow
|
||||
label="Division"
|
||||
value={tenderData?.tenderSelectedDivision}
|
||||
styles={styles}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Sub Division"
|
||||
value={tenderData?.tenderSelectedSubDivision}
|
||||
styles={styles}
|
||||
/>
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Officers" icon={Layers}>
|
||||
<Section title="Officers" icon={Layers} styles={styles} colors={colors}>
|
||||
<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) => (
|
||||
<InfoRow
|
||||
key={i}
|
||||
label={`Approval Officer ${i + 1}`}
|
||||
value={`${o.name} (${o.email})`}
|
||||
styles={styles}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Documents" icon={Download}>
|
||||
<Section title="Documents" icon={Download} styles={styles} colors={colors}>
|
||||
<View style={styles.card}>
|
||||
{tenderData?.tenderDocuments?.map((doc: any, i: number) => (
|
||||
<View key={i} style={styles.documentRow}>
|
||||
@@ -292,7 +456,7 @@ const LiveTenderDetail = () => {
|
||||
<View style={styles.fileIcon}>
|
||||
<FileText size={18} color={colors.primary} />
|
||||
</View>
|
||||
<Text style={styles.docName}>{doc.fileName}</Text>
|
||||
<AppText variant="bodySmall" style={styles.docName}>{doc.fileName}</AppText>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.downloadBtn}
|
||||
@@ -302,12 +466,12 @@ const LiveTenderDetail = () => {
|
||||
<Download
|
||||
size={18}
|
||||
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'}
|
||||
</Text>
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
@@ -319,143 +483,3 @@ const 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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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 colors from '../../Constant/Color';
|
||||
import { LiveTenderTileProps } from './types/types';
|
||||
import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native';
|
||||
import TenderDetail from './Modal/TenderFullDetail';
|
||||
import TenderFullDetail from './Modal/TenderFullDetail';
|
||||
import { Calendar, Tag, Info, ArrowRight, Clock, MapPin } from 'lucide-react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
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> = ({
|
||||
contractorId,
|
||||
@@ -21,6 +24,8 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
|
||||
tenderNumber,
|
||||
}) => {
|
||||
const navigation: any = useNavigation();
|
||||
const { colors, shadows, spacing } = useTheme();
|
||||
|
||||
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss'));
|
||||
|
||||
const contractorIds = typeof data?.contractorId === 'string'
|
||||
@@ -30,227 +35,214 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
|
||||
: [];
|
||||
|
||||
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 (
|
||||
<View style={styles.cardWrapper}>
|
||||
{/* Status Tag */}
|
||||
<View
|
||||
style={[
|
||||
styles.statusTag,
|
||||
{ backgroundColor: isPurchased ? colors.error : purchaseEnded ? '#94a3b8' : (purchased ? '#94a3b8' : '#4ade80') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.container}
|
||||
activeOpacity={0.9}
|
||||
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.badge}>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
{/* Header Section */}
|
||||
<View style={styles.headerRow}>
|
||||
<View style={[styles.idTag, { backgroundColor: colors.background }]}>
|
||||
<Tag size={14} color={colors.primary} />
|
||||
<Text style={styles.badgeText}>#{tenderNumber}</Text>
|
||||
</View>
|
||||
<Text style={styles.priceText}>₹{tenderFees}</Text>
|
||||
<AppText variant="caption" style={{ color: colors.primary, fontWeight: '700' }}>
|
||||
#{tenderNumber}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
{/* Work Title */}
|
||||
<Text style={styles.workTitle} numberOfLines={2}>
|
||||
<AppText variant="body" style={{ color: colors.primary }}>
|
||||
₹{tenderFees}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
{/* Title Section */}
|
||||
<AppText
|
||||
variant="body"
|
||||
style={[styles.title, { color: colors.text }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{tenderOfWork ?? 'General Tender Work'}
|
||||
</Text>
|
||||
</AppText>
|
||||
|
||||
{/* 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}>{format.date6(`${tenderDispatchDate}`)}</Text>
|
||||
|
||||
<View style={[styles.infoContainer, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.infoRow}>
|
||||
<View style={styles.infoCol}>
|
||||
<View style={styles.iconBox}>
|
||||
<Calendar size={16} color={colors.textSecondary} />
|
||||
</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 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' }]}>
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={[styles.infoCol, { alignItems: 'flex-end' }]}>
|
||||
<View style={styles.iconBox}>
|
||||
<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')}
|
||||
</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Action Bar */}
|
||||
<View style={styles.actionBar}>
|
||||
<TouchableOpacity style={styles.iconBtn} onPress={() => navigation.navigate('LiveTenderDetail', data)}>
|
||||
<Info size={18} color={colors.primary} />
|
||||
<Text style={styles.iconBtnText}>Details</Text>
|
||||
{/* Actions Section */}
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.detailsBtn, { borderColor: colors.border }]}
|
||||
onPress={() => navigation.navigate('LiveTenderDetail', data)}
|
||||
>
|
||||
<AppText variant="bodySmall" style={{ color: colors.textSecondary, fontWeight: '600' }}>
|
||||
View Details
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.mainButton,
|
||||
purchaseEnded
|
||||
? styles.btnDisabled
|
||||
: (purchased ? styles.btnDisabled : isPurchased
|
||||
? styles.btnPurchased
|
||||
: styles.btnActive),
|
||||
]}
|
||||
onPress={onPurchasePress}
|
||||
disabled={purchaseEnded || purchased}
|
||||
onPress={isPurchased ? () => navigation.navigate(NavigationString.PurchaseTenderList) : onPurchasePress}
|
||||
style={styles.actionBtnWrapper}
|
||||
>
|
||||
<Text
|
||||
<View
|
||||
style={[
|
||||
styles.mainButtonText,
|
||||
isPurchased && !purchaseEnded ? { color: colors.white } : { color: '#fff' },
|
||||
styles.actionBtn,
|
||||
{
|
||||
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')}
|
||||
</Text>
|
||||
{(!purchaseEnded && !purchased) && (
|
||||
<ArrowRight size={16} color={isPurchased ? colors.white : '#fff'} />
|
||||
)}
|
||||
<AppText variant="button" style={{ color: '#fff', fontSize: 12 }}>
|
||||
{purchaseEnded ? (purchased ? 'Purchased' : 'Closed') : (purchased ? 'Purchased' : isPurchased ? 'GO TO CART' : 'Add to Cart')}
|
||||
</AppText>
|
||||
{(!purchaseEnded && !purchased) && <ArrowRight size={14} color="#fff" />}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LiveTenderTile);
|
||||
|
||||
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: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
marginHorizontal: 16,
|
||||
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',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
badge: {
|
||||
idTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary + '25',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 12,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 20,
|
||||
gap: 6,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 12,
|
||||
title: {
|
||||
marginBottom: 16,
|
||||
fontWeight: '700',
|
||||
color: colors.primary,
|
||||
},
|
||||
priceText: {
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
color: '#0f172a',
|
||||
},
|
||||
workTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#334155',
|
||||
lineHeight: 24,
|
||||
},
|
||||
infoContainer: {
|
||||
borderRadius: 16,
|
||||
padding: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
infoGrid: {
|
||||
infoRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: '#f0f4f8',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 16,
|
||||
marginBottom: 18,
|
||||
alignItems: 'center',
|
||||
},
|
||||
infoItem: {
|
||||
infoCol: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
flex: 1,
|
||||
},
|
||||
infoTextGroup: {
|
||||
flexDirection: 'column',
|
||||
divider: {
|
||||
width: 1,
|
||||
height: 30,
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
marginHorizontal: 10,
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 10,
|
||||
color: '#94a3b8',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: '700',
|
||||
iconBox: {
|
||||
// width: 32,
|
||||
// height: 32,
|
||||
// borderRadius: 16,
|
||||
// alignItems: 'center',
|
||||
// justifyContent: 'center',
|
||||
// backgroundColor: 'rgba(0,0,0,0.03)',
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
color: '#1e293b',
|
||||
},
|
||||
actionBar: {
|
||||
actionRow: {
|
||||
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: {
|
||||
detailsBtn: {
|
||||
flex: 1,
|
||||
height: 46,
|
||||
borderRadius: 16,
|
||||
paddingVertical: 10,
|
||||
borderWidth: 1,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
actionBtnWrapper: {
|
||||
flex: 1.5,
|
||||
},
|
||||
actionBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
btnActive: {
|
||||
backgroundColor: '#4ade80', // light green
|
||||
},
|
||||
btnPurchased: {
|
||||
backgroundColor: colors.error,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.error,
|
||||
},
|
||||
btnDisabled: {
|
||||
backgroundColor: '#e2e8f0',
|
||||
},
|
||||
mainButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
paddingVertical: 10,
|
||||
borderRadius: 14,
|
||||
gap: 6,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ScrollView,
|
||||
SafeAreaView,
|
||||
Platform,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import Modal from 'react-native-modal';
|
||||
import moment from 'moment';
|
||||
@@ -74,8 +73,6 @@ const TenderFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
||||
statusBarTranslucent
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
|
||||
{/* Header */}
|
||||
<View style={[styles.header, { paddingTop: inset.top }]}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { StyleSheet, Text, View, ScrollView, StatusBar, TouchableOpacity, RefreshControl, ActivityIndicator, Platform } from 'react-native'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { StyleSheet, View, ScrollView, TouchableOpacity, RefreshControl, ActivityIndicator } from 'react-native'
|
||||
import React, { useCallback } from 'react'
|
||||
import { InsideHeader } from '../../Component/Index'
|
||||
import colors from '../../Constant/Color'
|
||||
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'
|
||||
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 = () => {
|
||||
// Sample data based on your JSON
|
||||
const { colors, spacing, shadows, typography } = useTheme();
|
||||
const route = useRoute<any>();
|
||||
const tenderDataId = route.params?.tender;
|
||||
const { data, refetch, isRefetching } = useProceedToBid(tenderDataId?.tenderId);
|
||||
@@ -15,101 +27,95 @@ const ProceedToBidDetail = () => {
|
||||
const [refreshing, setRefreshing] = 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>()
|
||||
// Format date from array
|
||||
|
||||
const handleStartBidding = () => {
|
||||
const handleStartBidding = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
navigation.navigate('TenderBidding', { tenderId: tenderDataId.tenderId });
|
||||
const deviceId = await DeviceInfo.getUniqueId();
|
||||
const url = `${BASE_URL}tenderBidding/getDsc-with-device-details?deviceId=${deviceId}`;
|
||||
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) {
|
||||
console.log("Error in starting bidding: ", error);
|
||||
} finally {
|
||||
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[]) => {
|
||||
if (!dateArray || dateArray.length < 3) return "N/A";
|
||||
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')}`;
|
||||
};
|
||||
|
||||
// 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 () => {
|
||||
try {
|
||||
setRefreshing(true);
|
||||
@@ -125,483 +131,410 @@ const ProceedToBidDetail = () => {
|
||||
}, [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 (
|
||||
<View style={{ flex: 1, backgroundColor: colors.white }}>
|
||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<InsideHeader title="Tender Detail" showArrow />
|
||||
|
||||
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}
|
||||
<AnimatedScrollView
|
||||
style={styles.container}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[colors.primary]} // Android
|
||||
tintColor={colors.primary} // iOS
|
||||
colors={[colors.primary]}
|
||||
tintColor={colors.primary}
|
||||
/>
|
||||
}>
|
||||
}
|
||||
>
|
||||
{/* 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>
|
||||
<Text style={styles.tenderNumber}>{tenderData?.tenderNumber}</Text>
|
||||
<Text style={styles.tenderId}>ID: {tenderData?.tenderId}</Text>
|
||||
<AppText variant="h3" color={colors.primary} style={{ fontWeight: '700' }}>{tenderData?.tenderNumber}</AppText>
|
||||
<AppText variant="caption" color={colors.textSecondary}>ID: {tenderData?.tenderId}</AppText>
|
||||
</View>
|
||||
<View style={[styles.statusBadge, { backgroundColor: tenderData?.biddingStatus ? colors.success : colors.warning }]}>
|
||||
<Text style={styles.statusBadgeText}>
|
||||
<Animated.View
|
||||
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"}
|
||||
</Text>
|
||||
</View>
|
||||
</AppText>
|
||||
</Animated.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}>
|
||||
<Text style={styles.headerInfoLabel}>NIT No.</Text>
|
||||
<Text style={styles.headerInfoValue}>{tenderData?.nitNo}</Text>
|
||||
<AppText variant="caption" color={colors.textSecondary}>NIT No.</AppText>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{tenderData?.nitNo}</AppText>
|
||||
</View>
|
||||
<View style={styles.separator} />
|
||||
<View style={[styles.separator, { backgroundColor: colors.border }]} />
|
||||
<View style={styles.headerInfoItem}>
|
||||
<Text style={styles.headerInfoLabel}>EMD Amount</Text>
|
||||
<Text style={styles.headerInfoValue}>₹{tenderData?.emdInFig}</Text>
|
||||
</View>
|
||||
<AppText variant="caption" color={colors.textSecondary}>EMD Amount</AppText>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>₹{tenderData?.emdInFig}</AppText>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
{/* Envelope Submission Status */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Envelope Submission Status</Text>
|
||||
<View style={styles.statusGrid}>
|
||||
<StatusIndicator status={tenderData?.envelopeASubmittStatus} label="Envelope A" />
|
||||
{tenderData?.showEB &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope B" />}
|
||||
{tenderData?.showLCS &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope LCS" />}
|
||||
{tenderData?.showQCBS &&<StatusIndicator status={tenderData?.envelopeBSubmittStatus} label="Envelope QCBS" />}
|
||||
{tenderData?.showEC &&<StatusIndicator status={tenderData?.envelopeCSubmittStatus} label="Envelope C" />}
|
||||
|
||||
|
||||
</View>
|
||||
<Animated.View
|
||||
entering={FadeInUp.delay(200).springify()}
|
||||
style={styles.section}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Envelope Submission Status</AppText>
|
||||
<View style={[styles.statusGrid, shadows.light]}>
|
||||
<StatusIndicator status={tenderData?.envelopeASubmittStatus} label="Envelope A" index={0} />
|
||||
{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} />}
|
||||
{tenderData?.showEC && <StatusIndicator status={tenderData?.envelopeCSubmittStatus} label="Envelope C" index={4} />}
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
{/* Envelope Logs */}
|
||||
{tenderData?.envelopeA_Logs && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Submission Logs</Text>
|
||||
{tenderData?.envelopeA_Logs && <EnvelopeLogCard log={tenderData?.envelopeA_Logs} title="Envelope A Submission" />}
|
||||
{tenderData?.envelopeB_Logs && <EnvelopeLogCard log={tenderData?.envelopeB_Logs} title="Envelope B Submission" />}
|
||||
{tenderData?.envelopeC_Logs && <EnvelopeLogCard log={tenderData?.envelopeC_Logs} title="Envelope C Submission" />}
|
||||
</View>
|
||||
<Animated.View
|
||||
entering={FadeInUp.delay(300).springify()}
|
||||
style={styles.section}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Submission Logs</AppText>
|
||||
{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 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Tender Details</Text>
|
||||
<View style={styles.detailsCard}>
|
||||
<InfoRow label="Envelope Type" value={tenderData?.envelopeType} />
|
||||
<InfoRow label="Tender Stages" value={tenderData?.tenderStages} />
|
||||
<InfoRow label="Bid Withdrawal" value={tenderData?.bidWithDraw} isHighlighted={tenderData?.bidWithdrawStatus} />
|
||||
<InfoRow label="Re-bid" value={tenderData?.reBid} />
|
||||
<InfoRow label="DSC Status" value={tenderData?.dscStatus ? "Required" : "Not Required"} />
|
||||
</View>
|
||||
<Animated.View
|
||||
entering={FadeInUp.delay(400).springify()}
|
||||
style={styles.section}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Tender Details</AppText>
|
||||
<View style={[styles.detailsCard, shadows.light]}>
|
||||
<InfoRow label="Envelope Type" value={tenderData?.envelopeType} index={0} />
|
||||
<InfoRow label="Tender Stages" value={tenderData?.tenderStages} index={1} />
|
||||
<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>
|
||||
</Animated.View>
|
||||
|
||||
{/* Time Period */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Submission Time Period</Text>
|
||||
<View style={styles.timeCard}>
|
||||
<View style={styles.timeRow}>
|
||||
<Text style={styles.timeLabel}>Starts On:</Text>
|
||||
<Text style={styles.timeValue}>{formatDate(tenderData?.submittedStartDate)}</Text>
|
||||
<Animated.View
|
||||
entering={FadeInUp.delay(500).springify()}
|
||||
style={styles.section}
|
||||
>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', marginBottom: spacing.s }}>Submission Time Period</AppText>
|
||||
<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 style={styles.timeRow}>
|
||||
<Text style={styles.timeLabel}>Ends On:</Text>
|
||||
<Text style={styles.timeValue}>{formatDate(tenderData?.submittedEndDate)}</Text>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{formatDate(tenderData?.submittedStartDate)}</AppText>
|
||||
</Animated.View>
|
||||
<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>
|
||||
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>{formatDate(tenderData?.submittedEndDate)}</AppText>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</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> */}
|
||||
</Animated.View>
|
||||
|
||||
{/* Final Status */}
|
||||
<View style={styles.finalStatusCard}>
|
||||
<Text style={styles.finalStatusTitle}>Overall Status</Text>
|
||||
<View style={styles.finalStatusRow}>
|
||||
<Text style={styles.finalStatusLabel}>All Envelopes Submitted:</Text>
|
||||
<Text style={[styles.finalStatusValue,
|
||||
{
|
||||
color: tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus
|
||||
? colors.success : colors.error
|
||||
}]}>
|
||||
{tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus
|
||||
? "COMPLETED" : "PENDING"}
|
||||
</Text>
|
||||
<Animated.View
|
||||
entering={FadeInUp.delay(600).springify()}
|
||||
style={[styles.finalStatusCard, { borderColor: colors.primary }]}
|
||||
>
|
||||
<AppText variant="body" color={colors.primary} style={{ fontWeight: '700', marginBottom: spacing.m, textAlign: 'center' }}>Overall Status</AppText>
|
||||
|
||||
<Animated.View
|
||||
entering={FadeIn.delay(700)}
|
||||
style={[styles.finalStatusRow, { borderBottomColor: colors.border }]}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Shield size={16} color={colors.textSecondary} />
|
||||
<AppText variant="bodySmall" color={colors.text}>All Envelopes Submitted:</AppText>
|
||||
</View>
|
||||
<View style={styles.finalStatusRow}>
|
||||
<Text style={styles.finalStatusLabel}>Bidding Status:</Text>
|
||||
<Text style={[styles.finalStatusValue,
|
||||
{ color: tenderData?.biddingStatus ? colors.success : colors.error }]}>
|
||||
<AppText variant="bodySmall" color={tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus ? colors.success : colors.error} style={{ fontWeight: '700' }}>
|
||||
{tenderData?.envelopeASubmittStatus && tenderData?.envelopeBSubmittStatus && tenderData?.envelopeCSubmittStatus ? "COMPLETED" : "PENDING"}
|
||||
</AppText>
|
||||
</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"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</AppText>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
|
||||
<View style={styles.footerSpace} />
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.footer}>
|
||||
{
|
||||
tenderData?.proceedtoBid && !tenderData?.viewBid && tenderData?.biddingStatus && tenderData?.bidWithdrawStatus === false ? (
|
||||
</AnimatedScrollView>
|
||||
|
||||
<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}>
|
||||
<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>
|
||||
|
||||
) :
|
||||
<Text style={styles.footerText}>One time bidding is completed</Text>
|
||||
}
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
|
||||
<AlertCircle size={16} color={colors.textSecondary} />
|
||||
<AppText variant="bodySmall" color={colors.textSecondary}>One time bidding is completed</AppText>
|
||||
</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
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const makeStyles = (colors: any, spacing: any, shadows: any) => StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
marginBottom: 60,
|
||||
padding: spacing.m,
|
||||
},
|
||||
headerCard: {
|
||||
backgroundColor: colors.primaryLight,
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
marginBottom: 20,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: colors.primary,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 16,
|
||||
padding: spacing.m,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
tenderHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
tenderNumber: {
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
color: colors.primary,
|
||||
},
|
||||
tenderId: {
|
||||
fontSize: 14,
|
||||
color: colors.darkGray,
|
||||
marginTop: 4,
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
},
|
||||
statusBadgeText: {
|
||||
color: colors.white,
|
||||
fontWeight: '600',
|
||||
fontSize: 12,
|
||||
},
|
||||
workDescription: {
|
||||
fontSize: 16,
|
||||
color: colors.darkText,
|
||||
lineHeight: 22,
|
||||
marginBottom: 16,
|
||||
},
|
||||
headerInfoRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
borderRadius: 10,
|
||||
padding: spacing.s,
|
||||
marginTop: spacing.m,
|
||||
},
|
||||
headerInfoItem: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerInfoLabel: {
|
||||
fontSize: 12,
|
||||
color: colors.darkGray,
|
||||
marginBottom: 4,
|
||||
},
|
||||
headerInfoValue: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.primary,
|
||||
},
|
||||
separator: {
|
||||
width: 1,
|
||||
height: 30,
|
||||
backgroundColor: colors.lightGray,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.primary,
|
||||
marginBottom: 12,
|
||||
paddingLeft: 4,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
statusGrid: {
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
padding: spacing.m,
|
||||
},
|
||||
statusContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
statusDot: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
marginRight: 10,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
marginBottom: spacing.s,
|
||||
gap: 8,
|
||||
},
|
||||
envelopeCard: {
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
padding: spacing.m,
|
||||
marginBottom: spacing.s,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.lightGray,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.primary,
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
envelopeBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
},
|
||||
envelopeBadgeText: {
|
||||
color: colors.white,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 12,
|
||||
},
|
||||
logDetailRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
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',
|
||||
marginBottom: 6,
|
||||
},
|
||||
detailsCard: {
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
padding: spacing.m,
|
||||
},
|
||||
infoRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.lightestGray,
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 14,
|
||||
color: colors.darkGray,
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: colors.darkText,
|
||||
},
|
||||
highlightedValue: {
|
||||
color: colors.success,
|
||||
fontWeight: '600',
|
||||
paddingVertical: spacing.s,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
timeCard: {
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
padding: spacing.m,
|
||||
},
|
||||
timeRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
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',
|
||||
paddingVertical: spacing.s,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
finalStatusCard: {
|
||||
backgroundColor: colors.primaryLight,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
marginTop: 10,
|
||||
padding: spacing.m,
|
||||
marginTop: spacing.s,
|
||||
marginBottom: spacing.l,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
finalStatusTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: colors.primary,
|
||||
marginBottom: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
finalStatusRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
finalStatusLabel: {
|
||||
fontSize: 16,
|
||||
color: colors.darkText,
|
||||
fontWeight: '500',
|
||||
},
|
||||
finalStatusValue: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
paddingVertical: spacing.s,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
footerSpace: {
|
||||
height: 30,
|
||||
},
|
||||
success: {
|
||||
color: colors.success,
|
||||
},
|
||||
error: {
|
||||
color: colors.error,
|
||||
},
|
||||
primary: {
|
||||
color: colors.primary,
|
||||
},
|
||||
warning: {
|
||||
color: colors.warning,
|
||||
},
|
||||
successLight: {
|
||||
backgroundColor: colors.successLight,
|
||||
},
|
||||
lightGray: {
|
||||
backgroundColor: colors.lightGray,
|
||||
height: 80,
|
||||
},
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: colors.white,
|
||||
padding: 16,
|
||||
backgroundColor: colors.surface,
|
||||
padding: spacing.m,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.lightGray,
|
||||
elevation: 10,
|
||||
},
|
||||
proceedButton: {
|
||||
backgroundColor: colors.primary,
|
||||
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,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -1,31 +1,53 @@
|
||||
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 colors from '../../Constant/Color';
|
||||
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useOfflineTenderPurchase } from '../../Services/ApiCall/Hooks';
|
||||
import { StackActions, useNavigation } from '@react-navigation/native';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import { showErrorAlert, showToast } from '../../Constant/ShowDailog';
|
||||
import { clearPurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
|
||||
const ProceedToPurTender = () => {
|
||||
const navigation: any = useNavigation();
|
||||
const Dispatch = useDispatch()
|
||||
const Dispatch = useDispatch();
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
|
||||
|
||||
const { mutate, isPending } = useOfflineTenderPurchase()
|
||||
const { mutate, isPending } = useOfflineTenderPurchase();
|
||||
|
||||
const calculateTotalAmount = (item: any) => {
|
||||
const gstAmount =
|
||||
(Number(item.documentFees) + Number(item.portalCharge)) *
|
||||
(Number(item.gst) / 100);
|
||||
return Number(item.documentFees) + Number(item.portalCharge) + gstAmount;
|
||||
};
|
||||
|
||||
const payableAmount =
|
||||
Number(item.documentFees) + Number(item.portalCharge) + gstAmount;
|
||||
return payableAmount
|
||||
}
|
||||
const grandTotal = SelectedPurchaseList.reduce(
|
||||
(sum: number, item: any) => sum + calculateTotalAmount(item),
|
||||
0
|
||||
);
|
||||
|
||||
const handleOfflinePurchase = async () => {
|
||||
const orderNo = `ORD${Date.now()}`;
|
||||
@@ -40,120 +62,208 @@ const ProceedToPurTender = () => {
|
||||
tenderFees: item.documentFees,
|
||||
tenderId: item.tenderId,
|
||||
tenderNumber: item.tenderNumber,
|
||||
tenderStatus: item.tenderStatus ?? "",
|
||||
tenderType: item.tenderType ?? "",
|
||||
tenderStatus: item.tenderStatus ?? '',
|
||||
tenderType: item.tenderType ?? '',
|
||||
isPurchased: item.purchaseStatus ?? false,
|
||||
totalPayableAmount: calculateTotalAmount(item),
|
||||
percentageAmount: calculateTotalAmount(item) / 100,
|
||||
newTenderDocumentFees: item.documentFees * index + 1,
|
||||
})),
|
||||
orderNo: orderNo,
|
||||
orderNo,
|
||||
};
|
||||
|
||||
try {
|
||||
mutate(body, {
|
||||
onSuccess: (res) => {
|
||||
onSuccess: (res: any) => {
|
||||
if (res.statusCode === 200) {
|
||||
showToast(res.message);
|
||||
navigation.dispatch(StackActions.pop(2));
|
||||
Dispatch(
|
||||
clearPurchaseTender()
|
||||
);
|
||||
|
||||
Dispatch(clearPurchaseTender());
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (error: any) => {
|
||||
showErrorAlert('Something went wrong');
|
||||
console.log(error, "================purchse Error");
|
||||
}
|
||||
})
|
||||
|
||||
console.log(error, '================purchse Error');
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("purchase offline error", error);
|
||||
}
|
||||
console.log('purchase offline error', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurchase = (type: 'online' | 'offline') => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
type === 'online' ? showErrorAlert('Online Purchase is disabled') : handleOfflinePurchase();
|
||||
|
||||
type === 'online'
|
||||
? showErrorAlert('Online Purchase is disabled')
|
||||
: handleOfflinePurchase();
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: any[0] }) => (
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Text style={styles.title}>{item.auctionNumber}</Text>
|
||||
<Text style={styles.pattern}>{item.auctionPattern}</Text>
|
||||
</View>
|
||||
const styles = makeStyles(colors, spacing, shadows);
|
||||
|
||||
<View style={styles.cardRow}>
|
||||
<Calendar size={18} color={colors.primary} />
|
||||
<Text style={styles.text}>End Date: {item.purchaseEnd}</Text>
|
||||
const renderItem = ({ item, index }: { item: any; index: number }) => {
|
||||
const total = calculateTotalAmount(item);
|
||||
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 style={styles.cardRow}>
|
||||
<User size={18} color={colors.primary} />
|
||||
<Text style={styles.text}>Tender No: {item.tenderNumber}</Text>
|
||||
</View>
|
||||
{/* Divider */}
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* 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}>
|
||||
<DollarSign size={18} color={colors.primary} />
|
||||
<Text style={styles.text}>Tender Fees: ₹{item.documentFees}</Text>
|
||||
</View>
|
||||
{/* Fee breakdown */}
|
||||
<View style={styles.feeSection}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={styles.feeHeader as any}>
|
||||
FEE BREAKDOWN
|
||||
</AppText>
|
||||
|
||||
<View style={styles.cardRow}>
|
||||
<DollarSign size={18} color={colors.primary} />
|
||||
<Text style={styles.text}>GST ({item.gst}%): ₹{item.gst / 100}</Text>
|
||||
<FeeRow
|
||||
label="Document Fees"
|
||||
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 style={styles.cardRow}>
|
||||
<DollarSign size={18} color={colors.primary} />
|
||||
<Text style={styles.text}>Portal Charge: ₹{item.portalCharge}</Text>
|
||||
{/* Total Banner */}
|
||||
<View style={styles.totalBanner}>
|
||||
<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 style={styles.totalRow}>
|
||||
<CreditCard size={20} color={colors.white} />
|
||||
<Text style={styles.total}>Total: ₹{calculateTotalAmount(item)}</Text>
|
||||
<AppText variant="h4" color={colors.buttonText}>
|
||||
₹ {total.toFixed(2)}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: colors.white }}>
|
||||
<InsideHeader title="Tender Purchase" showArrow />
|
||||
<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
|
||||
data={SelectedPurchaseList}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item?.tenderId?.toString()}
|
||||
contentContainerStyle={styles.container}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.bottomButtonContainer}>
|
||||
{/* Bottom Action Buttons */}
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { marginRight: 8 }]}
|
||||
style={[styles.button, styles.onlineButton]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => handlePurchase('online')}
|
||||
>
|
||||
<Text style={styles.buttonText}>Purchase Online</Text>
|
||||
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} />
|
||||
<Wifi size={18} color={colors.primary} />
|
||||
<AppText variant="button" color={colors.primary} style={styles.onlineBtnText as any}>
|
||||
Online
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { marginLeft: 8 }]}
|
||||
style={[styles.button, styles.offlineButton]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => handlePurchase('offline')}
|
||||
>
|
||||
<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.offlineBtnText as any}>
|
||||
Offline
|
||||
</AppText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{loading && (
|
||||
<View style={styles.loadingOverlay}>
|
||||
<Loader />
|
||||
{/* Loading Overlay */}
|
||||
{(loading || isPending) && (
|
||||
<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>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
@@ -161,95 +271,226 @@ const 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: {
|
||||
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: {
|
||||
padding: 16,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingTop: spacing.s,
|
||||
paddingBottom: 120,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
elevation: 5,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.12,
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
shadowRadius: 8,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 20,
|
||||
marginBottom: spacing.m,
|
||||
overflow: 'hidden',
|
||||
...shadows.medium,
|
||||
},
|
||||
cardAccent: {
|
||||
height: 4,
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
cardBody: {
|
||||
padding: spacing.m,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#0F172A',
|
||||
},
|
||||
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,
|
||||
indexBadge: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 12,
|
||||
paddingVertical: 10,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
justifyContent: 'center',
|
||||
marginTop: 2,
|
||||
},
|
||||
total: {
|
||||
fontSize: 15,
|
||||
patternBadge: {
|
||||
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',
|
||||
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',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.white,
|
||||
width: '100%',
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 10,
|
||||
elevation: 5
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingTop: spacing.s + 4,
|
||||
paddingBottom: spacing.m + 4,
|
||||
backgroundColor: colors.surface,
|
||||
gap: spacing.s,
|
||||
...shadows.strong,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 14,
|
||||
borderRadius: 16,
|
||||
gap: spacing.s,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontWeight: '700',
|
||||
fontSize: 14,
|
||||
onlineButton: {
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1.5,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
offlineButton: {
|
||||
backgroundColor: colors.buttonPrimary,
|
||||
...shadows.medium,
|
||||
},
|
||||
onlineBtnText: {
|
||||
letterSpacing: 1.2,
|
||||
} as any,
|
||||
offlineBtnText: {
|
||||
letterSpacing: 1.2,
|
||||
} as any,
|
||||
loadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
loadingCard: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 20,
|
||||
padding: spacing.l,
|
||||
alignItems: 'center',
|
||||
...shadows.strong,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ const PurchaseTenderAllList = () => {
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
<Animated.View
|
||||
|
||||
@@ -1,223 +1,410 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import Animated, { FadeIn, FadeOut, Layout, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||
import { ChevronDown, Trash2, Info } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
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 colors from '../../Constant/Color';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { togglePurchaseTender } from '../../Redux/Slices/SelectedPurchaseTender';
|
||||
import { useTheme } from '../../Hooks/useTheme';
|
||||
import AppText from '../../components/AppText';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const PurchaseTenderList = () => {
|
||||
const Navigation: any = useNavigation()
|
||||
const Dispatch = useDispatch()
|
||||
const Navigation: any = useNavigation();
|
||||
const Dispatch = useDispatch();
|
||||
const { colors, spacing, shadows, typography } = useTheme();
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
||||
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
const toggleExpand = (index: number) => {
|
||||
setExpandedIndex(prev => (prev === index ? null : index));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (SelectedPurchaseList.length === 0) {
|
||||
Navigation.goBack()
|
||||
Navigation.goBack();
|
||||
}
|
||||
},[SelectedPurchaseList]
|
||||
}, [SelectedPurchaseList]);
|
||||
|
||||
)
|
||||
const styles = makeStyles(colors, spacing, shadows);
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
{SelectedPurchaseList.map((item: any, index: any) => {
|
||||
const expanded = expandedIndex === index;
|
||||
return (
|
||||
<Animated.View
|
||||
key={item.auctionId}
|
||||
layout={Layout.springify()}
|
||||
style={styles.card}
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.container}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => toggleExpand(index)}
|
||||
>
|
||||
<CardHeader
|
||||
title={item.auctionOfWork}
|
||||
expanded={expanded}
|
||||
{SelectedPurchaseList.map((item: any, index: number) => (
|
||||
<TenderItem
|
||||
key={item.auctionId || index}
|
||||
item={item}
|
||||
index={index}
|
||||
expanded={expandedIndex === index}
|
||||
toggleExpand={toggleExpand}
|
||||
colors={colors}
|
||||
styles={styles}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Text style={styles.subTitle}>
|
||||
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>
|
||||
);
|
||||
})}
|
||||
<View style={{ height: 90 }} />
|
||||
</ScrollView>
|
||||
|
||||
{/* Footer */}
|
||||
<TouchableOpacity disabled={Boolean(!SelectedPurchaseList.length)} style={[styles.proceedButton, { backgroundColor: Boolean(!SelectedPurchaseList.length) ? colors.lightText : colors.primary }]} onPress={() => { Navigation.navigate(NavigationString.ProceedToPurTender) }}>
|
||||
<Text style={styles.proceedText}>Proceed to Purchase</Text>
|
||||
{/* Floating Proceed Button */}
|
||||
<View style={styles.footerContainer}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
disabled={!SelectedPurchaseList.length}
|
||||
style={[
|
||||
styles.proceedButton,
|
||||
{
|
||||
backgroundColor: SelectedPurchaseList.length
|
||||
? colors.buttonPrimary
|
||||
: colors.secondary,
|
||||
},
|
||||
]}
|
||||
onPress={() => Navigation.navigate(NavigationString.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>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseTenderList;
|
||||
|
||||
const CardHeader = ({ title, expanded }: {
|
||||
title: string;
|
||||
expanded: boolean;
|
||||
}) => {
|
||||
const rotation = useSharedValue(expanded ? 1 : 0);
|
||||
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
|
||||
const arrowStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${rotation.value * 180}deg` }],
|
||||
}));
|
||||
/* ─── Sub-components ─── */
|
||||
|
||||
return (
|
||||
<View style={styles.cardHeader}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Animated.View style={arrowStyle}>
|
||||
<ChevronDown size={22} color={colors.black} />
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRow = (label: string, value: string) => (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<Text style={styles.value}>{value}</Text>
|
||||
const DetailRow = ({
|
||||
label,
|
||||
value,
|
||||
colors,
|
||||
valueColor,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
colors: any;
|
||||
valueColor?: string;
|
||||
}) => (
|
||||
<View style={detailRowStyles.row}>
|
||||
<AppText variant="caption" color={colors.textSecondary} style={detailRowStyles.label as any}>
|
||||
{label}
|
||||
</AppText>
|
||||
<AppText variant="caption" color={valueColor || colors.text} style={detailRowStyles.value as any}>
|
||||
{value}
|
||||
</AppText>
|
||||
</View>
|
||||
);
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F8FAFC',
|
||||
},
|
||||
container: {
|
||||
padding: 16,
|
||||
paddingBottom: 110,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 14,
|
||||
},
|
||||
heading: {
|
||||
fontSize: 22,
|
||||
fontWeight: '800',
|
||||
color: colors.black,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 18,
|
||||
padding: 16,
|
||||
marginBottom: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
cardHeader: {
|
||||
const detailRowStyles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#0F172A',
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
subTitle: {
|
||||
fontSize: 14,
|
||||
color: '#64748B',
|
||||
marginTop: 6,
|
||||
},
|
||||
details: {
|
||||
marginTop: 16,
|
||||
paddingTop: 14,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E5E7EB',
|
||||
},
|
||||
row: {
|
||||
marginVertical: 6,
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: 'rgba(0,0,0,0.06)',
|
||||
},
|
||||
label: {
|
||||
fontSize: 13,
|
||||
color: '#64748B',
|
||||
},
|
||||
flex: 1,
|
||||
fontWeight: '500',
|
||||
} as any,
|
||||
value: {
|
||||
fontSize: 15,
|
||||
flex: 1,
|
||||
textAlign: 'right',
|
||||
fontWeight: '600',
|
||||
color: '#0F172A',
|
||||
} 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: {
|
||||
marginTop: 18,
|
||||
backgroundColor: colors.primary,
|
||||
marginTop: spacing.m,
|
||||
backgroundColor: colors.error,
|
||||
borderRadius: 14,
|
||||
paddingVertical: 12,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
gap: spacing.s,
|
||||
},
|
||||
removeText: {
|
||||
color: '#fff',
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1,
|
||||
} as any,
|
||||
footerContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: spacing.m,
|
||||
right: spacing.m,
|
||||
},
|
||||
proceedButton: {
|
||||
position: 'absolute',
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 18,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 16,
|
||||
borderRadius: 20,
|
||||
gap: spacing.s,
|
||||
...shadows.strong,
|
||||
},
|
||||
proceedText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 1.5,
|
||||
} as any,
|
||||
countBadge: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
minWidth: 24,
|
||||
height: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,268 +1,263 @@
|
||||
import React, { use, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native';
|
||||
import React, { useMemo } from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity, Platform } from 'react-native';
|
||||
import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||
import moment from 'moment';
|
||||
import CountdownTimer from '../../Component/Counter/CountdownTimer';
|
||||
import colors from '../../Constant/Color';
|
||||
import { useFetchTenderById } from '../../Services/ApiCall/Hooks';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
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 scale = useSharedValue(1);
|
||||
const elevation = useSharedValue(8);
|
||||
const { colors, spacing, shadows } = useTheme();
|
||||
const navigation = useNavigation<any>();
|
||||
const scale = useSharedValue(1);
|
||||
const elevation = useSharedValue(2); // Reduced initial elevation for a sleeker look
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.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 handlePressIn = () => {
|
||||
scale.value = withTiming(0.97, { duration: 120 });
|
||||
elevation.value = withTiming(3, { duration: 120 });
|
||||
scale.value = withTiming(0.98, { duration: 150 });
|
||||
};
|
||||
|
||||
const handlePressOut = () => {
|
||||
scale.value = withTiming(1, { duration: 160 });
|
||||
elevation.value = withTiming(8, { duration: 160 });
|
||||
scale.value = withTiming(1, { duration: 150 });
|
||||
};
|
||||
|
||||
const isLive = item?.biddingStatus === true;
|
||||
|
||||
const statusColor = isLive ? colors.primary : '#94A3B8';
|
||||
const lightStatusBg = isLive ? '#EFF6FF' : '#F8FAFC';
|
||||
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 (
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(400)}
|
||||
style={[styles.shadowContainer, animatedStyle]}
|
||||
entering={FadeInUp.duration(500).springify()}
|
||||
style={[styles.container, shadows.light, animatedStyle]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
onPress={onPressDetails}
|
||||
onPressIn={handlePressIn}
|
||||
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}>
|
||||
{/* Header */}
|
||||
<View style={styles.headerRow}>
|
||||
<View style={{ marginTop: 8 }}>
|
||||
<AppText variant="bodySmall" color={colors.text} numberOfLines={2}>
|
||||
{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 style={[styles.statusBadge, { backgroundColor: lightStatusBg }]}>
|
||||
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
||||
<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}>
|
||||
<AppText variant="caption" color={colors.textSecondary}>Tender Fee</AppText>
|
||||
<AppText variant="caption" style={{ fontWeight: '700' }}>
|
||||
₹{Number(item?.tenderFeeInFig || 0).toLocaleString('en-IN')}
|
||||
</Text>
|
||||
<Text style={styles.feeLabel}>Tender Fee</Text>
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Date */}
|
||||
<TouchableOpacity style={styles.dateRow} onPress={() => {
|
||||
setShowDetail(true)
|
||||
}}>
|
||||
<Text style={styles.dateText}>
|
||||
<Text style={styles.dateLabel}>Bid Ends: </Text>
|
||||
{moment(
|
||||
item?.keydates?.bidSubmissionDate?.endDateTime,
|
||||
'DD-MM-YYYY HH:mm:ss'
|
||||
).format('DD MMM YYYY, hh:mm A')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.infoRow}>
|
||||
<View style={styles.iconBox}>
|
||||
<Calendar size={18} color={colors.primary} />
|
||||
</View>
|
||||
<View>
|
||||
<AppText variant="caption" color={colors.textSecondary}>Bid Ends</AppText>
|
||||
<AppText variant="caption" style={{ fontWeight: '600' }}>
|
||||
{moment(item?.keydates?.bidSubmissionDate?.endDateTime, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
{/* Footer / Timer Action */}
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.footerTop}>
|
||||
<Text style={styles.footerLabel}>Remaining Time</Text>
|
||||
|
||||
<AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' }}>
|
||||
Time Remaining
|
||||
</AppText>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
||||
{/* Timer */}
|
||||
<View style={styles.timerContainer}>
|
||||
{isLive ? (
|
||||
<CountdownTimer
|
||||
endDate={
|
||||
item?.keydates?.bidSubmissionDate?.endDateTime
|
||||
}
|
||||
endDate={item?.keydates?.bidSubmissionDate?.endDateTime}
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.endedText}>--:--:--</Text>
|
||||
<AppText variant="bodySmall" style={{ fontWeight: '700', color: colors.textSecondary }}>
|
||||
-- : -- : --
|
||||
</AppText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* CTA Button */}
|
||||
<TouchableOpacity
|
||||
disabled={!isLive}
|
||||
onPress={() => {
|
||||
if (isLive) {
|
||||
if (isLive && 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={[
|
||||
styles.actionButton,
|
||||
{ backgroundColor: statusColor },
|
||||
!isLive && styles.disabledButton,
|
||||
{ backgroundColor: isLive ? colors.primary : colors.border },
|
||||
!isLive && styles.disabledButton
|
||||
]}
|
||||
>
|
||||
<Text style={styles.addText}>
|
||||
{isLive ? 'Proceed to Bid' : 'Tender Closed'}
|
||||
</Text>
|
||||
<AppText variant="button" color={isLive ? colors.buttonText : colors.textSecondary} style={{ marginRight: 8 }}>
|
||||
{isLive ? 'Bid Now' : 'Closed'}
|
||||
</AppText>
|
||||
{isLive && <ArrowRight size={16} color={colors.buttonText} />}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{/* Tender Detail Modal */}
|
||||
|
||||
<TenderFullDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
|
||||
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseTenderTile;
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
shadowContainer: {
|
||||
marginHorizontal: 5,
|
||||
marginTop: 14,
|
||||
marginBottom: 6,
|
||||
const styling = (colors: any, spacing: any) => StyleSheet.create({
|
||||
container: {
|
||||
marginHorizontal: spacing.s,
|
||||
marginBottom: spacing.m,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#fff',
|
||||
elevation: 8,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
cardInternal: {
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: colors.surface,
|
||||
padding: spacing.m,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0,0,0,0.03)',
|
||||
},
|
||||
statusAccent: {
|
||||
width: 5,
|
||||
height: '100%',
|
||||
header: {
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
mainInfo: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
headerRow: {
|
||||
headerTopUser: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
statusBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 6,
|
||||
marginBottom: 6,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
statusDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
marginRight: 5,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 20,
|
||||
gap: 6
|
||||
},
|
||||
statusText: {
|
||||
fontWeight: '700',
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
title: {
|
||||
fontWeight: '800',
|
||||
fontSize: 16,
|
||||
color: '#1E293B',
|
||||
},
|
||||
amountContainer: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
amount: {
|
||||
fontWeight: '900',
|
||||
fontSize: 18,
|
||||
color: '#0F172A',
|
||||
},
|
||||
feeLabel: {
|
||||
fontSize: 10,
|
||||
color: '#64748B',
|
||||
fontWeight: '500',
|
||||
},
|
||||
dateRow: {
|
||||
backgroundColor: '#F8FAFC',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 10,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#F1F5F9',
|
||||
},
|
||||
dateText: {
|
||||
fontSize: 12,
|
||||
color: '#334155',
|
||||
fontWeight: '600',
|
||||
},
|
||||
dateLabel: {
|
||||
fontWeight: '400',
|
||||
color: '#64748B',
|
||||
},
|
||||
footer: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#F1F5F9',
|
||||
paddingTop: 12,
|
||||
},
|
||||
footerTop: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
footerLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
color: '#94A3B8',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
endedText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
color: '#CBD5E1',
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: colors.border,
|
||||
marginBottom: spacing.m,
|
||||
opacity: 0.5
|
||||
},
|
||||
actionButton: {
|
||||
width: '100%',
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
infoContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.l,
|
||||
gap: spacing.s
|
||||
},
|
||||
infoRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
flex: 1,
|
||||
},
|
||||
iconBox: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary + '15', // light opacity primary
|
||||
justifyContent: '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: {
|
||||
backgroundColor: '#E2E8F0',
|
||||
},
|
||||
addText: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
opacity: 0.7
|
||||
}
|
||||
});
|
||||
|
||||
export default PurchaseTenderTile;
|
||||
|
||||