updated code with dsc and device binding

This commit is contained in:
Abin Toppo
2026-05-08 13:39:41 +05:30
parent c1ecc97e29
commit 66461370a2
120 changed files with 11910 additions and 8347 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ yarn-error.log
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
# http://docs.fastlane.tools/best-practices/source-control/
**/fastlane/report.xml
**/fastlane/Preview.html
+4
View File
@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
}
+4 -2
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { LogBox, StatusBar, View } from 'react-native';
import { LogBox, View } from 'react-native';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AlertNotificationRoot } from 'react-native-alert-notification';
import { PersistGate } from 'redux-persist/integration/react';
@@ -11,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;
+2 -2
View File
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
source 'http://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
# You may use http://rbenv.org/ or http://rvm.io/ to install and use this version
ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
+12 -12
View File
@@ -1,8 +1,8 @@
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
This is a new [**React Native**](http://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](http://github.com/react-native-community/cli).
# Getting Started
> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
> **Note**: Make sure you have completed the [Set Up Your Environment](http://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
## Step 1: Start Metro
@@ -48,7 +48,7 @@ Then, and every time you update your native dependencies, run:
bundle exec pod install
```
For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).
For more information, please visit [CocoaPods Getting Started guide](http://guides.cocoapods.org/using/getting-started.html).
```sh
# Using npm
@@ -66,7 +66,7 @@ This is one way to run your app — you can also build it directly from Android
Now that you have successfully run the app, let's make changes!
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh).
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](http://reactnative.dev/docs/fast-refresh).
When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
@@ -79,19 +79,19 @@ You've successfully run and modified your React Native App. :partying_face:
### Now what?
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started).
- If you want to add this new React Native code to an existing application, check out the [Integration guide](http://reactnative.dev/docs/integration-with-existing-apps).
- If you're curious to learn more about React Native, check out the [docs](http://reactnative.dev/docs/getting-started).
# Troubleshooting
If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
If you're having issues getting the above steps to work, see the [Troubleshooting](http://reactnative.dev/docs/troubleshooting) page.
# Learn More
To learn more about React Native, take a look at the following resources:
- [React Native Website](https://reactnative.dev) - learn more about React Native.
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
- [React Native Website](http://reactnative.dev) - learn more about React Native.
- [Getting Started](http://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
- [Learn the Basics](http://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
- [Blog](http://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
- [`@facebook/react-native`](http://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
@@ -1,4 +1,4 @@
arguments=--init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\init\\init.gradle --init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\protobuf\\init.gradle
arguments=--init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\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)
Binary file not shown.
+2 -2
View File
@@ -40,7 +40,7 @@ react {
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// See http://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
@@ -99,7 +99,7 @@ android {
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
// see http://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.procure",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 24
}
+2
View File
@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:name=".MainApplication"
@@ -22,13 +22,13 @@
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
This file is a copy of abc_edit_text_material (http://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
For more info, see http://bit.ly/3CdLStv (react-native/pull/29452) and http://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
+3 -3
View File
@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
@@ -19,13 +19,13 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
# http://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
+4 -4
View File
@@ -7,7 +7,7 @@
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
@@ -57,10 +57,10 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# http://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
# You can find Gradle at http://github.com/gradle/gradle/.
#
##############################################################################
@@ -85,7 +85,7 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
# Discard cd standard output in case $CDPATH is set (http://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
+1 -1
View File
@@ -5,7 +5,7 @@
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
+1 -1
View File
@@ -24,7 +24,7 @@ target 'procure' do
)
post_install do |installer|
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
# http://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
react_native_post_install(
installer,
config[:reactNativePath],
+1 -1
View File
@@ -32,7 +32,7 @@
<true/>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string/>
<string>We need your location to securely bind this device for tender bidding verification.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
+18 -2
View File
@@ -1,3 +1,4 @@
const path = require("path");
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
const defaultConfig = getDefaultConfig(__dirname);
@@ -5,7 +6,7 @@ const { assetExts, sourceExts } = defaultConfig.resolver;
/**
* Metro configuration
* https://reactnative.dev/docs/metro
* http://reactnative.dev/docs/metro
*
* @type {import('metro-config').MetroConfig}
*/
@@ -17,7 +18,22 @@ const config = {
},
resolver: {
assetExts: assetExts.filter((ext) => ext !== "svg"),
sourceExts: [...sourceExts, "svg"]
sourceExts: [...sourceExts, "svg"],
blockList: [
// Exclude native Android build artifacts that are created/deleted
// during Gradle builds, which crash Metro's file watcher (ENOENT)
/node_modules\/.*\/android\/\.cxx\/.*/,
/node_modules\/.*\/android\/build\/.*/,
/node_modules\/.*\/android\/\.gradle\/.*/,
]
},
watcher: {
additionalExclusions: [
// Also exclude from the watcher layer for safety
"**/android/.cxx/**",
"**/android/build/**",
"**/android/.gradle/**",
]
}
};
+700 -772
View File
File diff suppressed because it is too large Load Diff
+14 -10
View File
@@ -7,16 +7,18 @@
"ios": "react-native run-ios",
"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",
Binary file not shown.
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env node
const axios = require('axios');
const { BASE_URL } = require('../src/Services/ApiCall/ApiUrls');
const REGISTER_URL =
`${BASE_URL}contractor/registerContractorStage1`;
const DESIGNATION_URL =
`${BASE_URL}contractor/getAllDesignationContractor`;
const DEFAULT_PASSWORD = 'P@ssword123';
function getArgValue(name, fallback) {
const prefix = `--${name}=`;
const arg = process.argv.find(item => item.startsWith(prefix));
if (!arg) return fallback;
return arg.slice(prefix.length);
}
function toPositiveInt(value, fallback) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) return fallback;
return parsed;
}
function buildMobile(seed, index) {
// Keep 10 digits and start with 9 to match app-side validation.
const dynamicPart = (seed * 1000 + index) % 999999999;
return String(9000000000 + dynamicPart).padStart(10, '9').slice(0, 10);
}
function toAlphabeticSuffix(index) {
let n = index;
let label = '';
while (n > 0) {
const rem = (n - 1) % 26;
label = String.fromCharCode(65 + rem) + label;
n = Math.floor((n - 1) / 26);
}
return label || 'A';
}
async function getDefaultDesignation() {
try {
const { data } = await axios.get(DESIGNATION_URL, { timeout: 15000 });
const list = data?.data;
if (Array.isArray(list) && list.length > 0) {
const firstId = Number(list[0]?.designationid);
if (Number.isFinite(firstId) && firstId > 0) return firstId;
}
} catch (error) {
const message = error?.response?.data?.message || error?.message || 'Unknown error';
console.warn(`Failed to fetch designation list, using fallback id=1. ${message}`);
}
return 1;
}
async function registerUser({
designation,
emailPrefix,
firstNamePrefix,
seed,
index,
password,
}) {
const serial = seed * 1000 + index;
const emailAdd = `${emailPrefix}${serial}@example.com`;
const alphaSuffix = toAlphabeticSuffix(index);
const payload = {
firstName: `${firstNamePrefix}${alphaSuffix}`,
middleName: '',
lastName: 'Auto',
dialCode: '+91',
mobNo: buildMobile(seed, index),
emailAdd,
contractorType: 'Indian',
designation,
password,
otherDesignation: null,
conPassword: password,
device: 'mobileApp',
};
try {
const { data } = await axios.post(REGISTER_URL, payload, { timeout: 20000 });
const status = String(data?.status ?? data?.statusCode ?? '');
const ok = status === '201' || status === '200' || status === 'SUCCESS';
return {
ok,
email: emailAdd,
mobile: payload.mobNo,
message: data?.message || (ok ? 'Success' : 'Unexpected response'),
rawStatus: status || 'unknown',
details: data,
};
} catch (error) {
const responseData = error?.response?.data;
const apiMessage =
responseData?.message ||
responseData?.error ||
error?.message ||
'Request failed';
return {
ok: false,
email: emailAdd,
mobile: payload.mobNo,
message: apiMessage,
rawStatus: String(error?.response?.status || 'network_error'),
details: responseData,
};
}
}
async function main() {
const count = toPositiveInt(getArgValue('count', '100'), 100);
const concurrency = toPositiveInt(getArgValue('concurrency', '5'), 5);
const startIndex = toPositiveInt(getArgValue('start', '1'), 1);
const password = getArgValue('password', DEFAULT_PASSWORD);
const emailPrefix = getArgValue('emailPrefix', 'bulkuser');
const firstNamePrefix = getArgValue('firstNamePrefix', 'BulkUser');
const seed = Number(String(Date.now()).slice(-6));
const designation = await getDefaultDesignation();
console.log(
`Creating ${count} users (start=${startIndex}, concurrency=${concurrency}, designation=${designation})...`,
);
const indexes = Array.from({ length: count }, (_, i) => i + startIndex);
let cursor = 0;
const results = [];
async function worker() {
while (true) {
const current = cursor;
cursor += 1;
if (current >= indexes.length) break;
const index = indexes[current];
const result = await registerUser({
designation,
emailPrefix,
firstNamePrefix,
seed,
index,
password,
});
results.push(result);
const marker = result.ok ? 'OK' : 'FAIL';
console.log(`[${marker}] #${index} ${result.email} -> ${result.message}`);
}
}
await Promise.all(Array.from({ length: concurrency }, () => worker()));
const success = results.filter(item => item.ok).length;
const failed = results.length - success;
console.log('\nSummary');
console.log(`Total: ${results.length}`);
console.log(`Success: ${success}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
console.log('\nFailed entries (up to 20):');
results
.filter(item => !item.ok)
.slice(0, 20)
.forEach(item => {
console.log(`- ${item.email} (${item.mobile}) [${item.rawStatus}] ${item.message}`);
if (item.details) {
console.log(` details: ${JSON.stringify(item.details)}`);
}
});
process.exitCode = 1;
}
}
main().catch(error => {
console.error('Bulk user creation crashed:', error?.message || error);
process.exit(1);
});
+1
View File
@@ -102,6 +102,7 @@ export const apiCall = async <R, D = {}>(
success: false,
message:
error?.response?.data?.message || 'No Response From Server',
...(error?.response?.data || {})
} as R;
}
};
+76 -51
View File
@@ -1,37 +1,8 @@
import React from 'react';
import { StyleSheet, Text, View, StyleProp, ViewStyle, TextStyle } from 'react-native';
import { Dropdown } from 'react-native-element-dropdown';
import { Shield } from 'lucide-react-native';
const colors = {
primary: '#F97316',
primaryLight: '#FDBA74',
primaryHighlight: '#f4f1efff',
secondary: '#1B4B66',
secondaryLight: '#639599',
success: '#22C55E',
error: '#EF4444',
warning: '#F59E0B',
info: '#3B82F6',
golden: '#FFD700',
white: '#FFFFFF',
black: '#000000',
darkText: '#2B2B2B',
grayText: '#6B7280',
lightText: '#9CA3AF',
border: '#E5E7EB',
lightGray: '#F9FAFB',
disabled: '#D1D5DB',
placeholder: '#9CA3AF',
backdrop: 'rgba(0,0,0,0.5)',
shadow: 'rgba(0,0,0,0.12)',
Orange: '#F97316',
OrangeLight: '#FDBA74',
OrangeSoft: '#FFF3E8',
darkGray: '#5f6368',
lightestGray: '#f1f3f4',
successLight: '#e6f4ea',
};
import { Shield, ChevronDown } from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
type DropdownItem = {
[key: string]: any;
@@ -74,9 +45,22 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
dropdownStyle,
...rest
}) => {
const { colors, spacing, typography, shadows } = useTheme();
return (
<View style={[{ zIndex: 4000 }, containerStyle]}>
{label && <Text style={styles.label}>{label}</Text>}
<View style={[{ zIndex: 4000, marginBottom: spacing.m }, containerStyle]}>
{label && (
<Text style={[styles.label, {
color: colors.text,
fontSize: typography.size.s,
fontWeight: '600',
letterSpacing: 0.5,
textTransform: 'uppercase',
marginBottom: spacing.xs
}]}>
{label}
</Text>
)}
<Dropdown
data={data}
labelField={labelField}
@@ -86,23 +70,70 @@ const MyDropdown: React.FC<MyDropdownProps> = ({
onChange={onChange}
style={[
styles.dropdown,
{
backgroundColor: colors.surface,
borderColor: focusedField ? colors.primary : colors.border,
borderWidth: 1.5,
borderRadius: 12,
paddingHorizontal: spacing.m,
paddingVertical: 12,
height: 56,
},
focusedField && shadows.light,
style,
{ borderColor: focusedField ? colors.primary : '#e0e0e0' },
]}
containerStyle={[styles.dropdownContainer, dropdownStyle]}
selectedTextStyle={[styles.selectedText, selectedTextStyle]}
placeholderStyle={[styles.placeholderText, placeholderStyle]}
containerStyle={[
styles.dropdownContainer,
{
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 12,
...shadows.medium,
},
dropdownStyle
]}
selectedTextStyle={[
styles.selectedText,
{
color: colors.text,
fontSize: typography.size.m,
fontWeight: '500'
},
selectedTextStyle
]}
placeholderStyle={[
styles.placeholderText,
{
color: colors.warmGray,
fontSize: typography.size.m
},
placeholderStyle
]}
itemTextStyle={{ color: colors.textSecondary }}
activeColor={colors.background} // Beige for active item bg
onFocus={onFocus}
onBlur={onBlur}
search
searchPlaceholder="Search Class"
searchPlaceholder="Search..."
inputSearchStyle={{
borderRadius: 8,
backgroundColor: colors.background,
borderColor: colors.border,
color: colors.text
}}
renderLeftIcon={() => (
<Shield
style={{ marginRight: 10 }}
color={focusedField ? colors.primary : '#aaa'}
color={focusedField ? colors.primary : colors.warmGray}
size={20}
/>
)}
renderRightIcon={() => (
<ChevronDown
size={20}
color={focusedField ? colors.primary : colors.warmGray}
/>
)}
{...rest}
/>
</View>
@@ -113,25 +144,19 @@ export default MyDropdown;
const styles = StyleSheet.create({
label: {
marginVertical: 10,
color: colors.darkText,
// marginVertical: 10, handled inline
},
dropdown: {
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 12,
borderColor: colors.border,
backgroundColor: colors.white,
// handled inline with theme
},
dropdownContainer: {
borderRadius: 12,
borderColor: colors.border,
// handled inline
marginTop: 4,
},
selectedText: {
color: colors.darkText,
// handled inline
},
placeholderText: {
color: colors.placeholder,
// handled inline
},
});
+22 -16
View File
@@ -2,10 +2,9 @@ import React from 'react';
import { View, Text, TouchableOpacity, Image, StyleSheet, Dimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ArrowLeft } from 'lucide-react-native';
import colors from '../../Constant/Color';
import { useTheme } from '../../Hooks/useTheme';
import ImagePath from '../../Constant/ImagePath';
import { useNavigation } from '@react-navigation/native';
import FontsFamily from '../../Constant/FontsFamily';
import NavigationString from '../../Constant/NavigationString';
const { width } = Dimensions.get('window');
@@ -18,6 +17,7 @@ interface HeaderProps {
const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
const navigation: any = useNavigation();
const insets = useSafeAreaInsets();
const { colors, spacing, typography, shadows } = useTheme();
return (
<View
@@ -25,6 +25,8 @@ const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
styles.container,
{
paddingTop: insets.top - 10,
backgroundColor: colors.background, // Off-white
...shadows.light, // Soft shadow
},
]}
>
@@ -36,13 +38,21 @@ const InsideHeader: React.FC<HeaderProps> = ({ title, showArrow = false }) => {
style={styles.backButton}
activeOpacity={0.7}
>
<ArrowLeft size={26} color={colors.black} strokeWidth={2.5} />
<ArrowLeft size={24} color={colors.primary} strokeWidth={2.5} />
</TouchableOpacity>
)}
<Text style={styles.title}>{title}</Text>
<Text style={[styles.title, {
color: colors.primary,
fontSize: typography.size.xl,
fontWeight: '600',
letterSpacing: 0.5
}]}>
{title}
</Text>
</View>
{/* Optional: Add Logo or Right Action */}
<TouchableOpacity onPress={() => { navigation.navigate(NavigationString.Home) }}>
<Image
source={ImagePath.Icon.Logo2}
@@ -60,25 +70,21 @@ export default InsideHeader;
const styles = StyleSheet.create({
container: {
width,
backgroundColor: colors.white, // IMPORTANT for shadow
shadowColor: '#000', // iOS shadow
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.12,
shadowRadius: 10,
elevation: 6,
zIndex: 10,
paddingBottom: 10,
},
row: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 10,
paddingHorizontal: 16,
},
leftRow: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
backButton: {
@@ -88,16 +94,16 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
// backgroundColor: 'rgba(0,0,0,0.03)' // Optional touch target bg
},
title: {
fontSize: 20,
color: colors.black,
fontFamily: FontsFamily.poppinsSemiBold,
flex: 1,
},
logo: {
height: 60,
width: 60,
height: 48, // Slightly smaller for cleaner look
width: 48,
opacity: 0.9,
},
});
@@ -1,5 +1,5 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView, StatusBar } from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView } from 'react-native';
import LottieView from 'lottie-react-native';
import LinearGradient from 'react-native-linear-gradient';
import { AlertCircle, Clock, RefreshCw, Activity } from 'lucide-react-native';
@@ -10,7 +10,6 @@ import FontsFamily from '../../Constant/FontsFamily';
const ServerDown = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<ScrollView
contentContainerStyle={styles.scrollContainer}
showsVerticalScrollIndicator={false}>
+4 -12
View File
@@ -1,4 +1,4 @@
import { StyleSheet, Text, TouchableOpacity, ViewStyle } from 'react-native';
import { StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
import React from 'react';
import 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>
);
};
+39 -34
View File
@@ -7,7 +7,7 @@ import Animated, {
withTiming,
interpolateColor,
} from "react-native-reanimated";
import colors from "../../Constant/Color";
import { useTheme } from "../../Hooks/useTheme";
export interface SearchType {
placeholder?: string;
@@ -15,7 +15,7 @@ export interface SearchType {
label?: string;
filter?: boolean;
filterPress?: () => void;
debounceDelay?: number; // 👈 optional
debounceDelay?: number;
}
const AnimatedView = Animated.createAnimatedComponent(View);
@@ -26,8 +26,9 @@ export const SearchInput: React.FC<SearchType> = ({
label,
filter = true,
filterPress,
debounceDelay = 500, // 👈 default debounce
debounceDelay = 500,
}) => {
const { colors, shadows } = useTheme();
const isFocused = useSharedValue(0);
const [text, setText] = useState('');
const debounceRef = useRef<NodeJS.Timeout | null>(null);
@@ -36,18 +37,17 @@ export const SearchInput: React.FC<SearchType> = ({
const borderColor = interpolateColor(
isFocused.value,
[0, 1],
[colors.grayText + '50', colors.grayText]
[colors.overlay, colors.primary] // Use theme overlay/primary
);
return {
borderColor,
borderWidth: withTiming(isFocused.value ? 1.5 : 1),
borderWidth: withTiming(isFocused.value ? 1 : 1), // Constant thin border
transform: [{ scale: withTiming(isFocused.value ? 1.01 : 1) }],
shadowOpacity: withTiming(isFocused.value ? 0.1 : 0),
};
});
// 🔥 Debounce logic
// Debounce logic
useEffect(() => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
@@ -66,34 +66,47 @@ export const SearchInput: React.FC<SearchType> = ({
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
{label && <Text style={[styles.label, { color: colors.text }]}>{label}</Text>}
<View style={styles.row}>
<AnimatedView style={[styles.inputWrapper, inputContainerStyle]}>
<AnimatedView style={[
styles.inputWrapper,
{
backgroundColor: colors.glass,
...shadows.light // Use theme shadow
},
inputContainerStyle
]}>
<Search
size={18}
color={colors.grayText}
color={colors.primary} // Theme primary
style={styles.searchIcon}
/>
<TextInput
style={styles.input}
style={[styles.input, { color: colors.text }]}
placeholder={placeholder}
value={text}
onChangeText={setText} // 👈 update local state only
placeholderTextColor={colors.grayText}
onChangeText={setText}
placeholderTextColor={colors.textSecondary}
onFocus={() => (isFocused.value = withTiming(1))}
onBlur={() => (isFocused.value = withTiming(0))}
selectionColor={colors.grayText}
selectionColor={colors.primary}
/>
</AnimatedView>
{filter && (
<TouchableOpacity
activeOpacity={0.7}
style={styles.filterButton}
style={[
styles.filterButton,
{
backgroundColor: colors.surface,
borderColor: colors.overlay
}
]}
onPress={filterPress}
>
<Filter size={22} color={colors.black} />
<Filter size={20} color={colors.primary} />
</TouchableOpacity>
)}
</View>
@@ -110,7 +123,6 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
color: colors.black,
letterSpacing: 0.3,
},
row: {
@@ -122,39 +134,32 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'row',
alignItems: 'center',
height: 48,
backgroundColor: '#F9FAFB',
borderRadius: 12,
paddingHorizontal: 12,
height: 50, // Slightly taller pill
borderRadius: 25, // Full Pill
paddingHorizontal: 16,
borderWidth: 1,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowRadius: 4,
elevation: 2,
},
searchIcon: {
marginRight: 8,
marginRight: 10,
},
input: {
flex: 1,
height: '100%',
fontSize: 15,
color: colors.black,
fontWeight: '500',
paddingVertical: Platform.OS === 'ios' ? 0 : 8,
},
filterButton: {
height: 48,
width: 48,
backgroundColor: '#FFFFFF',
height: 50,
width: 50,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 12,
borderRadius: 25, // Circle
borderWidth: 1,
borderColor: colors.grayText + '30',
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 2,
shadowRadius: 4,
},
});
+97 -21
View File
@@ -1,34 +1,110 @@
import { View, Dimensions } from 'react-native';
import React from 'react';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
import React, { useEffect, useRef } from 'react';
import { View, Animated, StyleSheet, Dimensions } from 'react-native';
import { useTheme } from '../../Hooks/useTheme';
const Skeleton = () => {
const { width } = Dimensions.get('screen');
const cardWidth = width - 30; // full width with some padding
// 1. Create a Reusable "Bone" Component
const SkeletonBone = ({ width, height, style, borderRadius = 8 }) => {
const { isDarkMode } = useTheme();
const opacity = useRef(new Animated.Value(0.3)).current;
// Animation Loop
useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 1000,
useNativeDriver: true,
}),
])
);
animation.start();
return () => animation.stop();
}, [opacity]);
// Dynamic colors based on theme
const boneColor = isDarkMode ? '#3E4145' : '#E1E9EE';
return (
<View style={{ padding: 10 }}>
<Animated.View
style={[
{
width: width,
height: height,
backgroundColor: boneColor,
borderRadius: borderRadius,
opacity: opacity,
},
style,
]}
/>
);
};
// 2. Main Skeleton Layout
const Skeleton = () => {
const { colors } = useTheme();
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
{[...Array(6)].map((_, index) => (
<SkeletonPlaceholder key={index}>
<SkeletonPlaceholder.Item
width={cardWidth}
marginBottom={20}
borderRadius={12}
overflow="hidden"
<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;
+105 -67
View File
@@ -1,12 +1,14 @@
import React, { useRef, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Animated,
} from 'react-native';
import colors from '../Constant/Color';
import React, { useMemo } from 'react';
import { View, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
import { useTheme } from '../Hooks/useTheme';
import AppText from '../components/AppText';
import { Check } from 'lucide-react-native';
type StepProgressBarProps = {
currentStep: number;
@@ -14,96 +16,132 @@ type StepProgressBarProps = {
onStepPress?: (stepIndex: number) => void;
};
const STEP_LABELS = ['Info', 'Register', 'KYC', 'Address', 'Docs'];
const StepProgressBar: React.FC<StepProgressBarProps> = ({
currentStep,
steps,
onStepPress = () => { },
}) => {
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;
+8 -1
View File
@@ -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;
+3 -1
View File
@@ -24,6 +24,8 @@ const NavigationString = {
ResetPassword:'ResetPassword',
KycWebViewScreen:'KycWebViewScreen',
StageThreeScreen:'StageThree',
DecryptionScreen:'DecryptionScreen'
DecryptionScreen:'DecryptionScreen',
DeviceBinding: 'DeviceBinding',
DscBinding: 'DscBinding'
}
export default NavigationString
+20
View File
@@ -0,0 +1,20 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useCreate = <T, V>(
url: string,
options?: UseMutationOptions<T, Error, V>
) => {
const queryClient = useQueryClient();
return useMutation<T, Error, V>({
mutationFn: async (variables: V) => {
const { data } = await apiClient.post<T>(url, variables);
return data;
},
onSuccess: () => {
// Invalidate queries if needed, handled by component or passed in options
},
...options,
});
};
+17
View File
@@ -0,0 +1,17 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useDelete = <T>(
url: string,
options?: UseMutationOptions<T, Error, void>
) => {
const queryClient = useQueryClient();
return useMutation<T, Error, void>({
mutationFn: async () => {
const { data } = await apiClient.delete<T>(url);
return data;
},
...options,
});
};
+17
View File
@@ -0,0 +1,17 @@
import { useQuery, UseQueryOptions, QueryKey } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useFetch = <T>(
key: QueryKey,
url: string,
options?: UseQueryOptions<T, Error>
) => {
return useQuery<T, Error>({
queryKey: key,
queryFn: async () => {
const { data } = await apiClient.get<T>(url);
return data;
},
...options,
});
};
+32
View File
@@ -0,0 +1,32 @@
import { useColorScheme } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { lightColors, darkColors, spacing, typography, shadows } from '../theme';
import { selectThemeMode, setThemeMode } from '../Redux/Slices/ThemeSlice';
export const useTheme = () => {
const systemScheme = useColorScheme();
const themeMode = useSelector(selectThemeMode);
const dispatch = useDispatch();
const isDarkMode =
themeMode === 'system'
? systemScheme === 'dark'
: themeMode === 'dark';
const themeColors = isDarkMode ? darkColors : lightColors;
const toggleTheme = (mode: 'light' | 'dark' | 'system') => {
dispatch(setThemeMode(mode));
};
return {
colors: themeColors,
isDarkMode,
themeMode,
toggleTheme,
spacing,
typography,
shadows,
};
};
+17
View File
@@ -0,0 +1,17 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
export const useUpdate = <T, V>(
url: string,
options?: UseMutationOptions<T, Error, V>
) => {
const queryClient = useQueryClient();
return useMutation<T, Error, V>({
mutationFn: async (variables: V) => {
const { data } = await apiClient.put<T>(url, variables);
return data;
},
...options,
});
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+156 -81
View File
@@ -1,124 +1,173 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect } from 'react';
import {
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
},
});
+100
View File
@@ -0,0 +1,100 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, Switch, Image } from 'react-native';
import { DrawerContentScrollView, DrawerItemList } from '@react-navigation/drawer';
import { useTheme } from '../Hooks/useTheme';
import AppText from '../components/AppText';
import { Moon, Sun, Monitor } from 'lucide-react-native';
import ImagePath from '../Constant/ImagePath';
const CustomDrawer = (props: any) => {
const { colors, isDarkMode, themeMode, toggleTheme } = useTheme();
const renderThemeOption = (mode: 'light' | 'dark' | 'system', label: string, Icon: any) => {
const isActive = themeMode === mode;
return (
<TouchableOpacity
style={[
styles.themeOption,
isActive && { backgroundColor: colors.primary + '20' } // 20% opacity
]}
onPress={() => toggleTheme(mode)}
>
<Icon size={20} color={isActive ? colors.primary : colors.textSecondary} />
<AppText variant="medium" style={{ marginLeft: 10, color: isActive ? colors.primary : colors.textSecondary }}>
{label}
</AppText>
</TouchableOpacity>
);
};
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<DrawerContentScrollView {...props} contentContainerStyle={{ paddingTop: 0 }}>
{/* Header */}
<View style={[styles.header, { backgroundColor: colors.surface, borderBottomColor: colors.border }]}>
<Image source={ImagePath.Icon.Logo} style={styles.logo} resizeMode="contain" />
<AppText variant="bold" size={20} style={{ color: colors.text }}>ProcureLinc</AppText>
<AppText variant="regular" size={12} style={{ color: colors.textSecondary }}>Smart Procurement</AppText>
</View>
<View style={{ flex: 1, paddingTop: 10 }}>
<DrawerItemList {...props} />
</View>
{/* Theme Switch Section */}
<View style={[styles.section, { borderTopColor: colors.border }]}>
<AppText variant="bold" style={{ marginLeft: 16, marginBottom: 10, color: colors.text }}>Running Theme</AppText>
<View style={styles.themeRow}>
{renderThemeOption('light', 'Light', Sun)}
{renderThemeOption('dark', 'Dark', Moon)}
{renderThemeOption('system', 'System', Monitor)}
</View>
</View>
</DrawerContentScrollView>
<View style={[styles.footer, { borderTopColor: colors.border }]}>
<AppText variant="regular" size={12} style={{ color: colors.textSecondary, textAlign: 'center' }}>
Version 1.0.0
</AppText>
</View>
</View>
);
};
const styles = StyleSheet.create({
header: {
padding: 20,
paddingTop: 50, // safe area approximation
alignItems: 'center',
borderBottomWidth: 1,
},
logo: {
width: 80,
height: 80,
borderRadius: 40,
marginBottom: 10,
},
section: {
padding: 10,
borderTopWidth: 1,
marginTop: 'auto',
marginBottom: 20
},
themeRow: {
flexDirection: 'row',
justifyContent: 'space-around',
},
themeOption: {
alignItems: 'center',
justifyContent: 'center',
padding: 10,
borderRadius: 12,
flexDirection: 'row'
},
footer: {
padding: 20,
borderTopWidth: 1,
}
});
export default CustomDrawer;
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import { createDrawerNavigator } from '@react-navigation/drawer';
import BottomTab from './Bottom';
import CustomDrawer from './CustomDrawer';
import { useTheme } from '../Hooks/useTheme';
import NavigationString from '../Constant/NavigationString';
import { Dimensions } from 'react-native';
const Drawer = createDrawerNavigator();
const { width } = Dimensions.get('window');
const DrawerStack = () => {
const { colors } = useTheme();
return (
<Drawer.Navigator
drawerContent={(props) => <CustomDrawer {...props} />}
screenOptions={{
headerShown: false,
drawerType: 'slide',
drawerStyle: {
backgroundColor: colors.background,
width: width * 0.75,
},
drawerActiveTintColor: colors.primary,
drawerInactiveTintColor: colors.textSecondary,
overlayColor: 'rgba(0,0,0,0.5)',
}}
>
<Drawer.Screen
name="MainTabs"
component={BottomTab}
options={{ title: 'Home' }}
/>
{/* Add other drawer screens here that are NOT in the bottom tab */}
</Drawer.Navigator>
);
};
export default DrawerStack;
+6 -19
View File
@@ -4,35 +4,20 @@ import NavigationString from '../Constant/NavigationString';
import Screens from '../Screens';
import 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>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type ThemeMode = 'light' | 'dark' | 'system';
interface ThemeState {
mode: ThemeMode;
}
const initialState: ThemeState = {
mode: 'system',
};
export const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
setThemeMode: (state, action: PayloadAction<ThemeMode>) => {
state.mode = action.payload;
},
},
});
export const { setThemeMode } = themeSlice.actions;
export const selectThemeMode = (state: any) => state.theme.mode;
export default themeSlice.reducer;
+1
View File
@@ -17,4 +17,5 @@ export const tokenSlice = createSlice({
},
});
export const { setToken, removeToken } = tokenSlice.actions;
export const getToken = (state: any) => state.token.token;
export default tokenSlice.reducer;
+3 -1
View File
@@ -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({
+203 -153
View File
@@ -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,
});
+112 -67
View File
@@ -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,
},
});
+231 -197
View File
@@ -1,13 +1,26 @@
import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import moment from 'moment';
import colors from '../../Constant/Color';
import { LiveAuctionTileProps } from './types/types';
import AuctionDetail from './Modal/AuctionDetail';
import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native';
import {
Tag,
Calendar,
Clock,
ShoppingCart,
Trash2,
CheckCircle2,
XCircle,
} from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import NavigationString from '../../Constant/NavigationString';
import { useNavigation } from '@react-navigation/native';
const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({
const LiveAuctionTile: React.FC<LiveAuctionTileProps & { index?: number }> = ({
data,
index = 0,
isPurchased = false,
onPurchasePress,
auctionDispatchDate,
@@ -15,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,
});
+320 -293
View File
@@ -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,
});
+67 -69
View File
@@ -1,38 +1,43 @@
import React, { memo, useState } from 'react';
import React, { memo, useState, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, Modal } from 'react-native';
import { 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);
+1 -1
View File
@@ -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
+374 -176
View File
@@ -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,
},
});
+221 -185
View File
@@ -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,
});
+318 -158
View File
@@ -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,
},
});
+50 -65
View File
@@ -1,9 +1,11 @@
import React, { memo, useCallback, useRef, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native';
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import colors from '../../../Constant/Color';
import colors_const from '../../../Constant/Color';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import { Loader } from '../../../Component/Index';
import { useSelector } from 'react-redux';
import axios from 'axios';
@@ -11,6 +13,8 @@ import Variables from '../../../Constant/Variable';
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
const { colors } = useTheme();
const styles = React.useMemo(() => makeStyles(colors), [colors]);
const Token = useSelector((state: any) => state.token.token)
const [showDetail, setShowDetail] = useState(false);
const [value, setValue] = useState(`${item.bidAmount}`);
@@ -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: {},
});
+178 -74
View File
@@ -1,105 +1,209 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import LottieView from 'lottie-react-native';
import Animated, { FadeInUp } from 'react-native-reanimated';
import colors from '../../../Constant/Color';
import Animated, { FadeInDown, FadeInUp, ZoomIn } from 'react-native-reanimated';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import {
ShieldCheck,
CheckCircle2,
FileText,
Gavel,
ArrowRight,
} from 'lucide-react-native';
const STEPS = [
{ icon: FileText, label: 'Documents Submitted', done: true },
{ icon: ShieldCheck, label: 'EMD Verified', done: true },
{ icon: Gavel, label: 'Ready to Bid', done: true },
];
const EmdComplete = ({ onNext }: { onNext?: () => void }) => {
return (
<View style={styles.container}>
<LottieView
source={require('../../../lottie/confetti.json')}
autoPlay
loop={true}
style={styles.confetti}
/>
const { colors, spacing, shadows } = useTheme();
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,
})
File diff suppressed because it is too large Load Diff
+233 -242
View File
@@ -1,52 +1,74 @@
import React, { useCallback, useEffect, useState } from 'react';
import { 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 },
});
+37 -88
View File
@@ -11,16 +11,21 @@ import { AUCTION_ONE_TIME_BID, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECT
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import 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 },
});
+142 -259
View File
@@ -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',
},
});
+18 -4
View File
@@ -32,13 +32,17 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
};
}, []);
const navigateToLogin = () => {
const handleNavigationBack = () => {
if (data.returnRoute) {
navigation.replace(data.returnRoute, data.returnParams);
} else {
navigation.dispatch(
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 />}
/>
)}
+2 -1
View File
@@ -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: {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -263,7 +263,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
try {
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: {
+143 -210
View File
@@ -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,
+107 -81
View File
@@ -3,23 +3,22 @@ import {
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
Platform,
} from 'react-native';
import React, { use, useState } from 'react';
import { useNavigation, useRoute, NavigationProp, RouteProp } from '@react-navigation/native';
import colors from '../../Constant/Color';
import React, { useMemo, useState } from 'react';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import MyInput from '../../Component/MyInput';
import MyInput from '../../components/MyInput';
import { showToast } from '../../Constant/ShowDailog';
import { PARTNER_API_ENDPOINT } from '../../Services/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,
},
});
+128 -109
View File
@@ -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,
},
});
+111 -73
View File
@@ -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')}
+272 -293
View File
@@ -1,9 +1,7 @@
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
@@ -13,16 +11,18 @@ import {
ScrollView,
ToastAndroid,
} from 'react-native';
import colors from '../../Constant/Color';
import { User, Phone, Mail, Lock, Eye, EyeOff, ArrowLeft, CheckSquare, Square } from 'lucide-react-native';
import { 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,
},
});
+29 -53
View File
@@ -1,9 +1,8 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { 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,
};
+194
View File
@@ -0,0 +1,194 @@
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { InsideHeader } from '../../Component/Index';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { useNavigation, useRoute } from '@react-navigation/native';
import { AlertCircle, CheckCircle } from 'lucide-react-native';
import DeviceInfo from 'react-native-device-info';
import { apiCall } from '../../Axios/Axios';
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
import Geolocation from 'react-native-geolocation-service';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Platform } from 'react-native';
import { clampRGBA } from 'react-native-reanimated/lib/typescript/Colors';
const DeviceBinding = () => {
const { colors, spacing, typography } = useTheme();
const navigation = useNavigation<any>();
const route = useRoute<any>();
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const { message, showBindButton } = route.params || {
message: "Device is not mapped or not active. Please register and activate your device.",
showBindButton: false
};
const getLocation = (): Promise<{ latitude: string, longitude: string }> => {
return new Promise(async (resolve) => {
try {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
const result = await request(permission);
if (result === RESULTS.GRANTED) {
Geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude.toString(),
longitude: position.coords.longitude.toString(),
});
},
(error) => {
console.log("Location Error: ", error);
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback to standard
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
} else {
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback
}
} catch (err) {
console.log("Permission Error: ", err);
resolve({ latitude: "23.77", longitude: "77.34" }); // Fallback
}
});
};
const handleBindDevice = async () => {
setLoading(true);
try {
const deviceId = await DeviceInfo.getUniqueId();
const systemName = DeviceInfo.getSystemName();
const systemVersion = DeviceInfo.getSystemVersion();
const deviceOs = `${systemName} ${systemVersion}`;
const lastIp = await DeviceInfo.getIpAddress();
const brand = DeviceInfo.getBrand();
const model = DeviceInfo.getModel();
const deviceName = `${brand} ${model}`;
const browser = "Mobile App";
const { latitude, longitude } = await getLocation();
const location = `${longitude},${latitude}`;
const payload = {
browser,
deviceOs,
deviceId,
lastIp,
deviceName,
location,
};
const response: any = await apiCall('post', `${BASE_URL}tenderBidding/deviceBinding`, payload);
if (response?.status === 'SUCCESS' || response?.statusCode === 200) {
setSuccess(true);
// Optionally navigate back after a delay
setTimeout(() => {
navigation.goBack();
}, 2000);
}
} catch (error) {
console.log("Error binding device: ", error);
} finally {
setLoading(false);
}
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
},
iconContainer: {
marginBottom: spacing.l,
},
title: {
marginBottom: spacing.m,
textAlign: 'center',
},
message: {
textAlign: 'center',
marginBottom: spacing.xl,
lineHeight: 24,
},
button: {
backgroundColor: colors.primary,
paddingVertical: spacing.m,
paddingHorizontal: spacing.xl,
borderRadius: 12,
width: '100%',
alignItems: 'center',
marginBottom: spacing.m,
},
secondaryButton: {
backgroundColor: 'transparent',
paddingVertical: spacing.m,
width: '100%',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
}
});
return (
<View style={styles.container}>
<InsideHeader title="Device Binding" showArrow />
<View style={styles.content}>
<View style={styles.iconContainer}>
{success ? (
<CheckCircle size={64} color={colors.success} />
) : (
<AlertCircle size={64} color={colors.error} />
)}
</View>
<AppText variant="h3" color={colors.text} style={styles.title}>
{success ? "Binding Successful" : "Device Not Bound"}
</AppText>
<AppText variant="body" color={colors.textSecondary} style={styles.message}>
{success ? "Your device binding request has been submitted successfully." : message}
</AppText>
{showBindButton && !success && (
<TouchableOpacity
style={styles.button}
onPress={handleBindDevice}
disabled={loading}
>
{loading ? (
<ActivityIndicator size="small" color={colors.buttonText} />
) : (
<AppText variant="button" color={colors.buttonText}>
Bind This Device
</AppText>
)}
</TouchableOpacity>
)}
<TouchableOpacity
style={success ? styles.button : styles.secondaryButton}
onPress={() => navigation.goBack()}
>
<AppText variant="button" color={success ? colors.buttonText : colors.text}>
{success ? "Finish" : "Go Back"}
</AppText>
</TouchableOpacity>
</View>
</View>
);
};
export default DeviceBinding;
+79
View File
@@ -0,0 +1,79 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import { InsideHeader } from '../../Component/Index';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import { useNavigation, useRoute } from '@react-navigation/native';
import { AlertCircle } from 'lucide-react-native';
const DscBinding = () => {
const { colors, spacing, typography } = useTheme();
const navigation = useNavigation<any>();
const route = useRoute<any>();
// Fallback message just in case
const message = route.params?.message || "DSC is not mapped. Please map your DSC on the web portal before proceeding.";
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
},
iconContainer: {
marginBottom: spacing.l,
},
title: {
marginBottom: spacing.m,
textAlign: 'center',
},
message: {
textAlign: 'center',
marginBottom: spacing.xl,
lineHeight: 24,
},
button: {
backgroundColor: colors.primary,
paddingVertical: spacing.m,
paddingHorizontal: spacing.xl,
borderRadius: 12,
width: '100%',
alignItems: 'center',
}
});
return (
<View style={styles.container}>
<InsideHeader title="DSC Binding" showArrow />
<View style={styles.content}>
<View style={styles.iconContainer}>
<AlertCircle size={64} color={colors.error} />
</View>
<AppText variant="h3" color={colors.text} style={styles.title}>
DSC Not Bound
</AppText>
<AppText variant="body" color={colors.textSecondary} style={styles.message}>
{message}
</AppText>
<TouchableOpacity
style={styles.button}
onPress={() => navigation.goBack()}
>
<AppText variant="button" color={colors.buttonText}>
Go Back
</AppText>
</TouchableOpacity>
</View>
</View>
);
};
export default DscBinding;
+32 -10
View File
@@ -1,18 +1,19 @@
import React, { useCallback, useState, useEffect } from 'react';
import { 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;
+216 -192
View File
@@ -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,
},
});
+149 -157
View File
@@ -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}>
+348 -415
View File
@@ -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,
},
}),
},
});
+366 -125
View File
@@ -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,
},
});
+1 -1
View File
@@ -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
+350 -163
View File
@@ -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,
},
});
+170 -175
View File
@@ -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;

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