Compare commits
136 Commits
a1ea60841e
...
sms-testin
| Author | SHA1 | Date | |
|---|---|---|---|
| 6861a4a349 | |||
| 298c0c199f | |||
| dfdc293309 | |||
| 1da7574ddb | |||
| 30a45015d0 | |||
| d8ebd0ed0e | |||
| 89569ab1c3 | |||
| 5c959ba15c | |||
| d89a4f5109 | |||
| 1c3a07bd66 | |||
| d44ee5590e | |||
| 715162b713 | |||
| 8149ef2a5b | |||
| 1a2dea611b | |||
| 72a2c56392 | |||
| aef82237ac | |||
| 974f42bf95 | |||
| 4a8c69bb1e | |||
| 86aaaa1f6d | |||
| 6796793aac | |||
| fbf6df7181 | |||
| c7111d518a | |||
| 5d307607fd | |||
| 992092052a | |||
| 64fedabd89 | |||
| 4fc6f54fcd | |||
| 8c7e94759a | |||
| 8aa5b170ca | |||
| 04a1ce26ec | |||
| b19bc2e222 | |||
| b9147b30d5 | |||
| 3358ec7669 | |||
| 18db360a45 | |||
| b7fe6a9d18 | |||
| adb9a5330b | |||
| 0075abc906 | |||
| 353ec63916 | |||
| 71b52cfb43 | |||
| c1df43e9b6 | |||
| f0d5233afc | |||
| 4fe6af4098 | |||
| fda5d075ff | |||
| 71e0521dec | |||
| f6e851a9ee | |||
| 547f534037 | |||
| 66b2e71140 | |||
| 43d92d799b | |||
| 3135116f26 | |||
| 39165d631e | |||
|
|
ef481ec879 | ||
|
|
36702b198f | ||
|
|
f0718e9d68 | ||
|
|
d2cce89efb | ||
| 8cfca113bf | |||
| d6f61ebb31 | |||
| 078e715d20 | |||
| 5c8df8ace3 | |||
| 3e88aad43f | |||
| 5b7f3f0096 | |||
| b5b6c6ed49 | |||
| c26cc507a1 | |||
| 87fd36b748 | |||
| 3417c4b0e5 | |||
| 151140d563 | |||
| a8ee7833be | |||
| f73faaa635 | |||
| 5ac977e903 | |||
| f15b8ac3f7 | |||
| 8f8fdb70e6 | |||
| d86ff2c427 | |||
| 527111c1de | |||
| dfbdb3238d | |||
| 3d13edf676 | |||
| 32e8b85cee | |||
| 58e53d0aeb | |||
| 06ef2ab36b | |||
| 0362bf2013 | |||
| 73b96b82f7 | |||
| c78a90dbfe | |||
| df025babd5 | |||
| 4d19bf6146 | |||
| d36cad31c1 | |||
| 39e7a02ca5 | |||
| 0c7470d74b | |||
| 32463680e8 | |||
| 00cb98ae83 | |||
| 18844495c2 | |||
| 3f7869677c | |||
| 0e4072fe8f | |||
| b5acae85d5 | |||
| cc7c7a8042 | |||
| 475f30a4bb | |||
| b00bc6c8c0 | |||
| c47862bd60 | |||
| 8ef4f1327f | |||
| 87e00d540f | |||
| 44a6307995 | |||
| f6b24e4c6f | |||
| 9cf4c44bb0 | |||
| b3b51d423d | |||
| b1f4d380c6 | |||
| 17ebf8626a | |||
| e466dff424 | |||
| 8f2b981b5b | |||
| ba1ef0ed24 | |||
| d2044d49b5 | |||
| db39cfbcc9 | |||
| bf23627e0a | |||
| eba38c2e42 | |||
|
|
d4bba6dc49 | ||
| b56bf0d7df | |||
| 82e057d804 | |||
| 0f205873a9 | |||
| 191610c9b2 | |||
| b03e917d78 | |||
|
|
188fbe9bb1 | ||
|
|
817f3d75f5 | ||
|
|
c42b973bee | ||
|
|
def009003c | ||
| 52e9f59e6f | |||
| e4e104837b | |||
| dd7b7a6f4c | |||
| c322d1d2fd | |||
| b3fb387bdd | |||
|
|
b62b8a157d | ||
| c729b775c9 | |||
| 465065db57 | |||
| 0a6dde9ead | |||
| b513664a47 | |||
| d8d87e8da4 | |||
| 1204507375 | |||
| be9a6fc93f | |||
| b8337d1152 | |||
| d3792a1a06 | |||
| 845ce1fe78 | |||
| 45dbf8464a |
5
.gitignore
vendored
@@ -44,3 +44,8 @@ app.*.map.json
|
|||||||
lib/l10n/app_localizations.dart
|
lib/l10n/app_localizations.dart
|
||||||
lib/l10n/app_localizations_en.dart
|
lib/l10n/app_localizations_en.dart
|
||||||
lib/l10n/app_localizations_hi.dart
|
lib/l10n/app_localizations_hi.dart
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
android/key.properties
|
||||||
|
android/*.jks
|
||||||
|
android/*.keystore
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
analyzer:
|
analyzer:
|
||||||
errors:
|
errors:
|
||||||
dead_code: ignore
|
dead_code: ignore
|
||||||
|
non_constant_identifier_names: ignore
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
|
|||||||
@@ -22,12 +22,19 @@ if (flutterVersionName == null) {
|
|||||||
flutterVersionName = '1.0'
|
flutterVersionName = '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def keystoreProperties = new Properties()
|
||||||
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.example.kmobile"
|
namespace "com.example.kmobile"
|
||||||
compileSdk flutter.compileSdkVersion
|
compileSdk flutter.compileSdkVersion
|
||||||
ndkVersion "27.0.12077973"
|
ndkVersion "27.0.12077973"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@@ -51,15 +58,21 @@ android {
|
|||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig signingConfigs.release
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,4 +81,6 @@ flutter {
|
|||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {}
|
dependencies {
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.SEND_SMS"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||||
<application
|
<application
|
||||||
android:label="kmobile"
|
android:label="kmobile"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@@ -40,6 +42,20 @@
|
|||||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.SENDTO" />
|
||||||
|
<data android:scheme="mailto" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
package com.example.kmobile
|
package com.example.kmobile
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import android.view.WindowManager.LayoutParams
|
||||||
|
import android.os.Bundle
|
||||||
|
|
||||||
class MainActivity: FlutterFragmentActivity()
|
class MainActivity: FlutterFragmentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
window.addFlags(LayoutParams.FLAG_SECURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
android/app/src/main/res/drawable/notification_icon.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 26 KiB |
BIN
assets/images/ipos_logo.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 90 KiB |
BIN
assets/images/logo_2.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/images/profile.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
assets/images/profile.svg
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
assets/images/uco_logo.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/images/yes_bank_logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
flutter_01.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -427,7 +427,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -484,7 +484,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 21 KiB |
@@ -47,5 +47,19 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>We use Face ID to secure your data.</string>
|
<string>We use Face ID to secure your data.</string>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>https</string>
|
||||||
|
<string>http</string>
|
||||||
|
</array>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>This app needs camera access to scan QR codes and take pictures.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>This app needs photo library access to save and share images.</string>
|
||||||
|
<key>NSContactsUsageDescription</key>
|
||||||
|
<string>This app needs contacts access to easily send money to your contacts.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>This app needs microphone access for voice commands.</string>
|
||||||
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -8,6 +8,41 @@ class AuthService {
|
|||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
AuthService(this._dio);
|
AuthService(this._dio);
|
||||||
|
|
||||||
|
Future<void> simVerify(String uuid, String cifNo) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.post('/api/sim-details-verify', data: {
|
||||||
|
'uuid': uuid,
|
||||||
|
'cifNo': cifNo,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final String message = response.data.toString().toUpperCase();
|
||||||
|
|
||||||
|
if (message.contains("VERIFIED")) {
|
||||||
|
return; // Success
|
||||||
|
} else {
|
||||||
|
throw AuthException(message); // Throw message received
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw AuthException('Verification Failed');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.response?.statusCode == 401) {
|
||||||
|
throw AuthException(
|
||||||
|
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw NetworkException('Network error during verification');
|
||||||
|
} catch (e) {
|
||||||
|
throw UnexpectedException(
|
||||||
|
'Unexpected error during verification: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<AuthToken> login(AuthCredentials credentials) async {
|
Future<AuthToken> login(AuthCredentials credentials) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
@@ -25,7 +60,8 @@ class AuthService {
|
|||||||
print(e.toString());
|
print(e.toString());
|
||||||
}
|
}
|
||||||
if (e.response?.statusCode == 401) {
|
if (e.response?.statusCode == 401) {
|
||||||
throw AuthException('Invalid credentials');
|
throw AuthException(
|
||||||
|
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
|
||||||
}
|
}
|
||||||
throw NetworkException('Network error during login');
|
throw NetworkException('Network error during login');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -91,4 +127,72 @@ class AuthService {
|
|||||||
'Unexpected error during TPIN setup: ${e.toString()}');
|
'Unexpected error during TPIN setup: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> sendOtpForSettingPassword(String customerNo) async {
|
||||||
|
try {
|
||||||
|
final response =
|
||||||
|
await _dio.get('/api/otp/send/set-password', queryParameters: {
|
||||||
|
'customerNo': customerNo,
|
||||||
|
});
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception(response.data['error'] ?? 'Failed to send OTP');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw Exception('Network error: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> verifyOtpForSettingPassword(
|
||||||
|
String customerNo, String otp) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(
|
||||||
|
'/api/otp/verify/set-password',
|
||||||
|
queryParameters: {'customerNo': customerNo, 'otp': otp},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return response.data['token'];
|
||||||
|
} else {
|
||||||
|
throw Exception(response.data['error'] ?? 'Failed to verify OTP');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw Exception('Network error: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future changePassword(String newPassword, String token) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/auth/login_password',
|
||||||
|
data: {'login_password': newPassword},
|
||||||
|
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Error setting password');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future setTncflag() async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/auth/tnc',
|
||||||
|
data: {"flag": 'Y'},
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw AuthException('Failed to proceed with T&C');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e.toString());
|
||||||
|
}
|
||||||
|
throw NetworkException('Network error during T&C Setup');
|
||||||
|
} catch (e) {
|
||||||
|
throw UnexpectedException('Unexpected error: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:kmobile/core/errors/exceptions.dart';
|
||||||
import 'package:kmobile/data/models/ifsc.dart';
|
import 'package:kmobile/data/models/ifsc.dart';
|
||||||
import 'package:kmobile/data/models/beneficiary.dart';
|
import 'package:kmobile/data/models/beneficiary.dart';
|
||||||
|
|
||||||
@@ -39,9 +40,12 @@ class BeneficiaryService {
|
|||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.response?.statusCode == 404) {
|
if (e.response?.statusCode == 404) {
|
||||||
throw Exception('INVALID IFSC CODE');
|
throw Exception('INVALID IFSC CODE');
|
||||||
|
} else if (e.response?.statusCode == 401) {
|
||||||
|
throw Exception('INVALID IFSC CODE');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rethrow;
|
throw UnexpectedException(
|
||||||
|
'Unexpected error during login: ${e.toString()}');
|
||||||
}
|
}
|
||||||
return Ifsc.fromJson({});
|
return Ifsc.fromJson({});
|
||||||
}
|
}
|
||||||
@@ -59,6 +63,10 @@ class BeneficiaryService {
|
|||||||
'ifscCode': ifscCode,
|
'ifscCode': ifscCode,
|
||||||
'remitterName': remitterName,
|
'remitterName': remitterName,
|
||||||
},
|
},
|
||||||
|
options: Options(
|
||||||
|
sendTimeout: const Duration(seconds: 60),
|
||||||
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception("Invalid Beneficiary Details");
|
throw Exception("Invalid Beneficiary Details");
|
||||||
@@ -66,7 +74,7 @@ class BeneficiaryService {
|
|||||||
return response.data['name'];
|
return response.data['name'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Data for Validation
|
// Beneficiary Validate And ADD
|
||||||
Future<bool> sendForValidation(Beneficiary beneficiary) async {
|
Future<bool> sendForValidation(Beneficiary beneficiary) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
@@ -118,4 +126,27 @@ class BeneficiaryService {
|
|||||||
throw Exception('Unexpected error: ${e.toString()}');
|
throw Exception('Unexpected error: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Response> updateLimit({
|
||||||
|
required String beneficiaryAccountNo,
|
||||||
|
required String newLimit,
|
||||||
|
}) async {
|
||||||
|
log('inside update limit of beneficiary service');
|
||||||
|
final response = await _dio.patch(
|
||||||
|
'/api/beneficiary/update-limit',
|
||||||
|
data: {
|
||||||
|
'beneficiaryAccountNo': beneficiaryAccountNo,
|
||||||
|
'newLimit': int.tryParse(newLimit),
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
sendTimeout: const Duration(seconds: 60),
|
||||||
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("INTERNAL SERVER ERROR");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
136
lib/api/services/branch_service.dart
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
class Branch {
|
||||||
|
final String branch_code;
|
||||||
|
final String branch_name;
|
||||||
|
final String zone;
|
||||||
|
final String tehsil;
|
||||||
|
final String block;
|
||||||
|
final String block_code;
|
||||||
|
final String distt_name;
|
||||||
|
final String distt_code_slbc;
|
||||||
|
final String date_of_opening;
|
||||||
|
final String rbi_code_1;
|
||||||
|
final String rbi_code_2;
|
||||||
|
final String telephone_no;
|
||||||
|
final String type_of_branch;
|
||||||
|
final String rtgs_acct_no;
|
||||||
|
final String br_lattitude;
|
||||||
|
final String br_longitude;
|
||||||
|
final String pincode;
|
||||||
|
final String post_office;
|
||||||
|
|
||||||
|
Branch({
|
||||||
|
required this.branch_code,
|
||||||
|
required this.branch_name,
|
||||||
|
required this.zone,
|
||||||
|
required this.tehsil,
|
||||||
|
required this.block,
|
||||||
|
required this.block_code,
|
||||||
|
required this.distt_name,
|
||||||
|
required this.distt_code_slbc,
|
||||||
|
required this.date_of_opening,
|
||||||
|
required this.rbi_code_1,
|
||||||
|
required this.rbi_code_2,
|
||||||
|
required this.telephone_no,
|
||||||
|
required this.type_of_branch,
|
||||||
|
required this.rtgs_acct_no,
|
||||||
|
required this.br_lattitude,
|
||||||
|
required this.br_longitude,
|
||||||
|
required this.pincode,
|
||||||
|
required this.post_office,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Branch.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Branch(
|
||||||
|
branch_code: json['branch_code'] ?? json['branch_code'] ?? '',
|
||||||
|
branch_name: json['branch_name'] ?? json['branch_name'] ?? '',
|
||||||
|
zone: json['zone'] ?? json['zone'] ?? '',
|
||||||
|
tehsil: json['tehsil'] ?? json['tehsil'] ?? '',
|
||||||
|
block: json['block'] ?? json['block'] ?? '',
|
||||||
|
block_code: json['block_code'] ?? json['block_code'] ?? '',
|
||||||
|
distt_name: json['distt_name'] ?? json['distt_name'] ?? '',
|
||||||
|
distt_code_slbc: json['distt_code_slbc'] ?? json['distt_code_slbc'] ?? '',
|
||||||
|
date_of_opening: json['date_of_opening'] ?? json['date_of_opening'] ?? '',
|
||||||
|
rbi_code_1: json['rbi_code_1'] ?? json['rbi_code_1'] ?? '',
|
||||||
|
rbi_code_2: json['rbi_code_2'] ?? json['rbi_code_2'] ?? '',
|
||||||
|
telephone_no: json['telephone_no'] ?? json['telephone_no'] ?? '',
|
||||||
|
type_of_branch: json['type_of_branch'] ?? json['type_of_branch'] ?? '',
|
||||||
|
rtgs_acct_no: json['rtgs_acct_no'] ?? json['rtgs_acct_no'] ?? '',
|
||||||
|
br_lattitude: json['br_lattitude'] ?? json['br_lattitude'] ?? '',
|
||||||
|
br_longitude: json['br_longitude'] ?? json['br_longitude'] ?? '',
|
||||||
|
pincode: json['pincode'] ?? json['pincode'] ?? '',
|
||||||
|
post_office: json['post_office'] ?? json['post_office'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Branch> listFromJson(List<dynamic> jsonList) {
|
||||||
|
final beneficiaryList =
|
||||||
|
jsonList.map((beneficiary) => Branch.fromJson(beneficiary)).toList();
|
||||||
|
return beneficiaryList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Atm {
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
Atm({required this.name});
|
||||||
|
|
||||||
|
factory Atm.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Atm(
|
||||||
|
name: json['name'] ?? '', // Assuming the API returns a 'name' field
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Atm> listFromJson(List<dynamic> jsonList) {
|
||||||
|
return jsonList.map((atm) => Atm.fromJson(atm)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BranchService {
|
||||||
|
final Dio _dio;
|
||||||
|
BranchService(this._dio);
|
||||||
|
|
||||||
|
Future<List<Branch>> fetchBranchList() async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(
|
||||||
|
"/api/branch",
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return Branch.listFromJson(response.data);
|
||||||
|
} else {
|
||||||
|
throw Exception("Failed to fetch beneficiaries");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Atm>> fetchAtmList() async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(
|
||||||
|
"/api/atm",
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return Atm.listFromJson(response.data);
|
||||||
|
} else {
|
||||||
|
throw Exception("Failed to fetch ATM list: ${response.statusCode}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// You might want to log the error here for debugging
|
||||||
|
print("Error fetching ATM list: $e");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/api/services/change_password_service.dart
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
class ChangePasswordService {
|
||||||
|
final Dio _dio;
|
||||||
|
|
||||||
|
ChangePasswordService(this._dio);
|
||||||
|
|
||||||
|
Future getOtp({
|
||||||
|
required String mobileNumber,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/otp/send',
|
||||||
|
data: {'mobileNumber': mobileNumber, 'type': "CHANGE_LPWORD"},
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Invalid Mobile Number/Type");
|
||||||
|
}
|
||||||
|
print(response.toString());
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future getOtpTpin({
|
||||||
|
required String mobileNumber,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/otp/send',
|
||||||
|
data: {'mobileNumber': mobileNumber, 'type': "CHANGE_TPIN"},
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Invalid Mobile Number/Type");
|
||||||
|
}
|
||||||
|
print(response.toString());
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future validateOtp({
|
||||||
|
required String otp,
|
||||||
|
required String mobileNumber,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/otp/verify?mobileNumber=$mobileNumber',
|
||||||
|
data: {
|
||||||
|
'otp': otp,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Wrong OTP");
|
||||||
|
}
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future validateChangePwd({
|
||||||
|
required String OldLPsw,
|
||||||
|
required String newLPsw,
|
||||||
|
required String confirmLPsw,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/auth/change/login_password',
|
||||||
|
data: {
|
||||||
|
'OldLPsw': OldLPsw,
|
||||||
|
'newLPsw': newLPsw,
|
||||||
|
'confirmLPsw': confirmLPsw,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Wrong OTP");
|
||||||
|
}
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future validateChangeTpin({
|
||||||
|
required String oldTpin,
|
||||||
|
required String newTpin,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/auth/change/tpin',
|
||||||
|
data: {
|
||||||
|
'oldTpin': oldTpin,
|
||||||
|
'newTpin': newTpin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Wrong OTP");
|
||||||
|
}
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
191
lib/api/services/cheque_service.dart
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
class Cheque {
|
||||||
|
final String? type;
|
||||||
|
final String? InstrType;
|
||||||
|
final String? Date;
|
||||||
|
final String? branchCode;
|
||||||
|
final String? fromCheque;
|
||||||
|
final String? toCheque;
|
||||||
|
final String? Chequescount;
|
||||||
|
final String? ChequeNumber;
|
||||||
|
final String? transactionCode;
|
||||||
|
final int? amount;
|
||||||
|
final String? status;
|
||||||
|
final String? stopIssueDate;
|
||||||
|
final String? StopExpiryDate;
|
||||||
|
|
||||||
|
Cheque({
|
||||||
|
this.type,
|
||||||
|
this.InstrType,
|
||||||
|
this.Date,
|
||||||
|
this.branchCode,
|
||||||
|
this.fromCheque,
|
||||||
|
this.toCheque,
|
||||||
|
this.Chequescount,
|
||||||
|
this.ChequeNumber,
|
||||||
|
this.transactionCode,
|
||||||
|
this.amount,
|
||||||
|
this.status,
|
||||||
|
this.stopIssueDate,
|
||||||
|
this.StopExpiryDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Cheque.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Cheque(
|
||||||
|
type: json['type'] ?? '',
|
||||||
|
InstrType: json['InstrType'] ?? '',
|
||||||
|
Date: json['Date'] ?? '',
|
||||||
|
branchCode: json['branchCode'] ?? '',
|
||||||
|
fromCheque: json['fromCheque'] ?? '',
|
||||||
|
toCheque: json['toCheque'] ?? '',
|
||||||
|
Chequescount: json['Chequescount'] ?? '',
|
||||||
|
ChequeNumber: json['ChequeNumber'] ?? '',
|
||||||
|
transactionCode: json['transactionCode'] ?? '',
|
||||||
|
amount: json['amount'],
|
||||||
|
status: json['status'] ?? '',
|
||||||
|
stopIssueDate: json['stopIssueDate'] ?? '',
|
||||||
|
StopExpiryDate: json['StopExpiryDate'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Cheque> listFromJson(List<dynamic> jsonList) {
|
||||||
|
final chequeList =
|
||||||
|
jsonList.map((cheque) => Cheque.fromJson(cheque)).toList();
|
||||||
|
return chequeList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChequeService {
|
||||||
|
final Dio _dio;
|
||||||
|
ChequeService(this._dio);
|
||||||
|
|
||||||
|
Future<List<Cheque>> ChequeEnquiry({
|
||||||
|
required String accountNumber,
|
||||||
|
required String instrType,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(
|
||||||
|
"/api/cheque/enquiry",
|
||||||
|
queryParameters: {
|
||||||
|
'accountNumber': accountNumber,
|
||||||
|
'instrumentType': instrType,
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
if (response.data is Map<String, dynamic> &&
|
||||||
|
response.data.containsKey('records')) {
|
||||||
|
final records = response.data['records'];
|
||||||
|
if (records is List) {
|
||||||
|
return Cheque.listFromJson(records);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Exception(
|
||||||
|
"Unexpected API response format: 'records' list not found or malformed");
|
||||||
|
} else {
|
||||||
|
throw Exception("Failed to fetch");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error in ChequeEnquiry: $e');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future stopCheque({
|
||||||
|
required String accountno,
|
||||||
|
required String stopFromChequeNo,
|
||||||
|
required String instrType,
|
||||||
|
String? stopToChequeNo,
|
||||||
|
String? stopIssueDate,
|
||||||
|
String? stopExpiryDate,
|
||||||
|
String? stopAmount,
|
||||||
|
String? stopComment,
|
||||||
|
String? chequeIssueDate,
|
||||||
|
required String tpin,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/cheque/stop',
|
||||||
|
options: Options(
|
||||||
|
validateStatus: (int? status) => true,
|
||||||
|
receiveDataWhenStatusError: true,
|
||||||
|
),
|
||||||
|
data: {
|
||||||
|
'accountNumber': accountno,
|
||||||
|
'stopFromChequeNo': stopFromChequeNo,
|
||||||
|
'instrumentType': instrType,
|
||||||
|
'stopToChequeNo': stopToChequeNo,
|
||||||
|
'stopIssueDate': stopIssueDate,
|
||||||
|
'stopExpiryDate': stopExpiryDate,
|
||||||
|
'stopAmount': stopAmount,
|
||||||
|
'stopComment': stopComment,
|
||||||
|
'chqIssueDate': chequeIssueDate,
|
||||||
|
'tpin': tpin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future revokeStop({
|
||||||
|
required String accountno,
|
||||||
|
required String removeFromChequeNo,
|
||||||
|
required String instrType,
|
||||||
|
String? removeToChequeNo,
|
||||||
|
String? removeIssueDate,
|
||||||
|
String? removeExpiryDate,
|
||||||
|
String? removeAmount,
|
||||||
|
String? removeComment,
|
||||||
|
required String tpin,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/cheque/removeStop',
|
||||||
|
options: Options(
|
||||||
|
validateStatus: (int? status) => true,
|
||||||
|
receiveDataWhenStatusError: true,
|
||||||
|
),
|
||||||
|
data: {
|
||||||
|
'accountNumber': accountno,
|
||||||
|
'removeFromChequeNo': removeFromChequeNo,
|
||||||
|
'instrumentType': instrType,
|
||||||
|
'removeToChequeNo': removeToChequeNo,
|
||||||
|
'removeIssueDate': removeIssueDate,
|
||||||
|
'removeExpiryDate': removeExpiryDate,
|
||||||
|
'removeAmount': removeAmount,
|
||||||
|
'removeComment': removeComment,
|
||||||
|
'tpin': tpin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future registerPPS({
|
||||||
|
required String cheque_no,
|
||||||
|
required String account_number,
|
||||||
|
String? issue_date,
|
||||||
|
String? amount,
|
||||||
|
String? payee_name,
|
||||||
|
required String tpin,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/pps',
|
||||||
|
options: Options(
|
||||||
|
validateStatus: (int? status) => true,
|
||||||
|
receiveDataWhenStatusError: true,
|
||||||
|
),
|
||||||
|
data: {
|
||||||
|
'cheque_no': cheque_no,
|
||||||
|
'account_number': account_number,
|
||||||
|
'issue_date': issue_date,
|
||||||
|
'amount': amount,
|
||||||
|
'payee_name': payee_name,
|
||||||
|
'tpin': tpin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/api/services/limit_service.dart
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// ignore_for_file: collection_methods_unrelated_type
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
class Limit {
|
||||||
|
final double dailyLimit;
|
||||||
|
final double usedLimit;
|
||||||
|
|
||||||
|
Limit({
|
||||||
|
required this.dailyLimit,
|
||||||
|
required this.usedLimit,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Limit.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Limit(
|
||||||
|
dailyLimit: (json['dailyLimit'] as num).toDouble(),
|
||||||
|
usedLimit: (json['usedLimit'] as num).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LimitService {
|
||||||
|
final Dio _dio;
|
||||||
|
LimitService(this._dio);
|
||||||
|
|
||||||
|
Future<Limit> getLimit() async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get('/api/customer/daily-limit');
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
log('Response: ${response.data}');
|
||||||
|
return Limit.fromJson(response.data);
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to load');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw Exception('Network error: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void editLimit(double newLimit) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.post('/api/customer/daily-limit',
|
||||||
|
data: '{"amount": $newLimit}');
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
log('Response: ${response.data}');
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to load');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw Exception('Network error: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future getOtpTLimit({
|
||||||
|
required String mobileNumber,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/otp/send',
|
||||||
|
data: {'mobileNumber': mobileNumber, 'type': "TLIMIT"},
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Invalid Mobile Number/Type");
|
||||||
|
}
|
||||||
|
print(response.toString());
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future validateOtp({
|
||||||
|
required String otp,
|
||||||
|
required String mobileNumber,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/otp/verify?mobileNumber=$mobileNumber',
|
||||||
|
data: {
|
||||||
|
'otp': otp,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Wrong OTP");
|
||||||
|
}
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
102
lib/api/services/send_sms_service.dart
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:send_message/send_message.dart' show sendSMS;
|
||||||
|
import 'package:simcards/sim_card.dart';
|
||||||
|
import 'package:simcards/simcards.dart';
|
||||||
|
|
||||||
|
// This enum provides detailed status back to the UI layer.
|
||||||
|
enum PermissionStatusResult { granted, denied, permanentlyDenied, restricted }
|
||||||
|
|
||||||
|
class SmsService {
|
||||||
|
final Simcards _simcards = Simcards();
|
||||||
|
|
||||||
|
/// Handles the requesting of SMS and Phone permissions.
|
||||||
|
/// Returns a detailed status: granted, denied, or permanentlyDenied.
|
||||||
|
Future<PermissionStatusResult> handleSmsPermission() async {
|
||||||
|
var smsStatus = await Permission.sms.status;
|
||||||
|
var phoneStatus = await Permission.phone.status;
|
||||||
|
|
||||||
|
// Check initial status
|
||||||
|
if (smsStatus.isGranted && phoneStatus.isGranted) {
|
||||||
|
return PermissionStatusResult.granted;
|
||||||
|
}
|
||||||
|
if (smsStatus.isPermanentlyDenied || phoneStatus.isPermanentlyDenied) {
|
||||||
|
return PermissionStatusResult.permanentlyDenied;
|
||||||
|
}
|
||||||
|
if (smsStatus.isRestricted || phoneStatus.isRestricted) {
|
||||||
|
return PermissionStatusResult.restricted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request permissions if not granted
|
||||||
|
print("Requesting SMS and Phone permissions...");
|
||||||
|
await [Permission.phone, Permission.sms].request();
|
||||||
|
|
||||||
|
// Re-check status after request
|
||||||
|
smsStatus = await Permission.sms.status;
|
||||||
|
phoneStatus = await Permission.phone.status;
|
||||||
|
|
||||||
|
if (smsStatus.isGranted && phoneStatus.isGranted) {
|
||||||
|
return PermissionStatusResult.granted;
|
||||||
|
}
|
||||||
|
if (smsStatus.isPermanentlyDenied || phoneStatus.isPermanentlyDenied) {
|
||||||
|
return PermissionStatusResult.permanentlyDenied;
|
||||||
|
}
|
||||||
|
if (smsStatus.isRestricted || phoneStatus.isRestricted) {
|
||||||
|
return PermissionStatusResult.restricted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If none of the above, it's denied
|
||||||
|
return PermissionStatusResult.denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to send a single verification SMS.
|
||||||
|
/// This should only be called AFTER permissions have been granted.
|
||||||
|
Future<bool> sendVerificationSms({
|
||||||
|
required BuildContext context,
|
||||||
|
required String destinationNumber,
|
||||||
|
required String message,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
List<SimCard> simCardList = await _simcards.getSimCards();
|
||||||
|
if (simCardList.isEmpty) {
|
||||||
|
print("No SIM card detected.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await _sendSms(destinationNumber, message, simCardList.first);
|
||||||
|
} catch (e) {
|
||||||
|
print("An error occurred in the SMS process: $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Private function to perform the SMS sending action.
|
||||||
|
Future<bool> _sendSms(
|
||||||
|
String destinationNumber, String message, SimCard selectedSim) async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
try {
|
||||||
|
String smsMessage = message;
|
||||||
|
String result = await sendSMS(
|
||||||
|
message: smsMessage,
|
||||||
|
recipients: [destinationNumber],
|
||||||
|
sendDirect: true,
|
||||||
|
);
|
||||||
|
print("Background SMS send attempt result: $result");
|
||||||
|
|
||||||
|
if (result.toLowerCase().contains('sent')) {
|
||||||
|
print("Success: SMS appears to have been sent.");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
print("Failure: SMS was not sent. Result: $result");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Error attempting to send SMS directly: $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("SMS sending is only supported on Android.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ class UserService {
|
|||||||
|
|
||||||
Future<List<User>> getUserDetails() async {
|
Future<List<User>> getUserDetails() async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get('/api/customer/details');
|
final response = await _dio.get('/api/customer');
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
log('Response: ${response.data}');
|
log('Response: ${response.data}');
|
||||||
return (response.data as List)
|
return (response.data as List)
|
||||||
|
|||||||
213
lib/api/services/yojna_service.dart
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class YojnaService {
|
||||||
|
final Dio _dio;
|
||||||
|
|
||||||
|
YojnaService(this._dio);
|
||||||
|
|
||||||
|
Future<dynamic> fetchpmydetails({
|
||||||
|
required String scheme,
|
||||||
|
required String action,
|
||||||
|
required String accountno,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.post(
|
||||||
|
"/api/gov-scheme/req/PMJBY",
|
||||||
|
data: {
|
||||||
|
'scheme': scheme,
|
||||||
|
'action': action,
|
||||||
|
'accountNo': accountno,
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
log("PMY Details Response: ${response.data}");
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw Exception("INTERNAL SERVER ERROR");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log("Error fetching PMY details: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future secondvalidationPMJJBY({
|
||||||
|
String? aadharno,
|
||||||
|
String? accountno,
|
||||||
|
String? availablebalance,
|
||||||
|
String? country,
|
||||||
|
String? customerdob,
|
||||||
|
String? customername,
|
||||||
|
String? customerno,
|
||||||
|
String? dateofacctopening,
|
||||||
|
String? emailid,
|
||||||
|
String? financialyear,
|
||||||
|
String? gender,
|
||||||
|
String? ifsccode,
|
||||||
|
String? married,
|
||||||
|
String? mobileno,
|
||||||
|
String? pan,
|
||||||
|
String? pincode,
|
||||||
|
String? policynumber,
|
||||||
|
String? premiumamount,
|
||||||
|
String? state,
|
||||||
|
String? healthstatus,
|
||||||
|
String? collectionchannel,
|
||||||
|
String? nomineename,
|
||||||
|
String? nomineeaddress,
|
||||||
|
String? nomineerelationship,
|
||||||
|
String? nomineeminor,
|
||||||
|
String? ruralcategory,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/gov-scheme/create/PMJBY',
|
||||||
|
options: Options(
|
||||||
|
validateStatus: (int? status) => true,
|
||||||
|
receiveDataWhenStatusError: true,
|
||||||
|
),
|
||||||
|
data: {
|
||||||
|
'aadharno': aadharno ,
|
||||||
|
'accountno': accountno,
|
||||||
|
'availablebalance': availablebalance,
|
||||||
|
'country': country,
|
||||||
|
'customerdob': customerdob,
|
||||||
|
'customername': customername,
|
||||||
|
'customerno': customerno,
|
||||||
|
'dateofacctopening': dateofacctopening,
|
||||||
|
'emailid': emailid,
|
||||||
|
'financialyear': financialyear,
|
||||||
|
'gender': gender,
|
||||||
|
'ifsccode': ifsccode,
|
||||||
|
'married': married,
|
||||||
|
'mobileno': mobileno,
|
||||||
|
'pan': pan,
|
||||||
|
'pincode': pincode,
|
||||||
|
'policynumber': policynumber,
|
||||||
|
'premiumamount': premiumamount,
|
||||||
|
'state': state,
|
||||||
|
'healthstatus': healthstatus,
|
||||||
|
'collectionchannel': collectionchannel,
|
||||||
|
'nomineename': nomineename,
|
||||||
|
'nomineeaddress': nomineeaddress,
|
||||||
|
'nomineerelationship': nomineerelationship,
|
||||||
|
'nomineeminor': nomineeminor,
|
||||||
|
'ruralcategory': ruralcategory,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future secondvalidationPMSBY({
|
||||||
|
String? aadharno,
|
||||||
|
String? accountno,
|
||||||
|
String? address1,
|
||||||
|
String? address2,
|
||||||
|
String? availablebalance,
|
||||||
|
String? city,
|
||||||
|
String? country,
|
||||||
|
String? customerdob,
|
||||||
|
String? customername,
|
||||||
|
String? customerno,
|
||||||
|
String? emailid,
|
||||||
|
String? financialyear,
|
||||||
|
String? gender,
|
||||||
|
String? married,
|
||||||
|
String? mobileno,
|
||||||
|
String? pan,
|
||||||
|
String? pincode,
|
||||||
|
String? policyno,
|
||||||
|
String? premiumamount,
|
||||||
|
String? state,
|
||||||
|
String? healthstatus,
|
||||||
|
String? nomineename,
|
||||||
|
String? nomineeadress,
|
||||||
|
String? relationwithnominee,
|
||||||
|
String? nomineeminor,
|
||||||
|
String? collectionchannel,
|
||||||
|
String? ruralcategory,
|
||||||
|
String? policystatus,
|
||||||
|
}) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/gov-scheme/create/PMSBY',
|
||||||
|
options: Options(
|
||||||
|
validateStatus: (int? status) => true,
|
||||||
|
receiveDataWhenStatusError: true,
|
||||||
|
),
|
||||||
|
data: {
|
||||||
|
"aadharno": aadharno,
|
||||||
|
"accountno": accountno,
|
||||||
|
"address1": address1,
|
||||||
|
"address2": address2,
|
||||||
|
"availablebalance": availablebalance,
|
||||||
|
"city": city,
|
||||||
|
"country": country,
|
||||||
|
"customerdob": customerdob,
|
||||||
|
"customername": customername,
|
||||||
|
"customerno": customerno,
|
||||||
|
"emailid": emailid,
|
||||||
|
"financialyear": financialyear,
|
||||||
|
"gender": gender,
|
||||||
|
"married": married,
|
||||||
|
"mobileno": mobileno,
|
||||||
|
"pan": pan,
|
||||||
|
"pincode": pincode,
|
||||||
|
"policyno": policyno,
|
||||||
|
"premiumamount": premiumamount,
|
||||||
|
"state": state,
|
||||||
|
"healthstatus": healthstatus,
|
||||||
|
"nomineename": nomineename,
|
||||||
|
"nomineeadress": nomineeadress,
|
||||||
|
"relationwithnominee": relationwithnominee,
|
||||||
|
"nomineeminor": nomineeminor,
|
||||||
|
"collectionchannel": collectionchannel,
|
||||||
|
"ruralcategory": ruralcategory,
|
||||||
|
"policystatus": policystatus,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> enquiry({
|
||||||
|
required String scheme,
|
||||||
|
required String action,
|
||||||
|
required String financialyear,
|
||||||
|
String? customerno,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(
|
||||||
|
"/api/gov-scheme/enquiry/PMJBY",
|
||||||
|
queryParameters: {
|
||||||
|
'scheme': scheme,
|
||||||
|
'action': action,
|
||||||
|
'financialyear': financialyear,
|
||||||
|
'customerno': customerno,
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
log("PMY Details Response: ${response.data}");
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw Exception("INTERNAL SERVER ERROR");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log("Error fetching PMY details: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/app.dart
@@ -12,14 +12,15 @@ import 'package:kmobile/features/auth/controllers/theme_state.dart';
|
|||||||
import 'config/routes.dart';
|
import 'config/routes.dart';
|
||||||
import 'di/injection.dart';
|
import 'di/injection.dart';
|
||||||
import 'features/auth/controllers/auth_cubit.dart';
|
import 'features/auth/controllers/auth_cubit.dart';
|
||||||
import 'features/card/screens/card_management_screen.dart';
|
import 'features/accounts/screens/account_statement_screen.dart';
|
||||||
import 'features/auth/screens/splash_screen.dart';
|
import 'package:kmobile/features/auth/controllers/auth_state.dart';
|
||||||
import 'features/auth/screens/login_screen.dart';
|
import 'features/auth/screens/login_screen.dart';
|
||||||
import 'features/service/screens/service_screen.dart';
|
import 'features/service/screens/service_screen.dart';
|
||||||
import 'features/dashboard/screens/dashboard_screen.dart';
|
import 'features/dashboard/screens/dashboard_screen.dart';
|
||||||
import 'features/auth/screens/mpin_screen.dart';
|
import 'features/auth/screens/mpin_screen.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
class KMobile extends StatefulWidget {
|
class KMobile extends StatefulWidget {
|
||||||
const KMobile({super.key});
|
const KMobile({super.key});
|
||||||
@@ -34,25 +35,46 @@ class KMobile extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _KMobileState extends State<KMobile> {
|
class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
|
||||||
bool showSplash = true;
|
Timer? _backgroundTimer;
|
||||||
Locale? _locale;
|
Locale? _locale;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
loadPreferences();
|
loadPreferences();
|
||||||
Future.delayed(const Duration(seconds: 3), () {
|
}
|
||||||
setState(() {
|
|
||||||
showSplash = false;
|
@override
|
||||||
});
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_backgroundTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
switch (state) {
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
_backgroundTimer?.cancel();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
_backgroundTimer = Timer(const Duration(minutes: 2), () {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
SystemNavigator.pop();
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadPreferences() async {
|
Future<void> loadPreferences() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
// Load Locale
|
|
||||||
final String? langCode = prefs.getString('locale');
|
final String? langCode = prefs.getString('locale');
|
||||||
if (langCode != null) {
|
if (langCode != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -103,9 +125,12 @@ class _KMobileState extends State<KMobile> {
|
|||||||
theme: themeState.getLightThemeData(),
|
theme: themeState.getLightThemeData(),
|
||||||
darkTheme: themeState.getDarkThemeData(),
|
darkTheme: themeState.getDarkThemeData(),
|
||||||
themeMode: context.watch<ThemeModeCubit>().state.mode,
|
themeMode: context.watch<ThemeModeCubit>().state.mode,
|
||||||
|
navigatorObservers: [
|
||||||
|
getIt<RouteObserver<ModalRoute<void>>>(),
|
||||||
|
],
|
||||||
onGenerateRoute: AppRoutes.generateRoute,
|
onGenerateRoute: AppRoutes.generateRoute,
|
||||||
initialRoute: AppRoutes.splash,
|
initialRoute: AppRoutes.splash,
|
||||||
home: showSplash ? const SplashScreen() : const AuthGate(),
|
home: const AuthGate(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -117,7 +142,6 @@ class _KMobileState extends State<KMobile> {
|
|||||||
|
|
||||||
class AuthGate extends StatefulWidget {
|
class AuthGate extends StatefulWidget {
|
||||||
const AuthGate({super.key});
|
const AuthGate({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AuthGate> createState() => _AuthGateState();
|
State<AuthGate> createState() => _AuthGateState();
|
||||||
}
|
}
|
||||||
@@ -178,9 +202,8 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_checking) {
|
if (_checking) {
|
||||||
return const SplashScreen();
|
return const LoginScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isLoggedIn) {
|
if (_isLoggedIn) {
|
||||||
if (_hasMPin) {
|
if (_hasMPin) {
|
||||||
if (_biometricEnabled) {
|
if (_biometricEnabled) {
|
||||||
@@ -188,13 +211,11 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
future: _tryBiometric(),
|
future: _tryBiometric(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const SplashScreen();
|
return const LoginScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.data == true) {
|
if (snapshot.data == true) {
|
||||||
return const NavigationScaffold(); // Authenticated
|
return const NavigationScaffold();
|
||||||
}
|
}
|
||||||
|
|
||||||
return MPinScreen(
|
return MPinScreen(
|
||||||
mode: MPinMode.enter,
|
mode: MPinMode.enter,
|
||||||
onCompleted: (_) {
|
onCompleted: (_) {
|
||||||
@@ -225,7 +246,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
onCompleted: (_) async {
|
onCompleted: (_) async {
|
||||||
final storage = getIt<SecureStorage>();
|
final storage = getIt<SecureStorage>();
|
||||||
final localAuth = LocalAuthentication();
|
final localAuth = LocalAuthentication();
|
||||||
|
|
||||||
final optIn = await showDialog<bool>(
|
final optIn = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
@@ -246,7 +266,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (optIn == true) {
|
if (optIn == true) {
|
||||||
final canCheck = await localAuth.canCheckBiometrics;
|
final canCheck = await localAuth.canCheckBiometrics;
|
||||||
bool didAuth = false;
|
bool didAuth = false;
|
||||||
@@ -254,7 +273,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
authEnable = AppLocalizations.of(context).authenticateToEnable;
|
authEnable = AppLocalizations.of(context).authenticateToEnable;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canCheck) {
|
if (canCheck) {
|
||||||
didAuth = await localAuth.authenticate(
|
didAuth = await localAuth.authenticate(
|
||||||
localizedReason: authEnable,
|
localizedReason: authEnable,
|
||||||
@@ -269,7 +287,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
await storage.write('biometric_enabled', 'false');
|
await storage.write('biometric_enabled', 'false');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -287,7 +304,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
|
|
||||||
class NavigationScaffold extends StatefulWidget {
|
class NavigationScaffold extends StatefulWidget {
|
||||||
const NavigationScaffold({super.key});
|
const NavigationScaffold({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NavigationScaffold> createState() => _NavigationScaffoldState();
|
State<NavigationScaffold> createState() => _NavigationScaffoldState();
|
||||||
}
|
}
|
||||||
@@ -295,10 +311,23 @@ class NavigationScaffold extends StatefulWidget {
|
|||||||
class _NavigationScaffoldState extends State<NavigationScaffold> {
|
class _NavigationScaffoldState extends State<NavigationScaffold> {
|
||||||
final PageController _pageController = PageController();
|
final PageController _pageController = PageController();
|
||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
|
|
||||||
final List<Widget> _pages = [
|
final List<Widget> _pages = [
|
||||||
const DashboardScreen(),
|
const DashboardScreen(),
|
||||||
const CardManagementScreen(),
|
BlocBuilder<AuthCubit, AuthState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state is Authenticated) {
|
||||||
|
if (state.users.isNotEmpty) {
|
||||||
|
return AccountStatementScreen(
|
||||||
|
users: state.users,
|
||||||
|
selectedIndex: 0,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const Center(child: Text("No accounts found."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
},
|
||||||
|
),
|
||||||
const ServiceScreen(),
|
const ServiceScreen(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -342,10 +371,9 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
|
|||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
currentIndex: _selectedIndex,
|
currentIndex: _selectedIndex,
|
||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: const Color(0XFF1E58AD),
|
||||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
selectedItemColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
unselectedItemColor:
|
unselectedItemColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedIndex = index;
|
_selectedIndex = index;
|
||||||
@@ -358,8 +386,8 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
|
|||||||
label: AppLocalizations.of(context).home,
|
label: AppLocalizations.of(context).home,
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: const Icon(Icons.credit_card),
|
icon: const Icon(Icons.swap_vert_sharp),
|
||||||
label: AppLocalizations.of(context).card,
|
label: AppLocalizations.of(context).transactions,
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: const Icon(Icons.miscellaneous_services),
|
icon: const Icon(Icons.miscellaneous_services),
|
||||||
@@ -372,11 +400,9 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this widget at the end of the file
|
|
||||||
class BiometricPromptScreen extends StatelessWidget {
|
class BiometricPromptScreen extends StatelessWidget {
|
||||||
final VoidCallback onCompleted;
|
final VoidCallback onCompleted;
|
||||||
const BiometricPromptScreen({super.key, required this.onCompleted});
|
const BiometricPromptScreen({super.key, required this.onCompleted});
|
||||||
|
|
||||||
Future<void> _handleBiometric(BuildContext context) async {
|
Future<void> _handleBiometric(BuildContext context) async {
|
||||||
final localAuth = LocalAuthentication();
|
final localAuth = LocalAuthentication();
|
||||||
final canCheck = await localAuth.canCheckBiometrics;
|
final canCheck = await localAuth.canCheckBiometrics;
|
||||||
@@ -407,7 +433,7 @@ class BiometricPromptScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Future.microtask(() => _showDialog(context));
|
Future.microtask(() => _showDialog(context));
|
||||||
return const SplashScreen();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showDialog(BuildContext context) async {
|
Future<void> _showDialog(BuildContext context) async {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../features/dashboard/screens/dashboard_screen.dart';
|
|||||||
// import '../features/transactions/screens/transactions_screen.dart';
|
// import '../features/transactions/screens/transactions_screen.dart';
|
||||||
// import '../features/payments/screens/payments_screen.dart';
|
// import '../features/payments/screens/payments_screen.dart';
|
||||||
// import '../features/settings/screens/settings_screen.dart';
|
// import '../features/settings/screens/settings_screen.dart';
|
||||||
|
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
|
||||||
|
|
||||||
class AppRoutes {
|
class AppRoutes {
|
||||||
// Private constructor to prevent instantiation
|
// Private constructor to prevent instantiation
|
||||||
@@ -34,7 +35,9 @@ class AppRoutes {
|
|||||||
return MaterialPageRoute(builder: (_) => const SplashScreen());
|
return MaterialPageRoute(builder: (_) => const SplashScreen());
|
||||||
case login:
|
case login:
|
||||||
return MaterialPageRoute(builder: (_) => const LoginScreen());
|
return MaterialPageRoute(builder: (_) => const LoginScreen());
|
||||||
|
case TncRequiredScreen.routeName: // Renamed class
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (_) => const TncRequiredScreen()); // Renamed class
|
||||||
case mPin:
|
case mPin:
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
builder: (_) => const MPinScreen(
|
builder: (_) => const MPinScreen(
|
||||||
|
|||||||
@@ -15,7 +15,17 @@ class AppThemes {
|
|||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
textTheme: GoogleFonts.rubikTextTheme(),
|
textTheme: GoogleFonts.rubikTextTheme(),
|
||||||
);
|
).copyWith(
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
backgroundColor: const Color(0xFF01A04C),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
iconTheme: IconThemeData(color: colorScheme.onPrimary),
|
||||||
|
actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static ThemeData getDarkTheme(ThemeType type) {
|
static ThemeData getDarkTheme(ThemeType type) {
|
||||||
@@ -32,7 +42,17 @@ class AppThemes {
|
|||||||
textTheme: GoogleFonts.rubikTextTheme(
|
textTheme: GoogleFonts.rubikTextTheme(
|
||||||
ThemeData(brightness: Brightness.dark).textTheme,
|
ThemeData(brightness: Brightness.dark).textTheme,
|
||||||
),
|
),
|
||||||
);
|
).copyWith(
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
backgroundColor: const Color(0xFF01A04C),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
iconTheme: IconThemeData(color: colorScheme.onPrimary),
|
||||||
|
actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Color _getSeedColor(ThemeType type) {
|
static Color _getSeedColor(ThemeType type) {
|
||||||
|
|||||||
@@ -2,18 +2,22 @@ class Beneficiary {
|
|||||||
final String accountNo;
|
final String accountNo;
|
||||||
final String accountType;
|
final String accountType;
|
||||||
final String name;
|
final String name;
|
||||||
|
final DateTime? createdAt;
|
||||||
final String ifscCode;
|
final String ifscCode;
|
||||||
final String? bankName;
|
final String? bankName;
|
||||||
final String? branchName;
|
final String? branchName;
|
||||||
|
final String? transactionLimit;
|
||||||
final String? tpin;
|
final String? tpin;
|
||||||
|
|
||||||
Beneficiary({
|
Beneficiary({
|
||||||
required this.accountNo,
|
required this.accountNo,
|
||||||
required this.accountType,
|
required this.accountType,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
this.createdAt,
|
||||||
required this.ifscCode,
|
required this.ifscCode,
|
||||||
this.bankName,
|
this.bankName,
|
||||||
this.branchName,
|
this.branchName,
|
||||||
|
this.transactionLimit,
|
||||||
this.tpin,
|
this.tpin,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -21,10 +25,14 @@ class Beneficiary {
|
|||||||
return Beneficiary(
|
return Beneficiary(
|
||||||
accountNo: json['account_no'] ?? json['accountNo'] ?? '',
|
accountNo: json['account_no'] ?? json['accountNo'] ?? '',
|
||||||
accountType: json['account_type'] ?? json['accountType'] ?? '',
|
accountType: json['account_type'] ?? json['accountType'] ?? '',
|
||||||
|
createdAt: json['createdAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.tryParse(json['createdAt']),
|
||||||
name: json['name'] ?? '',
|
name: json['name'] ?? '',
|
||||||
ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '',
|
ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '',
|
||||||
bankName: json['bank_name'] ?? json['bankName'] ?? '',
|
bankName: json['bank_name'] ?? json['bankName'] ?? '',
|
||||||
branchName: json['branch_name'] ?? json['branchName'] ?? '',
|
branchName: json['branch_name'] ?? json['branchName'] ?? '',
|
||||||
|
transactionLimit: json['transactionLimit'] ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class ImpsTransaction {
|
|||||||
final String? remitterName;
|
final String? remitterName;
|
||||||
final String beneficiaryName;
|
final String beneficiaryName;
|
||||||
final String tpin;
|
final String tpin;
|
||||||
|
final String? remarks;
|
||||||
|
|
||||||
ImpsTransaction({
|
ImpsTransaction({
|
||||||
required this.fromAccount,
|
required this.fromAccount,
|
||||||
@@ -15,6 +16,7 @@ class ImpsTransaction {
|
|||||||
this.remitterName,
|
this.remitterName,
|
||||||
required this.beneficiaryName,
|
required this.beneficiaryName,
|
||||||
required this.tpin,
|
required this.tpin,
|
||||||
|
this.remarks,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -26,6 +28,7 @@ class ImpsTransaction {
|
|||||||
'remitterName': remitterName,
|
'remitterName': remitterName,
|
||||||
'beneficiaryName': beneficiaryName,
|
'beneficiaryName': beneficiaryName,
|
||||||
'tpin': tpin,
|
'tpin': tpin,
|
||||||
|
'remarks': remarks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class NeftTransaction {
|
|||||||
final String remitterName;
|
final String remitterName;
|
||||||
final String beneficiaryName;
|
final String beneficiaryName;
|
||||||
final String tpin;
|
final String tpin;
|
||||||
|
final String? remarks;
|
||||||
|
|
||||||
NeftTransaction({
|
NeftTransaction({
|
||||||
required this.fromAccount,
|
required this.fromAccount,
|
||||||
@@ -15,6 +16,7 @@ class NeftTransaction {
|
|||||||
required this.remitterName,
|
required this.remitterName,
|
||||||
required this.beneficiaryName,
|
required this.beneficiaryName,
|
||||||
required this.tpin,
|
required this.tpin,
|
||||||
|
this.remarks,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -26,6 +28,7 @@ class NeftTransaction {
|
|||||||
'remitterName': remitterName,
|
'remitterName': remitterName,
|
||||||
'beneficiaryName': beneficiaryName,
|
'beneficiaryName': beneficiaryName,
|
||||||
'tpin': tpin,
|
'tpin': tpin,
|
||||||
|
'remarks': remarks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class RtgsTransaction {
|
|||||||
final String remitterName;
|
final String remitterName;
|
||||||
final String beneficiaryName;
|
final String beneficiaryName;
|
||||||
final String tpin;
|
final String tpin;
|
||||||
|
final String? remarks;
|
||||||
|
|
||||||
RtgsTransaction({
|
RtgsTransaction({
|
||||||
required this.fromAccount,
|
required this.fromAccount,
|
||||||
@@ -15,6 +16,7 @@ class RtgsTransaction {
|
|||||||
required this.remitterName,
|
required this.remitterName,
|
||||||
required this.beneficiaryName,
|
required this.beneficiaryName,
|
||||||
required this.tpin,
|
required this.tpin,
|
||||||
|
this.remarks,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -26,6 +28,7 @@ class RtgsTransaction {
|
|||||||
'remitterName': remitterName,
|
'remitterName': remitterName,
|
||||||
'beneficiaryName': beneficiaryName,
|
'beneficiaryName': beneficiaryName,
|
||||||
'tpin': tpin,
|
'tpin': tpin,
|
||||||
|
'remarks': remarks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,18 @@ class Transaction {
|
|||||||
final String? date;
|
final String? date;
|
||||||
final String? amount;
|
final String? amount;
|
||||||
final String? type;
|
final String? type;
|
||||||
|
final String? balance;
|
||||||
|
final String? balanceType;
|
||||||
|
|
||||||
|
Transaction(
|
||||||
|
{this.id,
|
||||||
|
this.name,
|
||||||
|
this.date,
|
||||||
|
this.amount,
|
||||||
|
this.type,
|
||||||
|
this.balance,
|
||||||
|
this.balanceType});
|
||||||
|
|
||||||
Transaction({this.id, this.name, this.date, this.amount, this.type});
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'id': id,
|
'id': id,
|
||||||
@@ -13,6 +23,8 @@ class Transaction {
|
|||||||
'date': date,
|
'date': date,
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
'type': type,
|
'type': type,
|
||||||
|
'balance': balance,
|
||||||
|
'balanceType': balanceType
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +35,7 @@ class Transaction {
|
|||||||
date: json['date'] as String?,
|
date: json['date'] as String?,
|
||||||
amount: json['amount'] as String?,
|
amount: json['amount'] as String?,
|
||||||
type: json['type'] as String?,
|
type: json['type'] as String?,
|
||||||
);
|
balance: json['balance'] as String?,
|
||||||
|
balanceType: json['balanceType'] as String?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class Transfer {
|
|||||||
final String toAccountType;
|
final String toAccountType;
|
||||||
final String amount;
|
final String amount;
|
||||||
String? tpin;
|
String? tpin;
|
||||||
|
String? remarks;
|
||||||
|
|
||||||
Transfer({
|
Transfer({
|
||||||
required this.fromAccount,
|
required this.fromAccount,
|
||||||
@@ -11,6 +12,7 @@ class Transfer {
|
|||||||
required this.toAccountType,
|
required this.toAccountType,
|
||||||
required this.amount,
|
required this.amount,
|
||||||
this.tpin,
|
this.tpin,
|
||||||
|
this.remarks,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -20,6 +22,7 @@ class Transfer {
|
|||||||
'toAccountType': toAccountType,
|
'toAccountType': toAccountType,
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
'tpin': tpin,
|
'tpin': tpin,
|
||||||
|
'remarks': remarks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ class AuthRepository {
|
|||||||
|
|
||||||
static const _accessTokenKey = 'access_token';
|
static const _accessTokenKey = 'access_token';
|
||||||
static const _tokenExpiryKey = 'token_expiry';
|
static const _tokenExpiryKey = 'token_expiry';
|
||||||
|
static const _tncKey = 'tnc';
|
||||||
|
|
||||||
AuthRepository(this._authService, this._userService, this._secureStorage);
|
AuthRepository(this._authService, this._userService, this._secureStorage);
|
||||||
|
|
||||||
Future<List<User>> login(String customerNo, String password) async {
|
Future<(List<User>, AuthToken)> login(
|
||||||
|
String customerNo, String password) async {
|
||||||
// Create credentials and call service
|
// Create credentials and call service
|
||||||
final credentials =
|
final credentials =
|
||||||
AuthCredentials(customerNo: customerNo, password: password);
|
AuthCredentials(customerNo: customerNo, password: password);
|
||||||
@@ -27,7 +29,7 @@ class AuthRepository {
|
|||||||
|
|
||||||
// Get and save user profile
|
// Get and save user profile
|
||||||
final users = await _userService.getUserDetails();
|
final users = await _userService.getUserDetails();
|
||||||
return users;
|
return (users, authToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isLoggedIn() async {
|
Future<bool> isLoggedIn() async {
|
||||||
@@ -47,18 +49,38 @@ class AuthRepository {
|
|||||||
await _secureStorage.write(_accessTokenKey, token.accessToken);
|
await _secureStorage.write(_accessTokenKey, token.accessToken);
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
_tokenExpiryKey, token.expiresAt.toIso8601String());
|
_tokenExpiryKey, token.expiresAt.toIso8601String());
|
||||||
|
await _secureStorage.write(_tncKey, token.tnc.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAuthTokens() async {
|
||||||
|
await _secureStorage.deleteAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AuthToken?> _getAuthToken() async {
|
Future<AuthToken?> _getAuthToken() async {
|
||||||
final accessToken = await _secureStorage.read(_accessTokenKey);
|
final accessToken = await _secureStorage.read(_accessTokenKey);
|
||||||
final expiryString = await _secureStorage.read(_tokenExpiryKey);
|
final expiryString = await _secureStorage.read(_tokenExpiryKey);
|
||||||
|
final tncString = await _secureStorage.read(_tncKey);
|
||||||
|
|
||||||
if (accessToken != null && expiryString != null) {
|
if (accessToken != null && expiryString != null) {
|
||||||
return AuthToken(
|
final authToken = AuthToken(
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
expiresAt: DateTime.parse(expiryString),
|
expiresAt: DateTime.parse(expiryString),
|
||||||
|
tnc:
|
||||||
|
tncString == 'true', // Parse 'true' string to true, otherwise false
|
||||||
);
|
);
|
||||||
|
return authToken;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> acceptTnc() async {
|
||||||
|
// This method calls the setTncFlag function
|
||||||
|
try {
|
||||||
|
await _authService.setTncflag();
|
||||||
|
} catch (e) {
|
||||||
|
// Handle or rethrow the error as needed
|
||||||
|
print('Error setting TNC flag: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:kmobile/data/models/transaction.dart';
|
import 'package:kmobile/data/models/transaction.dart';
|
||||||
@@ -25,8 +23,6 @@ class TransactionRepositoryImpl implements TransactionRepository {
|
|||||||
queryParameters['toDate'] = DateFormat('ddMMyyyy').format(toDate);
|
queryParameters['toDate'] = DateFormat('ddMMyyyy').format(toDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
log('query params below');
|
|
||||||
log(queryParameters.toString());
|
|
||||||
final resp = await _dio.get(
|
final resp = await _dio.get(
|
||||||
'/api/transactions/account/$accountNo',
|
'/api/transactions/account/$accountNo',
|
||||||
queryParameters: queryParameters.isNotEmpty ? queryParameters : null,
|
queryParameters: queryParameters.isNotEmpty ? queryParameters : null,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/branch_service.dart';
|
||||||
|
import 'package:kmobile/api/services/cheque_service.dart';
|
||||||
|
import 'package:kmobile/api/services/limit_service.dart';
|
||||||
import 'package:kmobile/api/services/rtgs_service.dart';
|
import 'package:kmobile/api/services/rtgs_service.dart';
|
||||||
import 'package:kmobile/api/services/neft_service.dart';
|
import 'package:kmobile/api/services/neft_service.dart';
|
||||||
import 'package:kmobile/api/services/imps_service.dart';
|
import 'package:kmobile/api/services/imps_service.dart';
|
||||||
@@ -6,11 +10,13 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:kmobile/api/services/beneficiary_service.dart';
|
import 'package:kmobile/api/services/beneficiary_service.dart';
|
||||||
import 'package:kmobile/api/services/payment_service.dart';
|
import 'package:kmobile/api/services/payment_service.dart';
|
||||||
import 'package:kmobile/api/services/user_service.dart';
|
import 'package:kmobile/api/services/user_service.dart';
|
||||||
|
import 'package:kmobile/api/services/yojna_service.dart';
|
||||||
import 'package:kmobile/data/repositories/transaction_repository.dart';
|
import 'package:kmobile/data/repositories/transaction_repository.dart';
|
||||||
import 'package:kmobile/features/auth/controllers/theme_cubit.dart';
|
import 'package:kmobile/features/auth/controllers/theme_cubit.dart';
|
||||||
import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart';
|
import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart';
|
||||||
import '../api/services/auth_service.dart';
|
import '../api/services/auth_service.dart';
|
||||||
import '../api/interceptors/auth_interceptor.dart';
|
import '../api/interceptors/auth_interceptor.dart';
|
||||||
|
import '../api/services/change_password_service.dart';
|
||||||
import '../data/repositories/auth_repository.dart';
|
import '../data/repositories/auth_repository.dart';
|
||||||
import '../features/auth/controllers/auth_cubit.dart';
|
import '../features/auth/controllers/auth_cubit.dart';
|
||||||
import '../security/secure_storage.dart';
|
import '../security/secure_storage.dart';
|
||||||
@@ -18,6 +24,8 @@ import '../security/secure_storage.dart';
|
|||||||
final getIt = GetIt.instance;
|
final getIt = GetIt.instance;
|
||||||
|
|
||||||
Future<void> setupDependencies() async {
|
Future<void> setupDependencies() async {
|
||||||
|
getIt.registerSingleton<RouteObserver<ModalRoute<void>>>(
|
||||||
|
RouteObserver<ModalRoute<void>>());
|
||||||
//getIt.registerLazySingleton<ThemeController>(() => ThemeController());
|
//getIt.registerLazySingleton<ThemeController>(() => ThemeController());
|
||||||
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
|
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
|
||||||
getIt.registerSingleton<ThemeCubit>(ThemeCubit());
|
getIt.registerSingleton<ThemeCubit>(ThemeCubit());
|
||||||
@@ -45,9 +53,16 @@ Future<void> setupDependencies() async {
|
|||||||
|
|
||||||
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
|
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
|
||||||
getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>()));
|
getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>()));
|
||||||
|
getIt.registerSingleton<LimitService>(LimitService(getIt<Dio>()));
|
||||||
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
|
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
|
||||||
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
|
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
|
||||||
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
|
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
|
||||||
|
getIt.registerSingleton<BranchService>(BranchService(getIt<Dio>()));
|
||||||
|
getIt.registerSingleton<ChequeService>(ChequeService(getIt<Dio>()));
|
||||||
|
getIt.registerSingleton<YojnaService>(YojnaService(getIt<Dio>()));
|
||||||
|
getIt.registerLazySingleton<ChangePasswordService>(
|
||||||
|
() => ChangePasswordService(getIt<Dio>()),
|
||||||
|
);
|
||||||
|
|
||||||
// Add auth interceptor after repository is available
|
// Add auth interceptor after repository is available
|
||||||
getIt<Dio>().interceptors.add(
|
getIt<Dio>().interceptors.add(
|
||||||
@@ -55,21 +70,23 @@ Future<void> setupDependencies() async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Register controllers/cubits
|
// Register controllers/cubits
|
||||||
getIt.registerFactory<AuthCubit>(
|
getIt.registerFactory<AuthCubit>(() => AuthCubit(
|
||||||
() => AuthCubit(getIt<AuthRepository>(), getIt<UserService>()));
|
getIt<AuthRepository>(), getIt<UserService>(), getIt<SecureStorage>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Dio _createDioClient() {
|
Dio _createDioClient() {
|
||||||
final dio = Dio(
|
final dio = Dio(
|
||||||
BaseOptions(
|
BaseOptions(
|
||||||
baseUrl:
|
baseUrl:
|
||||||
'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080',
|
'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com', //test
|
||||||
//'http://localhost:8081',
|
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
|
||||||
connectTimeout: const Duration(seconds: 5),
|
//'https://kccbmbnk.net', //prod small
|
||||||
receiveTimeout: const Duration(seconds: 10),
|
connectTimeout: const Duration(seconds: 60),
|
||||||
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
|
'X-Login-Type': 'MB',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
175
lib/features/account_opening/screens/account_opening_screen.dart
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import 'package:flutter/material.dart'; // Keep if User model is generic
|
||||||
|
import 'package:kmobile/features/account_opening/screens/fd_screen.dart';
|
||||||
|
import 'package:kmobile/features/account_opening/screens/loan_screen.dart';
|
||||||
|
import 'package:kmobile/features/account_opening/screens/rd_screen.dart';
|
||||||
|
import 'package:kmobile/features/account_opening/screens/td_screen.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class AccountOpeningScreen extends StatefulWidget {
|
||||||
|
const AccountOpeningScreen({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AccountOpeningScreen> createState() => _AccountOpeningScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountOpeningScreenState extends State<AccountOpeningScreen> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
"Account Opening",
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AccountOpeningCardTile(
|
||||||
|
icon: Symbols.savings,
|
||||||
|
label: "Fixed Deposit (FD)",
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const FdScreen(
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: AccountOpeningCardTile(
|
||||||
|
icon: Symbols.currency_rupee,
|
||||||
|
label: "Term Deposit",
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const TermDepositScreen()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: AccountOpeningCardTile(
|
||||||
|
icon: Symbols.account_balance,
|
||||||
|
label: AppLocalizations.of(context).recurringDeposit,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const RecurringDepositScreen() ),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: AccountOpeningCardTile(
|
||||||
|
icon: Symbols.credit_card,
|
||||||
|
label: "Request Loan",
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const LoanScreen()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07,
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountOpeningCardTile extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool disable;
|
||||||
|
|
||||||
|
const AccountOpeningCardTile({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
this.disable = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
),
|
||||||
|
elevation: 4,
|
||||||
|
child: InkWell(
|
||||||
|
onTap:
|
||||||
|
disable ? null : onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: 48,
|
||||||
|
color: disable
|
||||||
|
? theme.disabledColor
|
||||||
|
: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: disable
|
||||||
|
? theme.disabledColor
|
||||||
|
: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
244
lib/features/account_opening/screens/fd_screen.dart
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/features/account_opening/screens/interest_rates_screen.dart';
|
||||||
|
|
||||||
|
class FdScreen extends StatefulWidget {
|
||||||
|
const FdScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FdScreen> createState() => _FdScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FdScreenState extends State<FdScreen> {
|
||||||
|
final TextEditingController _debitAccountController = TextEditingController();
|
||||||
|
final TextEditingController _amountController = TextEditingController();
|
||||||
|
final TextEditingController _yearsController = TextEditingController();
|
||||||
|
final TextEditingController _monthsController = TextEditingController();
|
||||||
|
final TextEditingController _daysController = TextEditingController();
|
||||||
|
|
||||||
|
String _rateOfInterest = "N/A";
|
||||||
|
String _maturityDate = "N/A";
|
||||||
|
String _maturityAmount = "N/A";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_debitAccountController.dispose();
|
||||||
|
_amountController.dispose();
|
||||||
|
_yearsController.dispose();
|
||||||
|
_monthsController.dispose();
|
||||||
|
_daysController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Fixed Deposit (FD)'),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Explanation Tile
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Earn more on your savings with a simple, secure Fixed Deposit.',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Debit Account Number
|
||||||
|
TextFormField(
|
||||||
|
controller: _debitAccountController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Debit Account Number',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Enter Amount
|
||||||
|
TextFormField(
|
||||||
|
controller: _amountController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Enter Amount',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixText: '₹ '
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Duration and Interest Rates Link
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Duration',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const InterestRatesScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Interest Rates',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// Duration TextBoxes
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _yearsController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Years',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _monthsController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Months',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _daysController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Days',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Rate of Interest and Maturity Date Display
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Rate of Interest',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
_rateOfInterest,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Maturity Date',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
_maturityDate,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Maturity Amount Display
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Maturity Amount',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
_maturityAmount,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Proceed Button
|
||||||
|
Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Implement proceed logic
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Proceed',
|
||||||
|
style: TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class InterestRatesScreen extends StatelessWidget {
|
||||||
|
const InterestRatesScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Interest Rates'),
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Text('This page will display a table of interest rates.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/features/account_opening/screens/loan_screen.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LoanScreen extends StatelessWidget {
|
||||||
|
const LoanScreen({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Request Loan"),
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Text("Loan Account"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/features/account_opening/screens/rd_screen.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RecurringDepositScreen extends StatelessWidget {
|
||||||
|
const RecurringDepositScreen({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Recurring Deposit"),
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Text("Recurring Deposit"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/features/account_opening/screens/td_screen.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TermDepositScreen extends StatelessWidget {
|
||||||
|
const TermDepositScreen({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Term Deposit (TD)"),
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Text("Term Deposit (TD)"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,30 +18,70 @@ class AccountInfoScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _AccountInfoScreen extends State<AccountInfoScreen> {
|
class _AccountInfoScreen extends State<AccountInfoScreen> {
|
||||||
late User selectedUser;
|
late User selectedUser;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
selectedUser = widget.users[widget.selectedIndex];
|
selectedUser = widget.users[widget.selectedIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getFullAccountType(String? accountType) {
|
||||||
|
if (accountType == null || accountType.isEmpty) return 'N/A';
|
||||||
|
// Convert to title case
|
||||||
|
switch (accountType.toLowerCase()) {
|
||||||
|
case 'sa':
|
||||||
|
return AppLocalizations.of(context).savingsAccount;
|
||||||
|
case 'sb':
|
||||||
|
return AppLocalizations.of(context).savingsAccount;
|
||||||
|
case 'ln':
|
||||||
|
return AppLocalizations.of(context).loanAccount;
|
||||||
|
case 'td':
|
||||||
|
return AppLocalizations.of(context).termDeposit;
|
||||||
|
case 'rd':
|
||||||
|
return AppLocalizations.of(context).recurringDeposit;
|
||||||
|
case 'ca':
|
||||||
|
return "Current Account";
|
||||||
|
case 'cc':
|
||||||
|
return "Cash Credit Account";
|
||||||
|
case 'od':
|
||||||
|
return "Overdraft Account";
|
||||||
|
default:
|
||||||
|
return AppLocalizations.of(context).unknownAccount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final users = widget.users;
|
final users = widget.users;
|
||||||
|
|
||||||
int selectedIndex = widget.selectedIndex;
|
int selectedIndex = widget.selectedIndex;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context)
|
title: Text(AppLocalizations.of(context)
|
||||||
.accountInfo
|
.accountInfo
|
||||||
.replaceFirst(RegExp('\n'), '')),
|
.replaceFirst(RegExp('\n'), '')),
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).accountNumber,
|
AppLocalizations.of(context).accountNumber,
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
),
|
),
|
||||||
|
|
||||||
DropdownButton<User>(
|
DropdownButton<User>(
|
||||||
value: selectedUser,
|
value: selectedUser,
|
||||||
onChanged: (User? newUser) {
|
onChanged: (User? newUser) {
|
||||||
@@ -54,39 +94,83 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
|
|||||||
items: widget.users.map((user) {
|
items: widget.users.map((user) {
|
||||||
return DropdownMenuItem<User>(
|
return DropdownMenuItem<User>(
|
||||||
value: user,
|
value: user,
|
||||||
child: Text(user.accountNo.toString()),
|
child: Text(
|
||||||
|
user.accountNo.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
isExpanded: true,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
InfoRow(
|
InfoRow(
|
||||||
title: AppLocalizations.of(context).customerNumber,
|
title: AppLocalizations.of(context).customerNumber,
|
||||||
value: selectedUser.cifNumber ?? 'N/A',
|
value: selectedUser.cifNumber ?? 'N/A',
|
||||||
),
|
),
|
||||||
|
InfoRow(
|
||||||
|
title: AppLocalizations.of(context).accountType,
|
||||||
|
value: getFullAccountType(selectedUser.accountType),
|
||||||
|
),
|
||||||
InfoRow(
|
InfoRow(
|
||||||
title: AppLocalizations.of(context).productName,
|
title: AppLocalizations.of(context).productName,
|
||||||
value: selectedUser.productType ?? 'N/A',
|
value: selectedUser.productType ?? 'N/A',
|
||||||
),
|
),
|
||||||
// InfoRow(title: 'Account Opening Date', value: users[selectedIndex].accountOpeningDate ?? 'N/A'),
|
|
||||||
InfoRow(
|
InfoRow(
|
||||||
title: AppLocalizations.of(context).accountStatus,
|
title: AppLocalizations.of(context).accountStatus,
|
||||||
value: 'OPEN',
|
value: 'OPEN',
|
||||||
),
|
),
|
||||||
InfoRow(
|
InfoRow(
|
||||||
title: AppLocalizations.of(context).availableBalance,
|
title:
|
||||||
|
AppLocalizations.of(context).availableBalance,
|
||||||
value: selectedUser.availableBalance ?? 'N/A',
|
value: selectedUser.availableBalance ?? 'N/A',
|
||||||
),
|
),
|
||||||
InfoRow(
|
InfoRow(
|
||||||
title: AppLocalizations.of(context).currentBalance,
|
title: AppLocalizations.of(context).currentBalance,
|
||||||
value: selectedUser.currentBalance ?? 'N/A',
|
value: selectedUser.currentBalance ?? 'N/A',
|
||||||
),
|
),
|
||||||
|
if (users[selectedIndex].approvedAmount != null)
|
||||||
users[selectedIndex].approvedAmount != null
|
InfoRow(
|
||||||
? InfoRow(
|
title:
|
||||||
title: AppLocalizations.of(context).approvedAmount,
|
AppLocalizations.of(context).approvedAmount,
|
||||||
value: selectedUser.approvedAmount ?? 'N/A',
|
value: selectedUser.approvedAmount ?? 'N/A',
|
||||||
)
|
),
|
||||||
: const SizedBox.shrink(),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -111,15 +195,18 @@ class InfoRow extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.bold,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface),
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:pdf/pdf.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:kmobile/data/models/transaction.dart';
|
import 'package:kmobile/data/models/transaction.dart';
|
||||||
import 'package:kmobile/data/repositories/transaction_repository.dart';
|
import 'package:kmobile/data/repositories/transaction_repository.dart';
|
||||||
import 'package:kmobile/di/injection.dart';
|
import 'package:kmobile/di/injection.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
import 'transaction_details_screen.dart';
|
import 'transaction_details_screen.dart';
|
||||||
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:open_filex/open_filex.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
|
||||||
class AccountStatementScreen extends StatefulWidget {
|
class AccountStatementScreen extends StatefulWidget {
|
||||||
final String accountNo;
|
final List<User> users;
|
||||||
final String balance;
|
final int selectedIndex;
|
||||||
|
|
||||||
const AccountStatementScreen({
|
const AccountStatementScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.accountNo,
|
required this.users,
|
||||||
required this.balance,
|
required this.selectedIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -21,17 +32,57 @@ class AccountStatementScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AccountStatementScreen extends State<AccountStatementScreen> {
|
class _AccountStatementScreen extends State<AccountStatementScreen> {
|
||||||
|
late User selectedUser;
|
||||||
DateTime? fromDate;
|
DateTime? fromDate;
|
||||||
DateTime? toDate;
|
DateTime? toDate;
|
||||||
bool _txLoading = true;
|
bool _txLoading = true;
|
||||||
List<Transaction> _transactions = [];
|
List<Transaction> _transactions = [];
|
||||||
final _minAmountController = TextEditingController();
|
final _minAmountController = TextEditingController();
|
||||||
final _maxAmountController = TextEditingController();
|
final _maxAmountController = TextEditingController();
|
||||||
|
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
selectedUser = widget.users[widget.selectedIndex];
|
||||||
_loadTransactions();
|
_loadTransactions();
|
||||||
|
_initializeNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeNotifications() async {
|
||||||
|
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||||
|
AndroidInitializationSettings('notification_icon');
|
||||||
|
|
||||||
|
const InitializationSettings initializationSettings =
|
||||||
|
InitializationSettings(android: initializationSettingsAndroid);
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.initialize(
|
||||||
|
initializationSettings,
|
||||||
|
onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse,
|
||||||
|
);
|
||||||
|
_requestNotificationPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDidReceiveNotificationResponse(
|
||||||
|
NotificationResponse notificationResponse) async {
|
||||||
|
final String? payload = notificationResponse.payload;
|
||||||
|
if (payload != null && payload.isNotEmpty) {
|
||||||
|
await OpenFilex.open(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requestNotificationPermission() async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
|
||||||
|
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>();
|
||||||
|
|
||||||
|
if (androidImplementation != null) {
|
||||||
|
await androidImplementation.requestNotificationsPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadTransactions() async {
|
Future<void> _loadTransactions() async {
|
||||||
@@ -42,7 +93,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
try {
|
try {
|
||||||
final repo = getIt<TransactionRepository>();
|
final repo = getIt<TransactionRepository>();
|
||||||
final txs = await repo.fetchTransactions(
|
final txs = await repo.fetchTransactions(
|
||||||
widget.accountNo,
|
selectedUser.accountNo ?? '',
|
||||||
fromDate: fromDate,
|
fromDate: fromDate,
|
||||||
toDate: toDate,
|
toDate: toDate,
|
||||||
);
|
);
|
||||||
@@ -87,12 +138,15 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final maxToDate = fromDate!.add(const Duration(days: 31)).isBefore(now)
|
final maxToDate = fromDate!.add(const Duration(days: 183)).isBefore(now)
|
||||||
? fromDate!.add(const Duration(days: 31))
|
? fromDate!.add(const Duration(days: 183))
|
||||||
: now;
|
: now;
|
||||||
|
final initialToDate = toDate ?? now;
|
||||||
|
final clampedInitialToDate =
|
||||||
|
initialToDate.isBefore(fromDate!) ? fromDate! : initialToDate;
|
||||||
final picked = await showDatePicker(
|
final picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: toDate ?? fromDate!,
|
initialDate: clampedInitialToDate,
|
||||||
firstDate: fromDate!,
|
firstDate: fromDate!,
|
||||||
lastDate: maxToDate,
|
lastDate: maxToDate,
|
||||||
);
|
);
|
||||||
@@ -123,25 +177,59 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${AppLocalizations.of(context).accountNumber}: ",
|
AppLocalizations.of(context).accountNumber,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 17,
|
fontWeight: FontWeight.w500, fontSize: 17),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
|
const VerticalDivider(
|
||||||
|
width: 20,
|
||||||
|
thickness: 1,
|
||||||
|
indent: 5,
|
||||||
|
endIndent: 5,
|
||||||
|
color: Colors.grey),
|
||||||
|
DropdownButton<User>(
|
||||||
|
value: selectedUser,
|
||||||
|
onChanged: (User? newUser) {
|
||||||
|
if (newUser != null) {
|
||||||
|
setState(() {
|
||||||
|
selectedUser = newUser;
|
||||||
|
});
|
||||||
|
_loadTransactions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: widget.users.map((user) {
|
||||||
|
return DropdownMenuItem<User>(
|
||||||
|
value: user,
|
||||||
|
child: Text(user.accountNo.toString()),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
underline: Container(), // Remove the underline
|
||||||
),
|
),
|
||||||
Text(widget.accountNo, style: const TextStyle(fontSize: 17)),
|
Spacer(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
),
|
||||||
Row(
|
),
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${AppLocalizations.of(context).availableBalance}: ",
|
"${AppLocalizations.of(context).availableBalance}: ",
|
||||||
@@ -149,11 +237,12 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(' ₹ ${widget.balance}',
|
Text(' ₹ ${selectedUser.availableBalance}',
|
||||||
style: const TextStyle(fontSize: 17)),
|
style: const TextStyle(fontSize: 17)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).filters,
|
AppLocalizations.of(context).filters,
|
||||||
style: const TextStyle(fontSize: 17),
|
style: const TextStyle(fontSize: 17),
|
||||||
@@ -223,8 +312,11 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
itemCount: 3,
|
itemCount: 3,
|
||||||
itemBuilder: (_, __) => ListTile(
|
itemBuilder: (_, __) => ListTile(
|
||||||
leading: Shimmer.fromColors(
|
leading: Shimmer.fromColors(
|
||||||
baseColor: Colors.grey[300]!,
|
baseColor:
|
||||||
highlightColor: Colors.grey[100]!,
|
Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
highlightColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 12,
|
radius: 12,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
@@ -232,21 +324,29 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Shimmer.fromColors(
|
title: Shimmer.fromColors(
|
||||||
baseColor: Colors.grey[300]!,
|
baseColor:
|
||||||
highlightColor: Colors.grey[100]!,
|
Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
highlightColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 10,
|
height: 10,
|
||||||
width: 100,
|
width: 100,
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Shimmer.fromColors(
|
subtitle: Shimmer.fromColors(
|
||||||
baseColor: Colors.grey[300]!,
|
baseColor:
|
||||||
highlightColor: Colors.grey[100]!,
|
Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
highlightColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 8,
|
height: 8,
|
||||||
width: 60,
|
width: 60,
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -257,20 +357,24 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
AppLocalizations.of(context).noTransactions,
|
AppLocalizations.of(context).noTransactions,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color:
|
||||||
|
Theme.of(context).colorScheme.onSurface,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
: ListView.separated(
|
: ListView.builder(
|
||||||
itemCount: _transactions.length,
|
itemCount: _transactions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final tx = _transactions[index];
|
final tx = _transactions[index];
|
||||||
return ListTile(
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 0, vertical: 4),
|
||||||
|
child: ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
tx.type == 'CR'
|
tx.type == 'CR'
|
||||||
? Symbols.call_received
|
? Symbols.call_received
|
||||||
: Symbols.call_made,
|
: Symbols.call_made,
|
||||||
color: tx.type == 'CR'
|
color: tx.type == 'CR'
|
||||||
? Colors.green
|
? const Color(0xFF10BB10)
|
||||||
: Theme.of(context).colorScheme.error,
|
: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -279,38 +383,670 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
tx.name != null
|
tx.name != null
|
||||||
? (tx.name!.length > 18
|
? (tx.name!.length > 22
|
||||||
? tx.name!.substring(0, 22)
|
? tx.name!.substring(0, 22)
|
||||||
: tx.name!)
|
: tx.name!)
|
||||||
: '',
|
: '',
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
trailing: Text(
|
trailing: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.end,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
"₹${tx.amount}",
|
"₹${tx.amount}",
|
||||||
style: const TextStyle(fontSize: 17),
|
style: const TextStyle(fontSize: 17),
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
"Bal: ₹${tx.balance}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize:
|
||||||
|
12), // Style matches tx.name
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => TransactionDetailsScreen(
|
builder: (_) =>
|
||||||
|
TransactionDetailsScreen(
|
||||||
transaction: tx),
|
transaction: tx),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) {
|
|
||||||
return const Divider();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
_exportToPdf();
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.download),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _exportToPdf() async {
|
||||||
|
if (_transactions.isEmpty) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('No transactions to export.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
0,
|
||||||
|
'Downloading PDF',
|
||||||
|
'Your account statement is being downloaded...',
|
||||||
|
const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'download_channel',
|
||||||
|
'Download Notifications',
|
||||||
|
channelDescription: 'Notifications for PDF downloads',
|
||||||
|
importance: Importance.low,
|
||||||
|
priority: Priority.low,
|
||||||
|
showProgress: true,
|
||||||
|
maxProgress: 0,
|
||||||
|
ongoing: true,
|
||||||
|
icon: 'notification_icon',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 1. LOAD ASSETS ---
|
||||||
|
final logoImage = pw.MemoryImage(
|
||||||
|
(await rootBundle.load('assets/images/logo.png')).buffer.asUint8List());
|
||||||
|
final timesFont = await rootBundle.load("assets/fonts/Rubik-Regular.ttf");
|
||||||
|
final timesBoldFont = await rootBundle.load("assets/fonts/Rubik-Bold.ttf");
|
||||||
|
final ttf = pw.Font.ttf(timesFont);
|
||||||
|
final ttfBold = pw.Font.ttf(timesBoldFont);
|
||||||
|
|
||||||
|
// --- 2. DEFINE COLORS ---
|
||||||
|
final primaryColor = PdfColor.fromHex("#1a5f3a");
|
||||||
|
final secondaryColor = PdfColor.fromHex("#2e7d32");
|
||||||
|
final debitColor = PdfColor.fromHex("#d32f2f");
|
||||||
|
final lightGreyColor = PdfColor.fromHex("#666");
|
||||||
|
final tableBorderColor = PdfColor.fromHex("#d0d0d0");
|
||||||
|
final lightBgColor = PdfColor.fromHex("#f9f9f9");
|
||||||
|
final warningBgColor = PdfColor.fromHex("#f8d7da");
|
||||||
|
final warningBorderColor = PdfColor.fromHex("#f5c6cb");
|
||||||
|
final warningTextColor = PdfColor.fromHex("#721c24");
|
||||||
|
|
||||||
|
// --- 3. CREATE PDF ---
|
||||||
|
final pdf = pw.Document(
|
||||||
|
theme: pw.ThemeData.withFont(base: ttf, bold: ttfBold),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 4. BUILD PAGES ---
|
||||||
|
pdf.addPage(
|
||||||
|
pw.MultiPage(
|
||||||
|
pageFormat: PdfPageFormat.a4.copyWith(
|
||||||
|
marginTop: 15 * PdfPageFormat.mm,
|
||||||
|
marginLeft: 10 * PdfPageFormat.mm,
|
||||||
|
marginRight: 10 * PdfPageFormat.mm,
|
||||||
|
marginBottom: 20 * PdfPageFormat.mm,
|
||||||
|
),
|
||||||
|
header: (context) =>
|
||||||
|
_buildHeader(logoImage, primaryColor, lightGreyColor),
|
||||||
|
footer: (context) {
|
||||||
|
return pw.Center(
|
||||||
|
child: pw.Text(
|
||||||
|
'** This is only for information purpose and not for legal use **',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 9,
|
||||||
|
color: lightGreyColor,
|
||||||
|
fontStyle: pw.FontStyle.italic)));
|
||||||
|
},
|
||||||
|
build: (context) => [
|
||||||
|
_buildAccountDetails(
|
||||||
|
customerName: selectedUser.name ?? '',
|
||||||
|
branchCode: selectedUser.branchId ?? '',
|
||||||
|
accountNo: selectedUser.accountNo ?? '',
|
||||||
|
cifNumber: selectedUser.cifNumber ?? '',
|
||||||
|
address: selectedUser.address ?? '',
|
||||||
|
lightGreyColor: lightGreyColor,
|
||||||
|
tableBorderColor: tableBorderColor,
|
||||||
|
lightBgColor: lightBgColor,
|
||||||
|
),
|
||||||
|
_buildWarning(
|
||||||
|
warningBgColor, warningBorderColor, debitColor, warningTextColor),
|
||||||
|
_buildPeriodHeader(
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
fromDate: fromDate,
|
||||||
|
toDate: toDate,
|
||||||
|
),
|
||||||
|
_buildTransactionsTable(
|
||||||
|
transactions: _transactions,
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
secondaryColor: secondaryColor,
|
||||||
|
debitColor: debitColor,
|
||||||
|
tableBorderColor: tableBorderColor,
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
pw.Text('END OF STATEMENT', style: const pw.TextStyle(fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
pdf.addPage(
|
||||||
|
pw.Page(
|
||||||
|
pageFormat: PdfPageFormat.a4.copyWith(
|
||||||
|
marginTop: 15 * PdfPageFormat.mm,
|
||||||
|
marginLeft: 10 * PdfPageFormat.mm,
|
||||||
|
marginRight: 10 * PdfPageFormat.mm,
|
||||||
|
marginBottom: 20 * PdfPageFormat.mm,
|
||||||
|
),
|
||||||
|
build: (context) => _buildLastPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 5. SAVE AND NOTIFY ---
|
||||||
|
try {
|
||||||
|
final Uint8List pdfBytes = await pdf.save();
|
||||||
|
final String timestamp =
|
||||||
|
DateFormat("ddMMyyyy_HHmm").format(DateTime.now());
|
||||||
|
final String fileName =
|
||||||
|
'Statement_${selectedUser.accountNo}_$timestamp.pdf';
|
||||||
|
|
||||||
|
String? filePath;
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final directory = Directory('/storage/emulated/0/Download');
|
||||||
|
if (!await directory.exists()) {
|
||||||
|
await directory.create(recursive: true);
|
||||||
|
}
|
||||||
|
final file = File('${directory.path}/$fileName');
|
||||||
|
await file.writeAsBytes(pdfBytes);
|
||||||
|
filePath = file.path;
|
||||||
|
} else {
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final file = await File('${tempDir.path}/$fileName').create();
|
||||||
|
await file.writeAsBytes(pdfBytes);
|
||||||
|
filePath = file.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
0,
|
||||||
|
'PDF Download Complete',
|
||||||
|
'Your account statement has been saved.',
|
||||||
|
const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'download_channel',
|
||||||
|
'Download Notifications',
|
||||||
|
channelDescription: 'Notifications for PDF downloads',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
showProgress: false,
|
||||||
|
ongoing: false,
|
||||||
|
autoCancel: true,
|
||||||
|
icon: 'notification_icon',
|
||||||
|
actions: [
|
||||||
|
AndroidNotificationAction('open_pdf', 'Open PDF',
|
||||||
|
showsUserInterface: true)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
payload: filePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('PDF saved to: $filePath'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
0,
|
||||||
|
'PDF Download Failed',
|
||||||
|
'Error saving PDF: $e',
|
||||||
|
const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'download_channel', 'Download Notifications',
|
||||||
|
channelDescription: 'Notifications for PDF downloads',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
showProgress: false,
|
||||||
|
ongoing: false,
|
||||||
|
icon: 'notification_icon'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Error saving PDF: $e'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildHeader(pw.MemoryImage logoImage, PdfColor primaryColor,
|
||||||
|
PdfColor lightGreyColor) {
|
||||||
|
return pw.Container(
|
||||||
|
margin: const pw.EdgeInsets.only(bottom: 15),
|
||||||
|
padding: const pw.EdgeInsets.only(bottom: 12),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
border: pw.Border(bottom: pw.BorderSide(color: primaryColor, width: 2)),
|
||||||
|
),
|
||||||
|
child: pw.Row(
|
||||||
|
children: [
|
||||||
|
pw.Image(logoImage, height: 55, width: 55),
|
||||||
|
pw.SizedBox(width: 12),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: primaryColor,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
letterSpacing: 0.3),
|
||||||
|
),
|
||||||
|
pw.Text(
|
||||||
|
'Head Office: Dharmsala, District Kangra (H.P.), Pin. 176215',
|
||||||
|
style: pw.TextStyle(fontSize: 10, color: lightGreyColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'e-Statement Service',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: primaryColor,
|
||||||
|
fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 2),
|
||||||
|
pw.Text(
|
||||||
|
'Generated: ${DateFormat("dd/MM/yyyy HH:mm").format(DateTime.now())}',
|
||||||
|
style: pw.TextStyle(fontSize: 9, color: lightGreyColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildAccountDetails({
|
||||||
|
required String customerName,
|
||||||
|
required String branchCode,
|
||||||
|
required String accountNo,
|
||||||
|
required String cifNumber,
|
||||||
|
required String address,
|
||||||
|
required PdfColor lightGreyColor,
|
||||||
|
required PdfColor tableBorderColor,
|
||||||
|
required PdfColor lightBgColor,
|
||||||
|
}) {
|
||||||
|
const cellPadding = pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8);
|
||||||
|
final border = pw.BorderSide(color: tableBorderColor, width: 1);
|
||||||
|
|
||||||
|
return pw.Table(
|
||||||
|
border: pw.TableBorder(
|
||||||
|
top: border,
|
||||||
|
bottom: border,
|
||||||
|
left: border,
|
||||||
|
right: border,
|
||||||
|
horizontalInside: border,
|
||||||
|
verticalInside: border,
|
||||||
|
),
|
||||||
|
columnWidths: {
|
||||||
|
0: const pw.FlexColumnWidth(1),
|
||||||
|
1: const pw.FlexColumnWidth(1),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
pw.TableRow(
|
||||||
|
children: [
|
||||||
|
_buildDetailCell(
|
||||||
|
'Customer Name', customerName, cellPadding, lightGreyColor),
|
||||||
|
_buildDetailCell(
|
||||||
|
'CIF Number', cifNumber, cellPadding, lightGreyColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.TableRow(
|
||||||
|
children: [
|
||||||
|
_buildDetailCell(
|
||||||
|
'Account Number', accountNo, cellPadding, lightGreyColor),
|
||||||
|
_buildDetailCell(
|
||||||
|
'Branch Code', branchCode, cellPadding, lightGreyColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.TableRow(
|
||||||
|
children: [
|
||||||
|
pw.Container(
|
||||||
|
padding: cellPadding,
|
||||||
|
// Using a Column inside a single cell to potentially wrap long address
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'Customer Address',
|
||||||
|
style: pw.TextStyle(fontSize: 10, color: lightGreyColor),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 2),
|
||||||
|
pw.Text(
|
||||||
|
address,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11, fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Empty container for the second column in this row, as it's a single spanning column
|
||||||
|
pw.Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildDetailCell(String label, String value, pw.EdgeInsets padding,
|
||||||
|
PdfColor lightGreyColor,
|
||||||
|
{bool vertical = false}) {
|
||||||
|
final children = [
|
||||||
|
pw.Text(
|
||||||
|
label,
|
||||||
|
style: pw.TextStyle(fontSize: 10, color: lightGreyColor),
|
||||||
|
),
|
||||||
|
if (vertical) pw.SizedBox(height: 2),
|
||||||
|
pw.Text(
|
||||||
|
value,
|
||||||
|
style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return pw.Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: vertical
|
||||||
|
? pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.start,
|
||||||
|
children: children)
|
||||||
|
: pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
children: children),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildWarning(PdfColor warningBgColor, PdfColor warningBorderColor,
|
||||||
|
PdfColor debitColor, PdfColor warningTextColor) {
|
||||||
|
return pw.Container(
|
||||||
|
margin: const pw.EdgeInsets.symmetric(vertical: 15),
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
color: warningBgColor,
|
||||||
|
border: pw.Border.all(color: warningBorderColor, width: 1),
|
||||||
|
borderRadius: pw.BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: pw.Row(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.SizedBox(width: 10),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.RichText(
|
||||||
|
text: pw.TextSpan(
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 10, color: warningTextColor, lineSpacing: 1.5),
|
||||||
|
children: [
|
||||||
|
pw.TextSpan(
|
||||||
|
text: 'NEVER SHARE ',
|
||||||
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
|
||||||
|
const pw.TextSpan(
|
||||||
|
text:
|
||||||
|
'your Card number, CVV, PIN, OTP, Internet Banking User ID, Password or URB with anyone even if the caller claims to be a bank employee. Sharing these details can lead to unauthorized access to your account.'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildPeriodHeader(
|
||||||
|
{required PdfColor primaryColor, DateTime? fromDate, DateTime? toDate}) {
|
||||||
|
String from = fromDate != null
|
||||||
|
? DateFormat('dd/MM/yyyy').format(fromDate)
|
||||||
|
: 'the beginning';
|
||||||
|
String to =
|
||||||
|
toDate != null ? DateFormat('dd/MM/yyyy').format(toDate) : 'today';
|
||||||
|
|
||||||
|
return pw.Container(
|
||||||
|
margin: const pw.EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
border: pw.Border.symmetric(
|
||||||
|
horizontal: pw.BorderSide(color: primaryColor, width: 2)),
|
||||||
|
color: PdfColor.fromHex("#f5f5f5"),
|
||||||
|
),
|
||||||
|
alignment: pw.Alignment.center,
|
||||||
|
child: pw.RichText(
|
||||||
|
text: pw.TextSpan(
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: primaryColor,
|
||||||
|
fontWeight: pw.FontWeight.bold),
|
||||||
|
children: [
|
||||||
|
const pw.TextSpan(text: 'Account statement from '),
|
||||||
|
pw.TextSpan(
|
||||||
|
text: from,
|
||||||
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
|
||||||
|
const pw.TextSpan(text: ' to '),
|
||||||
|
pw.TextSpan(
|
||||||
|
text: to, style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildTransactionsTable({
|
||||||
|
required List<Transaction> transactions,
|
||||||
|
required PdfColor primaryColor,
|
||||||
|
required PdfColor secondaryColor,
|
||||||
|
required PdfColor debitColor,
|
||||||
|
required PdfColor tableBorderColor,
|
||||||
|
}) {
|
||||||
|
//final border = pw.BorderSide(color: tableBorderColor, width: 1);
|
||||||
|
|
||||||
|
return pw.Table(
|
||||||
|
border: pw.TableBorder.all(color: tableBorderColor, width: 1),
|
||||||
|
columnWidths: {
|
||||||
|
0: const pw.FlexColumnWidth(1.5),
|
||||||
|
1: const pw.FlexColumnWidth(4),
|
||||||
|
2: const pw.FlexColumnWidth(2.2),
|
||||||
|
3: const pw.FlexColumnWidth(2.2),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
pw.TableRow(
|
||||||
|
decoration: pw.BoxDecoration(color: secondaryColor),
|
||||||
|
children: [
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
child: pw.Text('Date',
|
||||||
|
textAlign: pw.TextAlign.center,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.white)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
child: pw.Text('Mode / Particulars',
|
||||||
|
textAlign: pw.TextAlign.left,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.white)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
child: pw.Text('Withdrawals / Deposits',
|
||||||
|
textAlign: pw.TextAlign.center,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.white)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
child: pw.Text('Balance',
|
||||||
|
textAlign: pw.TextAlign.center,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...transactions.map((tx) {
|
||||||
|
final amount = double.tryParse(tx.amount ?? '0') ?? 0;
|
||||||
|
final isDebit = tx.type == 'DR';
|
||||||
|
return pw.TableRow(
|
||||||
|
decoration: const pw.BoxDecoration(color: PdfColors.white),
|
||||||
|
children: [
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
child: pw.Text(tx.date ?? '',
|
||||||
|
textAlign: pw.TextAlign.center,
|
||||||
|
style: const pw.TextStyle(fontSize: 11)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
child: pw.Text(tx.name ?? '',
|
||||||
|
textAlign: pw.TextAlign.left,
|
||||||
|
style: const pw.TextStyle(fontSize: 11)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
child: pw.Text(
|
||||||
|
'${NumberFormat.currency(locale: 'en_IN', symbol: '₹').format(amount)} ${tx.type}',
|
||||||
|
textAlign: pw.TextAlign.right,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: isDebit ? debitColor : secondaryColor,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
child: pw.Text(
|
||||||
|
'${NumberFormat.currency(locale: 'en_IN', symbol: '₹').format(double.tryParse(tx.balance ?? '0') ?? 0)} ${tx.balanceType ?? ''}',
|
||||||
|
textAlign: pw.TextAlign.right,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11, fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildLastPage() {
|
||||||
|
return pw.Container(
|
||||||
|
padding: const pw.EdgeInsets.all(20),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
border: pw.Border.all(color: PdfColors.black, width: 2),
|
||||||
|
color: PdfColor.fromHex("#fafafa"),
|
||||||
|
borderRadius: pw.BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'IMPORTANT INFORMATION:',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 15),
|
||||||
|
..._buildInfoPoints(PdfColors.black, [
|
||||||
|
'The Kangra Central Cooperative Bank Officials or representatives will NEVER ask you for your personal information i.e. your card details, passwords, PIN, CVV, OTP etc. Do not share such details with anyone over phone, SMS or email.',
|
||||||
|
'Always stay vigilant to suspicious emails. Do not open any suspicious emails.',
|
||||||
|
'Always stay vigilant when giving out sensitive personal or account information.',
|
||||||
|
'Beware of messages that instill a sense of urgency (e.g., account will expire unless you "verify" your information). Contact the Bank directly if unsure.',
|
||||||
|
'Always log out of secondary devices and reset your passwords frequently.',
|
||||||
|
'Use strong passwords: Create strong passwords that are difficult for hackers to guess.',
|
||||||
|
'Use public Wi-Fi with caution: Be careful when using public Wi-Fi networks.',
|
||||||
|
'Back up your data regularly to a secure, encrypted, off-site location.',
|
||||||
|
'Follow corporate security policies: Adhere to your company\'s security guidelines.',
|
||||||
|
'Assess third-party app permissions carefully before granting access.',
|
||||||
|
'Lock your computer and mobile phone when not in use.',
|
||||||
|
'Don\'t leave devices unattended. Keep all mobile devices, such as laptops and cell phones, physically secured.',
|
||||||
|
'Don\'t leave Bluetooth / Wireless turned on when not in use. Enable them only when needed and in a safe environment.',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<pw.Widget> _buildInfoPoints(PdfColor primaryColor, List<String> points) {
|
||||||
|
return points.map((point) {
|
||||||
|
return pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.only(bottom: 10),
|
||||||
|
child: pw.Row(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Container(
|
||||||
|
width: 15,
|
||||||
|
child: pw.Text('*',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
color: primaryColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: pw.FontWeight.bold))),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Text(
|
||||||
|
point,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
lineSpacing: 1.6,
|
||||||
|
fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildDateBox(String label, DateTime? date) {
|
Widget buildDateBox(String label, DateTime? date) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||||
|
|||||||
155
lib/features/accounts/screens/all_accounts_screen.dart
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
|
||||||
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class AllAccountsScreen extends StatefulWidget {
|
||||||
|
final List<User> users;
|
||||||
|
const AllAccountsScreen({super.key, required this.users});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AllAccountsScreen> createState() => _AllAccountsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AllAccountsScreenState extends State<AllAccountsScreen> {
|
||||||
|
final Map<String, bool> _visibilityMap = {};
|
||||||
|
|
||||||
|
String getFullAccountType(BuildContext context, String? accountType) {
|
||||||
|
// This is duplicated from dashboard_screen.dart.
|
||||||
|
// In a real app, this should be moved to a utility/helper class.
|
||||||
|
if (accountType == null || accountType.isEmpty) return 'N/A';
|
||||||
|
switch (accountType.toLowerCase()) {
|
||||||
|
case 'sa':
|
||||||
|
return AppLocalizations.of(context).savingsAccount;
|
||||||
|
case 'sb':
|
||||||
|
return AppLocalizations.of(context).savingsAccount;
|
||||||
|
case 'ln':
|
||||||
|
return AppLocalizations.of(context).loanAccount;
|
||||||
|
case 'td':
|
||||||
|
return AppLocalizations.of(context).termDeposit;
|
||||||
|
case 'rd':
|
||||||
|
return AppLocalizations.of(context).recurringDeposit;
|
||||||
|
case 'ca':
|
||||||
|
return "Current Account";
|
||||||
|
case 'cc':
|
||||||
|
return "Cash Credit Account";
|
||||||
|
case 'od':
|
||||||
|
return "Overdraft Account";
|
||||||
|
default:
|
||||||
|
return AppLocalizations.of(context).unknownAccount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context).viewall),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16.0), // Added space below the app bar
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: widget.users.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = widget.users[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: _buildAccountCard(user),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
), // Closing Expanded
|
||||||
|
], // Closing Column
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAccountCard(User user) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final accountNo = user.accountNo ?? '';
|
||||||
|
final isVisible = _visibilityMap[accountNo] ?? false;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF01A04C),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Top section: Account Type and Number
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
getFullAccountType(context, user.accountType),
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
user.accountNo ?? 'N/A',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Bottom section: Balance and Toggle
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"₹ ",
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
isVisible ? user.currentBalance ?? '0.00' : '*****',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_visibilityMap[accountNo] = !isVisible;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
isVisible ? Symbols.visibility_lock : Symbols.visibility,
|
||||||
|
color: theme.scaffoldBackgroundColor,
|
||||||
|
weight: 800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,9 @@ class TransactionDetailsScreen extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar:
|
appBar:
|
||||||
AppBar(title: Text(AppLocalizations.of(context).transactionDetails)),
|
AppBar(title: Text(AppLocalizations.of(context).transactionDetails)),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -37,8 +39,12 @@ class TransactionDetailsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Icon(
|
Icon(
|
||||||
isCredit ? Symbols.call_received : Symbols.call_made,
|
isCredit
|
||||||
color: isCredit ? Colors.green : Colors.red,
|
? Symbols.call_received
|
||||||
|
: Symbols.call_made,
|
||||||
|
color: isCredit
|
||||||
|
? const Color(0xFF10BB10)
|
||||||
|
: Theme.of(context).colorScheme.error,
|
||||||
size: 28,
|
size: 28,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -47,9 +53,9 @@ class TransactionDetailsScreen extends StatelessWidget {
|
|||||||
// Date centered
|
// Date centered
|
||||||
Text(
|
Text(
|
||||||
transaction.date ?? "",
|
transaction.date ?? "",
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.grey,
|
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@@ -57,28 +63,47 @@ class TransactionDetailsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
Divider(color: Theme.of(context).dividerColor),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 5,
|
flex: 5,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
_buildDetailRow(AppLocalizations.of(context).transactionType,
|
_buildDetailRow(
|
||||||
|
AppLocalizations.of(context).transactionType,
|
||||||
transaction.type ?? ""),
|
transaction.type ?? ""),
|
||||||
_buildDetailRow(AppLocalizations.of(context).transferType,
|
_buildDetailRow(AppLocalizations.of(context).transferType,
|
||||||
transaction.name.split("/").first ?? ""),
|
transaction.name.split("/").first ?? ""),
|
||||||
if (transaction.name.length > 12) ...[
|
// if (transaction.name.length > 12) ...[
|
||||||
_buildDetailRow(AppLocalizations.of(context).utrNo,
|
// _buildDetailRow(AppLocalizations.of(context).utrNo,
|
||||||
transaction.name.split("= ")[1].split(" ")[0] ?? ""),
|
// transaction.name.split("= ")[1].split(" ")[0] ?? ""),
|
||||||
_buildDetailRow(
|
// _buildDetailRow(
|
||||||
AppLocalizations.of(context).beneficiaryAccountNo,
|
// AppLocalizations.of(context).beneficiaryAccountNo,
|
||||||
transaction.name.split("A/C ").last ?? "")
|
// transaction.name.split("A/C ").last ?? "")
|
||||||
]
|
// ]
|
||||||
|
_buildDetailRow(AppLocalizations.of(context).details,
|
||||||
|
transaction.name),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:kmobile/api/services/user_service.dart';
|
import 'package:kmobile/api/services/user_service.dart';
|
||||||
import 'package:kmobile/core/errors/exceptions.dart';
|
import 'package:kmobile/core/errors/exceptions.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/features/auth/models/auth_token.dart';
|
||||||
|
import 'package:kmobile/security/secure_storage.dart';
|
||||||
import '../../../data/repositories/auth_repository.dart';
|
import '../../../data/repositories/auth_repository.dart';
|
||||||
import 'auth_state.dart';
|
import 'auth_state.dart';
|
||||||
|
|
||||||
class AuthCubit extends Cubit<AuthState> {
|
class AuthCubit extends Cubit<AuthState> {
|
||||||
final AuthRepository _authRepository;
|
final AuthRepository _authRepository;
|
||||||
final UserService _userService;
|
final UserService _userService;
|
||||||
|
final SecureStorage _secureStorage;
|
||||||
|
|
||||||
AuthCubit(this._authRepository, this._userService) : super(AuthInitial()) {
|
AuthCubit(this._authRepository, this._userService, this._secureStorage)
|
||||||
|
: super(AuthInitial()) {
|
||||||
checkAuthStatus();
|
checkAuthStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,22 +34,62 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
|
|
||||||
Future<void> refreshUserData() async {
|
Future<void> refreshUserData() async {
|
||||||
try {
|
try {
|
||||||
// emit(AuthLoading());
|
|
||||||
final users = await _userService.getUserDetails();
|
final users = await _userService.getUserDetails();
|
||||||
emit(Authenticated(users));
|
emit(Authenticated(users));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(AuthError('Failed to refresh user data: ${e.toString()}'));
|
emit(AuthError('Failed to refresh user data: ${e.toString()}'));
|
||||||
// Optionally, re-emit the previous state or handle as needed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> login(String customerNo, String password) async {
|
Future<void> login(String customerNo, String password) async {
|
||||||
emit(AuthLoading());
|
emit(AuthLoading());
|
||||||
try {
|
try {
|
||||||
final users = await _authRepository.login(customerNo, password);
|
final (users, authToken) =
|
||||||
emit(Authenticated(users));
|
await _authRepository.login(customerNo, password);
|
||||||
|
|
||||||
|
if (authToken.tnc == false) {
|
||||||
|
emit(ShowTncDialog(authToken, users));
|
||||||
|
} else {
|
||||||
|
await _checkMpinAndNavigate(users);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(AuthError(e is AuthException ? e.message : e.toString()));
|
emit(AuthError(e is AuthException ? e.message : e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> onTncDialogResult(
|
||||||
|
bool agreed, AuthToken authToken, List<User> users) async {
|
||||||
|
if (agreed) {
|
||||||
|
try {
|
||||||
|
await _authRepository.acceptTnc();
|
||||||
|
// The user is NOT fully authenticated yet. Just check for MPIN.
|
||||||
|
await _checkMpinAndNavigate(users);
|
||||||
|
} catch (e) {
|
||||||
|
emit(AuthError('Failed to accept TNC: $e'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit(NavigateToTncRequiredScreen());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mpinSetupCompleted() {
|
||||||
|
if (state is NavigateToMpinSetupScreen) {
|
||||||
|
final users = (state as NavigateToMpinSetupScreen).users;
|
||||||
|
emit(Authenticated(users));
|
||||||
|
} else {
|
||||||
|
// Handle unexpected state if necessary
|
||||||
|
emit(AuthError("Invalid state during MPIN setup completion."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkMpinAndNavigate(List<User> users) async {
|
||||||
|
final mpin = await _secureStorage.read('mpin');
|
||||||
|
if (mpin == null) {
|
||||||
|
// No MPIN, tell UI to navigate to MPIN setup, carrying user data
|
||||||
|
emit(NavigateToMpinSetupScreen(users));
|
||||||
|
} else {
|
||||||
|
// MPIN exists, user is authenticated
|
||||||
|
emit(Authenticated(users));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import '../../../data/models/user.dart';
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/features/auth/models/auth_token.dart';
|
||||||
|
|
||||||
abstract class AuthState extends Equatable {
|
abstract class AuthState extends Equatable {
|
||||||
|
const AuthState();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [];
|
List<Object> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthInitial extends AuthState {}
|
class AuthInitial extends AuthState {}
|
||||||
@@ -12,20 +15,44 @@ class AuthLoading extends AuthState {}
|
|||||||
|
|
||||||
class Authenticated extends AuthState {
|
class Authenticated extends AuthState {
|
||||||
final List<User> users;
|
final List<User> users;
|
||||||
|
const Authenticated(this.users);
|
||||||
Authenticated(this.users);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [users];
|
List<Object> get props => [users];
|
||||||
}
|
}
|
||||||
|
|
||||||
class Unauthenticated extends AuthState {}
|
class Unauthenticated extends AuthState {}
|
||||||
|
|
||||||
class AuthError extends AuthState {
|
class AuthError extends AuthState {
|
||||||
final String message;
|
final String message;
|
||||||
|
const AuthError(this.message);
|
||||||
AuthError(this.message);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [message];
|
List<Object> get props => [message];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- New States for Navigation and Dialog ---
|
||||||
|
|
||||||
|
// State to indicate that the TNC dialog needs to be shown
|
||||||
|
class ShowTncDialog extends AuthState {
|
||||||
|
final AuthToken authToken;
|
||||||
|
final List<User> users;
|
||||||
|
const ShowTncDialog(this.authToken, this.users);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [authToken, users];
|
||||||
|
}
|
||||||
|
|
||||||
|
// States to trigger specific navigations from the UI
|
||||||
|
class NavigateToTncRequiredScreen extends AuthState {}
|
||||||
|
|
||||||
|
class NavigateToMpinSetupScreen extends AuthState {
|
||||||
|
final List<User> users;
|
||||||
|
|
||||||
|
const NavigateToMpinSetupScreen(this.users);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [users];
|
||||||
|
}
|
||||||
|
|
||||||
|
class NavigateToDashboardScreen extends AuthState {}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class ThemeModeCubit extends Cubit<ThemeModeState> {
|
|||||||
|
|
||||||
Future<void> loadThemeMode() async {
|
Future<void> loadThemeMode() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final modeIndex = prefs.getInt('theme_mode') ?? 2; // default system
|
final modeIndex = prefs.getInt('theme_mode') ?? 0; // default system
|
||||||
final mode = ThemeMode.values[modeIndex];
|
final mode = ThemeMode.values[modeIndex];
|
||||||
emit(ThemeModeState(mode: mode));
|
emit(ThemeModeState(mode: mode));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ class ThemeState extends Equatable {
|
|||||||
List<Object?> get props => [themeType];
|
List<Object?> get props => [themeType];
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kmobile/config/theme_type.dart';
|
import 'package:kmobile/config/theme_type.dart';
|
||||||
import 'package:kmobile/config/themes.dart';
|
import 'package:kmobile/config/themes.dart';
|
||||||
|
|
||||||
class ThemeState extends Equatable {
|
class ThemeState extends Equatable {
|
||||||
final ThemeType themeType;
|
final ThemeType themeType;
|
||||||
const ThemeState({required this.themeType});
|
const ThemeState({required this.themeType});
|
||||||
@@ -35,7 +35,4 @@ class ThemeState extends Equatable {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [themeType];
|
List<Object?> get props => [themeType];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,31 @@ import 'package:equatable/equatable.dart';
|
|||||||
class AuthToken extends Equatable {
|
class AuthToken extends Equatable {
|
||||||
final String accessToken;
|
final String accessToken;
|
||||||
final DateTime expiresAt;
|
final DateTime expiresAt;
|
||||||
|
final bool tnc;
|
||||||
|
|
||||||
const AuthToken({
|
const AuthToken({
|
||||||
required this.accessToken,
|
required this.accessToken,
|
||||||
required this.expiresAt,
|
required this.expiresAt,
|
||||||
|
required this.tnc,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AuthToken.fromJson(Map<String, dynamic> json) {
|
factory AuthToken.fromJson(Map<String, dynamic> json) {
|
||||||
|
final token = json['token'];
|
||||||
|
|
||||||
|
// Safely extract tnc.mobile directly from the outer JSON
|
||||||
|
bool tncMobileValue = false; // Default to false if not found or invalid
|
||||||
|
if (json.containsKey('tnc') && json['tnc'] is Map<String, dynamic>) {
|
||||||
|
final tncMap = json['tnc'] as Map<String, dynamic>;
|
||||||
|
if (tncMap.containsKey('mobile') && tncMap['mobile'] is bool) {
|
||||||
|
tncMobileValue = tncMap['mobile'] as bool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return AuthToken(
|
return AuthToken(
|
||||||
accessToken: json['token'],
|
accessToken: token,
|
||||||
expiresAt: _decodeExpiryFromToken(json['token']),
|
expiresAt: _decodeExpiryFromToken(
|
||||||
|
token), // This method is still valid for JWT expiry
|
||||||
|
tnc: tncMobileValue, // Use the correctly extracted value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +57,45 @@ class AuthToken extends Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static bool _decodeTncFromToken(String token) {
|
||||||
|
// try {
|
||||||
|
// final parts = token.split('.');
|
||||||
|
// if (parts.length != 3) {
|
||||||
|
// throw Exception('Invalid JWT format for TNC decoding');
|
||||||
|
// }
|
||||||
|
// final payload = parts[1];
|
||||||
|
// String normalized = base64Url.normalize(payload);
|
||||||
|
// final payloadMap = json.decode(utf8.decode(base64Url.decode(normalized)));
|
||||||
|
|
||||||
|
// if (payloadMap is! Map<String, dynamic> || !payloadMap.containsKey('tnc')) {
|
||||||
|
// // If 'tnc' is not present in the payload, default to false
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// final tncValue = payloadMap['tnc'];
|
||||||
|
|
||||||
|
// // Handle different representations of 'true'
|
||||||
|
// if (tncValue is bool) {
|
||||||
|
// return tncValue;
|
||||||
|
// }
|
||||||
|
// if (tncValue is String) {
|
||||||
|
// return tncValue.toLowerCase() == 'true';
|
||||||
|
// }
|
||||||
|
// if (tncValue is int) {
|
||||||
|
// return tncValue == 1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Default to false for any other case
|
||||||
|
// return false;
|
||||||
|
// } catch (e) {
|
||||||
|
// log('Error decoding tnc from token: $e');
|
||||||
|
// // Default to false if decoding fails or 'tnc' is not found/invalid
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
bool get isExpired => DateTime.now().isAfter(expiresAt);
|
bool get isExpired => DateTime.now().isAfter(expiresAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [accessToken, expiresAt];
|
List<Object> get props => [accessToken, expiresAt, tnc];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:kmobile/di/injection.dart';
|
import 'package:kmobile/app.dart';
|
||||||
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
|
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
|
||||||
import 'package:kmobile/security/secure_storage.dart';
|
import 'package:kmobile/features/auth/screens/set_password_screen.dart';
|
||||||
import '../../../app.dart';
|
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
|
||||||
|
import 'package:kmobile/features/auth/screens/verification_screen.dart';
|
||||||
|
import 'package:kmobile/widgets/tnc_dialog.dart';
|
||||||
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import '../controllers/auth_cubit.dart';
|
import '../controllers/auth_cubit.dart';
|
||||||
import '../controllers/auth_state.dart';
|
import '../controllers/auth_state.dart';
|
||||||
|
|
||||||
@@ -22,7 +23,6 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
final _customerNumberController = TextEditingController();
|
final _customerNumberController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
bool _obscurePassword = true;
|
bool _obscurePassword = true;
|
||||||
//bool _showWelcome = true;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -31,55 +31,274 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submitForm() {
|
void _submitForm() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
|
final bool? verificationSuccess = await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => VerificationScreen(
|
||||||
|
customerNo: _customerNumberController.text.trim(),
|
||||||
|
password: _passwordController.text,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (verificationSuccess == true && mounted) {
|
||||||
context.read<AuthCubit>().login(
|
context.read<AuthCubit>().login(
|
||||||
_customerNumberController.text.trim(),
|
_customerNumberController.text.trim(),
|
||||||
_passwordController.text,
|
_passwordController.text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// return Scaffold(
|
||||||
|
// body: BlocConsumer<AuthCubit, AuthState>(
|
||||||
|
// listener: (context, state) async {
|
||||||
|
// if (state is ShowTncDialog) {
|
||||||
|
// // The dialog now returns a boolean for the 'disagree' case,
|
||||||
|
// // or it completes when the 'proceed' action is finished.
|
||||||
|
// final agreed = await showDialog<bool>(
|
||||||
|
// context: context,
|
||||||
|
// barrierDismissible: false,
|
||||||
|
// builder: (dialogContext) => TncDialog(
|
||||||
|
// onProceed: () async {
|
||||||
|
// // This function is passed to the dialog.
|
||||||
|
// // It calls the cubit and completes when the cubit's work is done.
|
||||||
|
// await context
|
||||||
|
// .read<AuthCubit>()
|
||||||
|
// .onTncDialogResult(true, state.authToken, state.users);
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // If 'agreed' is false, it means the user clicked 'Disagree'.
|
||||||
|
// if (agreed == false) {
|
||||||
|
// if (!context.mounted) return;
|
||||||
|
// context
|
||||||
|
// .read<AuthCubit>()
|
||||||
|
// .onTncDialogResult(false, state.authToken, state.users);
|
||||||
|
// }
|
||||||
|
// } else if (state is NavigateToTncRequiredScreen) {
|
||||||
|
// Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
|
||||||
|
// } else if (state is NavigateToMpinSetupScreen) {
|
||||||
|
// Navigator.of(context).push( // Use push, NOT pushReplacement
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (_) => MPinScreen(
|
||||||
|
// mode: MPinMode.set,
|
||||||
|
// onCompleted: (_) {
|
||||||
|
// // This clears the entire stack and pushes the dashboard
|
||||||
|
// Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
|
||||||
|
// MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||||
|
// (route) => false,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// } else if (state is NavigateToDashboardScreen) {
|
||||||
|
// Navigator.of(context).pushReplacement(
|
||||||
|
// MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||||
|
// );
|
||||||
|
// } else if (state is AuthError) {
|
||||||
|
// if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
|
||||||
|
// Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
// builder: (_) => SetPasswordScreen(
|
||||||
|
// customerNo: _customerNumberController.text.trim(),
|
||||||
|
// )));
|
||||||
|
// } else {
|
||||||
|
// ScaffoldMessenger.of(context)
|
||||||
|
// .showSnackBar(SnackBar(content: Text(state.message)));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// builder: (context, state) {
|
||||||
|
// // The commented out section is removed for clarity, the logic is now above.
|
||||||
|
// return Padding(
|
||||||
|
// padding: const EdgeInsets.all(24.0),
|
||||||
|
// child: Form(
|
||||||
|
// key: _formKey,
|
||||||
|
// child: Column(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
// children: [
|
||||||
|
// Image.asset(
|
||||||
|
// 'assets/images/logo.png',
|
||||||
|
// width: 150,
|
||||||
|
// height: 150,
|
||||||
|
// errorBuilder: (context, error, stackTrace) {
|
||||||
|
// return Icon(
|
||||||
|
// Icons.account_balance,
|
||||||
|
// size: 100,
|
||||||
|
// color: Theme.of(context).primaryColor,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
// Text(
|
||||||
|
// AppLocalizations.of(context).kccb,
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: 32,
|
||||||
|
// fontWeight: FontWeight.bold,
|
||||||
|
// color: Theme.of(context).primaryColor,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 48),
|
||||||
|
// TextFormField(
|
||||||
|
// controller: _customerNumberController,
|
||||||
|
// decoration: InputDecoration(
|
||||||
|
// labelText: AppLocalizations.of(context).customerNumber,
|
||||||
|
// border: const OutlineInputBorder(),
|
||||||
|
// isDense: true,
|
||||||
|
// filled: true,
|
||||||
|
// fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
// enabledBorder: OutlineInputBorder(
|
||||||
|
// borderSide: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.outline),
|
||||||
|
// ),
|
||||||
|
// focusedBorder: OutlineInputBorder(
|
||||||
|
// borderSide: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.primary,
|
||||||
|
// width: 2),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// keyboardType: TextInputType.number,
|
||||||
|
// textInputAction: TextInputAction.next,
|
||||||
|
// validator: (value) {
|
||||||
|
// if (value == null || value.isEmpty) {
|
||||||
|
// return AppLocalizations.of(context).pleaseEnterUsername;
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 24),
|
||||||
|
// TextFormField(
|
||||||
|
// controller: _passwordController,
|
||||||
|
// obscureText: _obscurePassword,
|
||||||
|
// textInputAction: TextInputAction.done,
|
||||||
|
// onFieldSubmitted: (_) => _submitForm(),
|
||||||
|
// decoration: InputDecoration(
|
||||||
|
// labelText: AppLocalizations.of(context).password,
|
||||||
|
// border: const OutlineInputBorder(),
|
||||||
|
// isDense: true,
|
||||||
|
// filled: true,
|
||||||
|
// fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
// enabledBorder: OutlineInputBorder(
|
||||||
|
// borderSide: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.outline),
|
||||||
|
// ),
|
||||||
|
// focusedBorder: OutlineInputBorder(
|
||||||
|
// borderSide: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.primary,
|
||||||
|
// width: 2),
|
||||||
|
// ),
|
||||||
|
// suffixIcon: IconButton(
|
||||||
|
// icon: Icon(
|
||||||
|
// _obscurePassword
|
||||||
|
// ? Icons.visibility
|
||||||
|
// : Icons.visibility_off,
|
||||||
|
// ),
|
||||||
|
// onPressed: () {
|
||||||
|
// setState(() {
|
||||||
|
// _obscurePassword = !_obscurePassword;
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// validator: (value) {
|
||||||
|
// if (value == null || value.isEmpty) {
|
||||||
|
// return AppLocalizations.of(context).pleaseEnterPassword;
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 24),
|
||||||
|
// SizedBox(
|
||||||
|
// width: 250,
|
||||||
|
// child: ElevatedButton(
|
||||||
|
// onPressed: state is AuthLoading ? null : _submitForm,
|
||||||
|
// style: ElevatedButton.styleFrom(
|
||||||
|
// shape: const StadiumBorder(),
|
||||||
|
// padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
// backgroundColor:
|
||||||
|
// Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
// foregroundColor: Theme.of(context).primaryColorDark,
|
||||||
|
// side: BorderSide(
|
||||||
|
// color: Theme.of(context).colorScheme.outline,
|
||||||
|
// width: 1),
|
||||||
|
// elevation: 0,
|
||||||
|
// ),
|
||||||
|
// child: state is AuthLoading
|
||||||
|
// ? const CircularProgressIndicator()
|
||||||
|
// : Text(
|
||||||
|
// AppLocalizations.of(context).login,
|
||||||
|
// style: TextStyle(
|
||||||
|
// color: Theme.of(context)
|
||||||
|
// .colorScheme
|
||||||
|
// .onPrimaryContainer),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 25),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// appBar: AppBar(title: const Text('Login')),
|
|
||||||
body: BlocConsumer<AuthCubit, AuthState>(
|
body: BlocConsumer<AuthCubit, AuthState>(
|
||||||
listener: (context, state) async {
|
listener: (context, state) {
|
||||||
if (state is Authenticated) {
|
if (state is ShowTncDialog) {
|
||||||
final storage = getIt<SecureStorage>();
|
showDialog<bool>(
|
||||||
final mpin = await storage.read('mpin');
|
context: context,
|
||||||
if (!context.mounted) return;
|
barrierDismissible: false,
|
||||||
if (mpin == null) {
|
builder: (dialogContext) => TncDialog(
|
||||||
Navigator.of(context).pushReplacement(
|
onProceed: () async {
|
||||||
|
// Pop the dialog before the cubit action
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
await context
|
||||||
|
.read<AuthCubit>()
|
||||||
|
.onTncDialogResult(true, state.authToken, state.users);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (state is NavigateToTncRequiredScreen) {
|
||||||
|
Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
|
||||||
|
} else if (state is NavigateToMpinSetupScreen) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
// Use push, NOT pushReplacement
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => MPinScreen(
|
builder: (_) => MPinScreen(
|
||||||
mode: MPinMode.set,
|
mode: MPinMode.set,
|
||||||
onCompleted: (_) {
|
onCompleted: (_) {
|
||||||
Navigator.of(
|
// Call the cubit to signal MPIN setup is complete
|
||||||
context,
|
context.read<AuthCubit>().mpinSetupCompleted();
|
||||||
rootNavigator: true,
|
|
||||||
).pushReplacement(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => const NavigationScaffold(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else if (state is Authenticated) {
|
||||||
Navigator.of(context).pushReplacement(
|
// This is the single source of truth for navigating to the dashboard
|
||||||
|
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
|
||||||
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||||
|
(route) => false,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} else if (state is AuthError) {
|
} else if (state is AuthError) {
|
||||||
ScaffoldMessenger.of(
|
if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
|
||||||
context,
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
).showSnackBar(SnackBar(content: Text(state.message)));
|
builder: (_) => SetPasswordScreen(
|
||||||
|
customerNo: _customerNumberController.text.trim(),
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text(state.message)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
// The builder part remains largely the same, focusing on UI display
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
@@ -100,7 +319,6 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Title
|
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).kccb,
|
AppLocalizations.of(context).kccb,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -110,21 +328,22 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _customerNumberController,
|
controller: _customerNumberController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).customerNumber,
|
labelText: AppLocalizations.of(context).customerNumber,
|
||||||
// prefixIcon: Icon(Icons.person),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outline),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
@@ -137,7 +356,6 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Password
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: _obscurePassword,
|
obscureText: _obscurePassword,
|
||||||
@@ -150,10 +368,13 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outline),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2),
|
||||||
),
|
),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -176,7 +397,6 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
//Login Button
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 250,
|
width: 250,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
@@ -187,50 +407,23 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).scaffoldBackgroundColor,
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
foregroundColor: Theme.of(context).primaryColorDark,
|
foregroundColor: Theme.of(context).primaryColorDark,
|
||||||
side: BorderSide(color: Theme.of(context).colorScheme.outline, width: 1),
|
side: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
width: 1),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
child: state is AuthLoading
|
child: state is AuthLoading
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: Text(
|
: Text(
|
||||||
AppLocalizations.of(context).login,
|
AppLocalizations.of(context).login,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
|
||||||
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: Text(AppLocalizations.of(context).or),
|
|
||||||
),
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 25),
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
// Register Button
|
|
||||||
SizedBox(
|
|
||||||
width: 250,
|
|
||||||
child: ElevatedButton(
|
|
||||||
//disable until registration is implemented
|
|
||||||
onPressed: null,
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
shape: const StadiumBorder(),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
backgroundColor: Theme.of(context).primaryColorLight,
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
child: Text(AppLocalizations.of(context).register),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'dart:math';
|
|||||||
// import 'dart:developer';
|
// import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kmobile/app.dart';
|
|
||||||
import 'package:kmobile/di/injection.dart';
|
import 'package:kmobile/di/injection.dart';
|
||||||
import 'package:kmobile/security/secure_storage.dart';
|
import 'package:kmobile/security/secure_storage.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
@@ -16,12 +15,18 @@ class MPinScreen extends StatefulWidget {
|
|||||||
final MPinMode mode;
|
final MPinMode mode;
|
||||||
final String? initialPin;
|
final String? initialPin;
|
||||||
final void Function(String pin)? onCompleted;
|
final void Function(String pin)? onCompleted;
|
||||||
|
final bool disableBiometric;
|
||||||
|
final String? customTitle;
|
||||||
|
final String? customConfirmTitle;
|
||||||
|
|
||||||
const MPinScreen({
|
const MPinScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.mode,
|
required this.mode,
|
||||||
this.initialPin,
|
this.initialPin,
|
||||||
this.onCompleted,
|
this.onCompleted,
|
||||||
|
this.disableBiometric = false,
|
||||||
|
this.customTitle,
|
||||||
|
this.customConfirmTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -78,7 +83,7 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
|
|||||||
CurvedAnimation(parent: _waveController, curve: Curves.easeInOut),
|
CurvedAnimation(parent: _waveController, curve: Curves.easeInOut),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.mode == MPinMode.enter) {
|
if (widget.mode == MPinMode.enter && !widget.disableBiometric) {
|
||||||
_tryBiometricBeforePin();
|
_tryBiometricBeforePin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,29 +178,36 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MPinMode.set:
|
case MPinMode.set:
|
||||||
// propagate parent onCompleted into confirm step
|
// Navigate to confirm and wait for result
|
||||||
Navigator.push(
|
final result = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => MPinScreen(
|
builder: (_) => MPinScreen(
|
||||||
mode: MPinMode.confirm,
|
mode: MPinMode.confirm,
|
||||||
initialPin: pin,
|
initialPin: pin,
|
||||||
onCompleted: widget.onCompleted, // <-- use parent callback
|
onCompleted: (confirmedPin) {
|
||||||
|
// Just pop with the pin, don't call parent callback yet
|
||||||
|
Navigator.of(context).pop(confirmedPin);
|
||||||
|
},
|
||||||
|
disableBiometric: widget.disableBiometric,
|
||||||
|
customTitle: widget.customConfirmTitle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If confirm succeeded, call parent callback
|
||||||
|
if (result != null && mounted) {
|
||||||
|
widget.onCompleted?.call(result);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case MPinMode.confirm:
|
case MPinMode.confirm:
|
||||||
if (widget.initialPin == pin) {
|
if (widget.initialPin == pin) {
|
||||||
// 1) persist the pin
|
// 1) persist the pin
|
||||||
await storage.write('mpin', pin);
|
await storage.write('mpin', pin);
|
||||||
|
|
||||||
// 3) now clear the entire navigation stack and go to your main scaffold
|
// 2) Call the onCompleted callback to let the parent handle navigation
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
|
widget.onCompleted?.call(pin);
|
||||||
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
|
||||||
(route) => false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -339,6 +351,9 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getTitle() {
|
String getTitle() {
|
||||||
|
if (widget.customTitle != null) {
|
||||||
|
return widget.customTitle!;
|
||||||
|
}
|
||||||
switch (widget.mode) {
|
switch (widget.mode) {
|
||||||
case MPinMode.enter:
|
case MPinMode.enter:
|
||||||
return AppLocalizations.of(context).enterMPIN;
|
return AppLocalizations.of(context).enterMPIN;
|
||||||
|
|||||||
296
lib/features/auth/screens/set_password_screen.dart
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/auth_service.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
import 'package:kmobile/features/auth/screens/login_screen.dart';
|
||||||
|
|
||||||
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class SetPasswordScreen extends StatefulWidget {
|
||||||
|
final String customerNo;
|
||||||
|
const SetPasswordScreen({super.key, required this.customerNo});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SetPasswordScreen> createState() => _SetPasswordScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SetPasswordStep { initial, otp, setPassword, success }
|
||||||
|
|
||||||
|
class _SetPasswordScreenState extends State<SetPasswordScreen> {
|
||||||
|
final _authService = getIt<AuthService>();
|
||||||
|
SetPasswordStep _currentStep = SetPasswordStep.initial;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
String? _otpToken;
|
||||||
|
|
||||||
|
final _otpController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _confirmPasswordController = TextEditingController();
|
||||||
|
|
||||||
|
bool _obscurePassword = true;
|
||||||
|
bool _obscureConfirmPassword = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_otpController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendOtp() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await _authService.sendOtpForSettingPassword(widget.customerNo);
|
||||||
|
setState(() {
|
||||||
|
_currentStep = SetPasswordStep.otp;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = e.toString();
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _verifyOtp() async {
|
||||||
|
if (_otpController.text.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_error = 'Please enter OTP';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final token = await _authService.verifyOtpForSettingPassword(
|
||||||
|
widget.customerNo, _otpController.text);
|
||||||
|
setState(() {
|
||||||
|
_otpToken = token;
|
||||||
|
_currentStep = SetPasswordStep.setPassword;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = AppLocalizations.of(context).invalidOtp;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setPassword() async {
|
||||||
|
if (_passwordController.text.isEmpty ||
|
||||||
|
_confirmPasswordController.text.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_error = 'Please fill both password fields';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_passwordController.text != _confirmPasswordController.text) {
|
||||||
|
setState(() {
|
||||||
|
_error = 'Passwords do not match';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_otpToken == null) {
|
||||||
|
setState(() {
|
||||||
|
_error = 'OTP token is missing. Please restart the process.';
|
||||||
|
_currentStep = SetPasswordStep.initial;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _authService.changePassword(_passwordController.text, _otpToken!);
|
||||||
|
setState(() {
|
||||||
|
_currentStep = SetPasswordStep.success;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = e.toString();
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Set New Password'),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (_error != null) ...[
|
||||||
|
Text(
|
||||||
|
_error!,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 20),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
if (_isLoading)
|
||||||
|
const CircularProgressIndicator()
|
||||||
|
else
|
||||||
|
_buildStepContent(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStepContent() {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case SetPasswordStep.initial:
|
||||||
|
return _buildInitialStep();
|
||||||
|
case SetPasswordStep.otp:
|
||||||
|
return _buildOtpStep();
|
||||||
|
case SetPasswordStep.setPassword:
|
||||||
|
return _buildSetPasswordStep();
|
||||||
|
case SetPasswordStep.success:
|
||||||
|
return _buildSuccessStep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInitialStep() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'You need to set a new password to continue. We will send an OTP to your registered mobile number.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _sendOtp,
|
||||||
|
child: const Text('Proceed'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOtpStep() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'An OTP has been sent to your registered mobile number. Please enter it below.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
TextField(
|
||||||
|
controller: _otpController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Enter OTP',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _verifyOtp,
|
||||||
|
child: const Text('Verify OTP'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSetPasswordStep() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: _obscurePassword,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'New Password',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscurePassword ? Icons.visibility : Icons.visibility_off,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_obscurePassword = !_obscurePassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
TextFormField(
|
||||||
|
controller: _confirmPasswordController,
|
||||||
|
obscureText: _obscureConfirmPassword,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Confirm New Password',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscureConfirmPassword
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_obscureConfirmPassword = !_obscureConfirmPassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _setPassword,
|
||||||
|
child: const Text('Set Password'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSuccessStep() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.check_circle, color: Colors.green, size: 80),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
'Password set successfully!',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'You can now log in with your new password.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Back to Login'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
177
lib/features/auth/screens/sms_verification_helper.dart
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/send_sms_service.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class SmsVerificationHelper {
|
||||||
|
final SmsService _smsService = SmsService();
|
||||||
|
|
||||||
|
Future<void> _showPermanentlyDeniedDialog(BuildContext context) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text("Permission Required"),
|
||||||
|
content: const Text(
|
||||||
|
"SMS and Phone permissions are required for device verification. Please enable them in your app settings to continue."),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text("Open Settings"),
|
||||||
|
onPressed: () {
|
||||||
|
openAppSettings(); // Opens the phone's settings screen for this app
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showRestrictedSmsDialog(BuildContext context) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text("SMS Permission Restricted"),
|
||||||
|
content: const SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"It seems your device is restricting this app from sending SMS messages, which is required for verification. Please follow these steps to enable it:\n"),
|
||||||
|
Text("1. Open your device Settings.",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
Text("2. Go to 'Apps' or 'Apps & notifications'."),
|
||||||
|
Text("3. Find and tap on this app ('KMobile')."),
|
||||||
|
Text("4. Tap on the three dots (⋮) in the top right corner."),
|
||||||
|
Text(
|
||||||
|
"5. Select 'Allow restricted settings' and confirm. This is crucial to allow SMS permission."),
|
||||||
|
Text("6. Now you have two options to allow SMS permission:"),
|
||||||
|
Text(
|
||||||
|
" a. Tap on 'Permissions', then find 'SMS' is set to 'Allow'."),
|
||||||
|
Text(
|
||||||
|
" b. Alternatively, you can return to the KMobile app, and the SMS permission pop-up should appear again, allowing you to grant it directly."),
|
||||||
|
Text(
|
||||||
|
"\nSome devices have an additional setting for 'Premium SMS'. If the above doesn't work, look for a 'Premium SMS access' setting (you can search for it in your Settings app) and set it to 'Always Allow' for this app.\n"),
|
||||||
|
Text(
|
||||||
|
"After you've enabled the permission, please come back to the app."),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text("I've Enabled It"),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> initiateSmsSequence({
|
||||||
|
required BuildContext context,
|
||||||
|
}) async {
|
||||||
|
bool hasPermission = false;
|
||||||
|
|
||||||
|
// --- PERMISSION LOOP ---
|
||||||
|
while (!hasPermission) {
|
||||||
|
// handleSmsPermission will check the status and request if not granted.
|
||||||
|
final status = await _smsService.handleSmsPermission();
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case PermissionStatusResult.granted:
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("Permissions Granted! Proceeding..."),
|
||||||
|
duration: Duration(seconds: 2)),
|
||||||
|
);
|
||||||
|
hasPermission = true; // This will break the loop
|
||||||
|
break;
|
||||||
|
case PermissionStatusResult.denied:
|
||||||
|
// The user denied the permission. We show a dialog to explain why we need it
|
||||||
|
// and give them a chance to cancel or let the loop try again.
|
||||||
|
final tryAgain = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text("Permission Required"),
|
||||||
|
content: const Text(
|
||||||
|
"This app requires SMS and Phone permissions to verify your device. Please grant the permissions to continue."),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text("Try Again"),
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (tryAgain != true) {
|
||||||
|
return null; // User chose to cancel.
|
||||||
|
}
|
||||||
|
// If they chose "Try Again", the loop will repeat.
|
||||||
|
break;
|
||||||
|
case PermissionStatusResult.permanentlyDenied:
|
||||||
|
await _showPermanentlyDeniedDialog(context);
|
||||||
|
// Give user time to come back from settings
|
||||||
|
await Future.delayed(const Duration(seconds: 5));
|
||||||
|
// The loop will repeat and re-check the status.
|
||||||
|
break;
|
||||||
|
case PermissionStatusResult.restricted:
|
||||||
|
await _showRestrictedSmsDialog(context);
|
||||||
|
// Give user time to come back from settings
|
||||||
|
await Future.delayed(const Duration(seconds: 5));
|
||||||
|
// The loop will repeat and re-check the status.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SMS SENDING LOOP ---
|
||||||
|
// This part will only be reached if hasPermission is true.
|
||||||
|
int retries = 3;
|
||||||
|
while (retries > 0) {
|
||||||
|
var uuid = const Uuid();
|
||||||
|
String uniqueId = uuid.v4();
|
||||||
|
String smsMessage = uniqueId;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content:
|
||||||
|
Text("Attempting to send verification SMS... (${4 - retries})"),
|
||||||
|
duration: const Duration(seconds: 2)),
|
||||||
|
);
|
||||||
|
|
||||||
|
bool isSmsSent = await _smsService.sendVerificationSms(
|
||||||
|
context: context,
|
||||||
|
destinationNumber: '9580079717', // Replace with your number
|
||||||
|
message: smsMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSmsSent) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("SMS sent successfully!"),
|
||||||
|
duration: Duration(seconds: 2)),
|
||||||
|
);
|
||||||
|
return uniqueId;
|
||||||
|
} else {
|
||||||
|
retries--;
|
||||||
|
if (retries > 0) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("SMS failed to send. Retrying in 5 seconds..."),
|
||||||
|
duration: Duration(seconds: 4)),
|
||||||
|
);
|
||||||
|
await Future.delayed(const Duration(seconds: 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all retries fail
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -10,11 +12,29 @@ class SplashScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SplashScreenState extends State<SplashScreen> {
|
class _SplashScreenState extends State<SplashScreen> {
|
||||||
|
String _version = '';
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadVersion() async {
|
||||||
|
final PackageInfo info = await PackageInfo.fromPlatform();
|
||||||
|
if (mounted) {
|
||||||
|
// Check if the widget is still in the tree
|
||||||
|
setState(() {
|
||||||
|
_version = 'Version ${info.version} (${info.buildNumber})';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
fit: StackFit.expand,
|
||||||
|
children: <Widget>[
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/kconnect2.webp',
|
'assets/images/kconnect2.webp',
|
||||||
@@ -26,7 +46,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).kconnect,
|
AppLocalizations.of(context).kccbMobile,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 36,
|
fontSize: 36,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -51,8 +71,20 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(color: Color(0xFFFFFFFF)),
|
||||||
color: Color(0xFFFFFFFF)),
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 90,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Text(
|
||||||
|
_version,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
58
lib/features/auth/screens/tnc_required_screen.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TncRequiredScreen extends StatelessWidget {
|
||||||
|
// Renamed class
|
||||||
|
const TncRequiredScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static const routeName = '/tnc-required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Terms and Conditions'),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'You must accept the Terms and Conditions to use the application.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
// This will take the user back to the previous screen
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Go Back'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
170
lib/features/auth/screens/verification_screen.dart
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/auth_service.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
import 'package:kmobile/features/auth/screens/sms_verification_helper.dart';
|
||||||
|
|
||||||
|
class VerificationScreen extends StatefulWidget {
|
||||||
|
final String customerNo;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
const VerificationScreen({
|
||||||
|
super.key,
|
||||||
|
required this.customerNo,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VerificationScreen> createState() => _VerificationScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VerificationScreenState extends State<VerificationScreen> {
|
||||||
|
String _statusMessage = "Starting verification...";
|
||||||
|
Timer? _timer;
|
||||||
|
int _countdown = 120;
|
||||||
|
bool _isVerifying = false;
|
||||||
|
bool _verificationFailed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_startVerificationProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startTimer() {
|
||||||
|
_timer?.cancel(); // Cancel any existing timer
|
||||||
|
_countdown = 120;
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
if (_countdown > 0) {
|
||||||
|
setState(() {
|
||||||
|
_countdown--;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_timer?.cancel();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "Verification timed out. Please try again.";
|
||||||
|
_isVerifying = false;
|
||||||
|
_verificationFailed = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startVerificationProcess() async {
|
||||||
|
setState(() {
|
||||||
|
_isVerifying = true;
|
||||||
|
_verificationFailed = false;
|
||||||
|
_statusMessage = "Starting verification...";
|
||||||
|
});
|
||||||
|
_startTimer();
|
||||||
|
|
||||||
|
// 1. Send SMS
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "SMS sending...";
|
||||||
|
});
|
||||||
|
final smsHelper = SmsVerificationHelper();
|
||||||
|
final uuid = await smsHelper.initiateSmsSequence(context: context);
|
||||||
|
|
||||||
|
if (uuid != null && mounted) {
|
||||||
|
// SMS sending was successful, now wait before verifying.
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "SMS sent. Waiting for network delivery...";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adding a 10-second delay to account for SMS network latency.
|
||||||
|
await Future.delayed(const Duration(seconds: 10));
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// 2. Verify SIM
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "Verifying with server...";
|
||||||
|
});
|
||||||
|
|
||||||
|
final authService = getIt<AuthService>();
|
||||||
|
try {
|
||||||
|
await authService.simVerify(uuid, widget.customerNo);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "Verification successful!";
|
||||||
|
_isVerifying = false;
|
||||||
|
});
|
||||||
|
_timer?.cancel();
|
||||||
|
|
||||||
|
// Pop with success result
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
if(e.toString().contains("NOT_VERIFIED")){
|
||||||
|
_statusMessage = "SIM details not found. Please ensure your mobile number is registered with the bank.";
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
_statusMessage = e.toString();
|
||||||
|
}
|
||||||
|
_isVerifying = false;
|
||||||
|
_verificationFailed = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_statusMessage =
|
||||||
|
"SMS sending failed. Please check permissions and try again.";
|
||||||
|
_isVerifying = false;
|
||||||
|
_verificationFailed = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Device Verification"),
|
||||||
|
automaticallyImplyLeading: !_isVerifying,
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (_isVerifying) const CircularProgressIndicator(),
|
||||||
|
if (!_isVerifying && _verificationFailed)
|
||||||
|
const Icon(Icons.error_outline, color: Colors.red, size: 50),
|
||||||
|
if (!_isVerifying && !_verificationFailed)
|
||||||
|
const Icon(Icons.check_circle_outline,
|
||||||
|
color: Colors.green, size: 50),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
_statusMessage,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (_isVerifying)
|
||||||
|
Text(
|
||||||
|
"Time remaining: $_countdown seconds",
|
||||||
|
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
||||||
|
),
|
||||||
|
if (_verificationFailed && !_isVerifying) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _startVerificationProcess,
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@ class AddBeneficiaryScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _accountNumberFieldKey = GlobalKey<FormFieldState>();
|
||||||
|
final _confirmAccountNumberFieldKey = GlobalKey<FormFieldState>();
|
||||||
|
final _ifscFieldKey = GlobalKey<FormFieldState>();
|
||||||
final TextEditingController accountNumberController = TextEditingController();
|
final TextEditingController accountNumberController = TextEditingController();
|
||||||
final TextEditingController confirmAccountNumberController =
|
final TextEditingController confirmAccountNumberController =
|
||||||
TextEditingController();
|
TextEditingController();
|
||||||
@@ -32,6 +34,7 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
final TextEditingController branchNameController = TextEditingController();
|
final TextEditingController branchNameController = TextEditingController();
|
||||||
final TextEditingController ifscController = TextEditingController();
|
final TextEditingController ifscController = TextEditingController();
|
||||||
final TextEditingController phoneController = TextEditingController();
|
final TextEditingController phoneController = TextEditingController();
|
||||||
|
final _ifscFocusNode = FocusNode();
|
||||||
final service = getIt<BeneficiaryService>();
|
final service = getIt<BeneficiaryService>();
|
||||||
|
|
||||||
bool _isValidating = false;
|
bool _isValidating = false;
|
||||||
@@ -43,6 +46,11 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_ifscFocusNode.addListener(() {
|
||||||
|
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
|
||||||
|
_validateIFSC();
|
||||||
|
}
|
||||||
|
});
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
accountType = 'Savings';
|
accountType = 'Savings';
|
||||||
@@ -50,15 +58,29 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
accountNumberController.dispose();
|
||||||
|
confirmAccountNumberController.dispose();
|
||||||
|
nameController.dispose();
|
||||||
|
bankNameController.dispose();
|
||||||
|
branchNameController.dispose();
|
||||||
|
ifscController.dispose();
|
||||||
|
phoneController.dispose();
|
||||||
|
_ifscFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _validateIFSC() async {
|
void _validateIFSC() async {
|
||||||
var beneficiaryService = getIt<BeneficiaryService>();
|
var beneficiaryService = getIt<BeneficiaryService>();
|
||||||
final ifsc = ifscController.text.trim().toUpperCase();
|
final ifsc = ifscController.text.trim().toUpperCase();
|
||||||
if (ifsc.isEmpty) return;
|
if (ifsc.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
final result = await beneficiaryService.validateIFSC(ifsc);
|
final result = await beneficiaryService.validateIFSC(ifsc);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (result.bankName == '') {
|
if (result.bankName.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
|
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
|
||||||
);
|
);
|
||||||
@@ -69,6 +91,23 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
branchNameController.text = result.branchName;
|
branchNameController.text = result.branchName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
final errorMessage = e.toString().toUpperCase();
|
||||||
|
String snackbarMessage =
|
||||||
|
AppLocalizations.of(context).somethingWentWrong;
|
||||||
|
|
||||||
|
if (errorMessage.contains('INVALID') && errorMessage.contains('IFSC')) {
|
||||||
|
snackbarMessage = AppLocalizations.of(context).invalidIfsc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(snackbarMessage)),
|
||||||
|
);
|
||||||
|
bankNameController.clear();
|
||||||
|
branchNameController.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _validateBeneficiary() async {
|
void _validateBeneficiary() async {
|
||||||
@@ -225,7 +264,9 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Form(
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -237,6 +278,7 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
key: _accountNumberFieldKey,
|
||||||
controller: accountNumberController,
|
controller: accountNumberController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(
|
labelText: AppLocalizations.of(
|
||||||
@@ -267,6 +309,7 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Confirm Account Number
|
// Confirm Account Number
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
key: _confirmAccountNumberFieldKey,
|
||||||
controller: confirmAccountNumberController,
|
controller: confirmAccountNumberController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(
|
labelText: AppLocalizations.of(
|
||||||
@@ -294,31 +337,34 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
focusNode: _ifscFocusNode,
|
||||||
|
key: _ifscFieldKey,
|
||||||
controller: ifscController,
|
controller: ifscController,
|
||||||
maxLength: 11,
|
maxLength: 11,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
LengthLimitingTextInputFormatter(11),
|
LengthLimitingTextInputFormatter(11),
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).ifscCode,
|
labelText:
|
||||||
|
AppLocalizations.of(context).ifscCode,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
textCapitalization: TextCapitalization.characters,
|
textCapitalization: TextCapitalization.characters,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onFieldSubmitted: (_) {
|
|
||||||
_validateIFSC();
|
|
||||||
},
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
final trimmed = value.trim().toUpperCase();
|
final trimmed = value.trim().toUpperCase();
|
||||||
if (trimmed.length < 11) {
|
if (trimmed.length < 11) {
|
||||||
// clear bank/branch if backspace or changed
|
// clear bank/branch if backspace or changed
|
||||||
bankNameController.clear();
|
bankNameController.clear();
|
||||||
branchNameController.clear();
|
branchNameController.clear();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
|
final pattern =
|
||||||
|
RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return AppLocalizations.of(context).enterIfsc;
|
return AppLocalizations.of(context).enterIfsc;
|
||||||
} else if (!pattern.hasMatch(
|
} else if (!pattern.hasMatch(
|
||||||
@@ -332,12 +378,14 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// 🔹 Bank Name (Disabled)
|
// Bank Name (Disabled)
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: bankNameController,
|
controller: bankNameController,
|
||||||
enabled: false, // changed from readOnly to disabled
|
enabled:
|
||||||
|
false, // changed from readOnly to disabled
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).bankName,
|
labelText:
|
||||||
|
AppLocalizations.of(context).bankName,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
@@ -346,9 +394,11 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
// 🔹 Branch Name (Disabled)
|
// 🔹 Branch Name (Disabled)
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: branchNameController,
|
controller: branchNameController,
|
||||||
enabled: false, // changed from readOnly to disabled
|
enabled:
|
||||||
|
false, // changed from readOnly to disabled
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).branchName,
|
labelText:
|
||||||
|
AppLocalizations.of(context).branchName,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
@@ -376,9 +426,10 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
validator: (value) => value == null ||
|
validator: (value) =>
|
||||||
value.isEmpty
|
value == null || value.isEmpty
|
||||||
? AppLocalizations.of(context).nameRequired
|
? AppLocalizations.of(context)
|
||||||
|
.nameRequired
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -390,18 +441,26 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isValidating
|
onPressed: _isValidating ||
|
||||||
|
ifscController.text.length != 11
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (confirmAccountNumberController
|
final isAccountValid =
|
||||||
.text ==
|
_accountNumberFieldKey
|
||||||
accountNumberController.text) {
|
.currentState!
|
||||||
|
.validate();
|
||||||
|
final isConfirmAccountValid =
|
||||||
|
_confirmAccountNumberFieldKey
|
||||||
|
.currentState!
|
||||||
|
.validate();
|
||||||
|
final isIfscValid = _ifscFieldKey
|
||||||
|
.currentState!
|
||||||
|
.validate();
|
||||||
|
|
||||||
|
if (isAccountValid &&
|
||||||
|
isConfirmAccountValid &&
|
||||||
|
isIfscValid) {
|
||||||
_validateBeneficiary();
|
_validateBeneficiary();
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_validationError =
|
|
||||||
'Please enter a valid and matching account number.';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: _isValidating
|
child: _isValidating
|
||||||
@@ -421,7 +480,8 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: accountType,
|
value: accountType,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).accountType,
|
labelText:
|
||||||
|
AppLocalizations.of(context).accountType,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
@@ -454,8 +514,8 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
validator: (value) =>
|
validator: (value) => value == null ||
|
||||||
value == null || value.length != 10
|
value.length != 10
|
||||||
? AppLocalizations.of(context).enterValidPhone
|
? AppLocalizations.of(context).enterValidPhone
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -476,8 +536,9 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
foregroundColor:
|
foregroundColor: Theme.of(context)
|
||||||
Theme.of(context).colorScheme.onPrimaryContainer),
|
.colorScheme
|
||||||
|
.onPrimaryContainer),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).validateAndAdd,
|
AppLocalizations.of(context).validateAndAdd,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
@@ -488,6 +549,22 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:kmobile/data/models/beneficiary.dart';
|
import 'package:kmobile/data/models/beneficiary.dart';
|
||||||
import 'package:kmobile/di/injection.dart';
|
import 'package:kmobile/di/injection.dart';
|
||||||
import 'package:kmobile/widgets/bank_logos.dart';
|
import 'package:kmobile/widgets/bank_logos.dart';
|
||||||
@@ -6,23 +7,48 @@ import 'package:kmobile/api/services/beneficiary_service.dart';
|
|||||||
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class BeneficiaryDetailsScreen extends StatelessWidget {
|
class BeneficiaryDetailsScreen extends StatefulWidget {
|
||||||
final Beneficiary beneficiary;
|
final Beneficiary beneficiary;
|
||||||
|
|
||||||
BeneficiaryDetailsScreen({super.key, required this.beneficiary});
|
const BeneficiaryDetailsScreen({super.key, required this.beneficiary});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BeneficiaryDetailsScreen> createState() =>
|
||||||
|
_BeneficiaryDetailsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BeneficiaryDetailsScreenState extends State<BeneficiaryDetailsScreen> {
|
||||||
final service = getIt<BeneficiaryService>();
|
final service = getIt<BeneficiaryService>();
|
||||||
|
late String _currentLimit;
|
||||||
|
final _limitController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentLimit = (widget.beneficiary.transactionLimit == null ||
|
||||||
|
widget.beneficiary.transactionLimit!.isEmpty)
|
||||||
|
? 'N/A'
|
||||||
|
: widget.beneficiary.transactionLimit!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_limitController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _deleteBeneficiary(BuildContext context) async {
|
void _deleteBeneficiary(BuildContext context) async {
|
||||||
try {
|
try {
|
||||||
await service.deleteBeneficiary(beneficiary.accountNo);
|
await service.deleteBeneficiary(widget.beneficiary.accountNo);
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_showSuccessDialog(context);
|
_showSuccessDialog(context);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Failed to delete beneficiary: $e')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'${AppLocalizations.of(context).failedToDeleteBeneficiary} : $e')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,11 +58,12 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Success'),
|
title: Text(AppLocalizations.of(context).success),
|
||||||
content: const Text('Beneficiary deleted successfully.'),
|
content:
|
||||||
|
Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('OK'),
|
child: Text(AppLocalizations.of(context).ok),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
},
|
},
|
||||||
@@ -52,20 +79,19 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Delete Beneficiary'),
|
title: Text(AppLocalizations.of(context).deleteBeneficiary),
|
||||||
content:
|
content: Text(AppLocalizations.of(context)
|
||||||
const Text('Are you sure you want to delete this beneficiary?'),
|
.areYouSureYouWantToDeleteThisBeneficiary),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Cancel'),
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Delete'),
|
child: Text(AppLocalizations.of(context).delete),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
//Navigator.of(context).pop();
|
|
||||||
_deleteBeneficiary(context);
|
_deleteBeneficiary(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -75,6 +101,73 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showEditLimitDialog() async {
|
||||||
|
_limitController.text = _currentLimit == 'N/A' ? '' : _currentLimit;
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
final localizations = AppLocalizations.of(dialogContext);
|
||||||
|
final theme = Theme.of(dialogContext);
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(localizations.editLimit),
|
||||||
|
content: TextField(
|
||||||
|
controller: _limitController,
|
||||||
|
autofocus: true,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(RegExp(r'^\d+')),
|
||||||
|
],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: localizations.limitAmount,
|
||||||
|
prefixText: '₹',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: Text(localizations.cancel),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final value = _limitController.text;
|
||||||
|
if (value.isEmpty || int.tryParse(value) == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.updateLimit(
|
||||||
|
beneficiaryAccountNo: widget.beneficiary.accountNo,
|
||||||
|
newLimit: value,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_currentLimit = value;
|
||||||
|
});
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(localizations.limitUpdatedSuccess),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("${localizations.error}: $e"),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(localizations.save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -82,7 +175,9 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
title: Text(AppLocalizations.of(context).beneficiarydetails),
|
title: Text(AppLocalizations.of(context).beneficiarydetails),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -92,11 +187,11 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
child: getBankLogo(beneficiary.bankName, context),
|
child: getBankLogo(widget.beneficiary.bankName, context),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(
|
Text(
|
||||||
beneficiary.name,
|
widget.beneficiary.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20, fontWeight: FontWeight.bold),
|
fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
@@ -104,26 +199,28 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildDetailRow('${AppLocalizations.of(context).bankName} ',
|
_buildDetailRow('${AppLocalizations.of(context).bankName} ',
|
||||||
beneficiary.bankName ?? 'N/A'),
|
widget.beneficiary.bankName ?? 'N/A'),
|
||||||
_buildDetailRow('${AppLocalizations.of(context).accountNumber} ',
|
_buildDetailRow(
|
||||||
beneficiary.accountNo),
|
'${AppLocalizations.of(context).accountNumber} ',
|
||||||
_buildDetailRow('${AppLocalizations.of(context).accountType} ',
|
widget.beneficiary.accountNo),
|
||||||
beneficiary.accountType),
|
_buildDetailRow(
|
||||||
|
'${AppLocalizations.of(context).accountType} ',
|
||||||
|
widget.beneficiary.accountType),
|
||||||
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
|
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
|
||||||
beneficiary.ifscCode),
|
widget.beneficiary.ifscCode),
|
||||||
_buildDetailRow('${AppLocalizations.of(context).branchName} ',
|
_buildDetailRow('${AppLocalizations.of(context).branchName} ',
|
||||||
beneficiary.branchName ?? 'N/A'),
|
widget.beneficiary.branchName ?? 'N/A'),
|
||||||
|
_buildDetailRow(
|
||||||
|
"Beneficiary Transactional Limit", _currentLimit),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
// ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
// onPressed: () {
|
onPressed: _showEditLimitDialog,
|
||||||
// // Set Transaction Limit for this beneficiary
|
icon: const Icon(Icons.currency_rupee),
|
||||||
// },
|
label: Text(AppLocalizations.of(context).editLimit),
|
||||||
// icon: const Icon(Icons.currency_rupee),
|
),
|
||||||
// label: const Text('Set Limit'),
|
|
||||||
// ),
|
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Delete beneficiary option
|
// Delete beneficiary option
|
||||||
@@ -137,6 +234,22 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -161,3 +274,4 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,10 @@ class _BeneficiaryResultPageState extends State<BeneficiaryResultPage> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).done,
|
AppLocalizations.of(context).done,
|
||||||
style: const TextStyle(fontSize: 18), // slightly bigger text
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer), // slightly bigger text
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,21 +21,47 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
|
|||||||
var service = getIt<BeneficiaryService>();
|
var service = getIt<BeneficiaryService>();
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
List<Beneficiary> _beneficiaries = [];
|
List<Beneficiary> _beneficiaries = [];
|
||||||
|
List<Beneficiary> _filteredBeneficiaries = [];
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadBeneficiaries();
|
_loadBeneficiaries();
|
||||||
|
_searchController.addListener(() {
|
||||||
|
_filterBeneficiaries(_searchController.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadBeneficiaries() async {
|
Future<void> _loadBeneficiaries() async {
|
||||||
final data = await service.fetchBeneficiaryList();
|
final data = await service.fetchBeneficiaryList();
|
||||||
setState(() {
|
setState(() {
|
||||||
_beneficiaries = data;
|
_beneficiaries = data;
|
||||||
|
_filteredBeneficiaries = data;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _filterBeneficiaries(String query) {
|
||||||
|
setState(() {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
_filteredBeneficiaries = _beneficiaries;
|
||||||
|
} else {
|
||||||
|
_filteredBeneficiaries = _beneficiaries.where((beneficiary) {
|
||||||
|
final lowerQuery = query.toLowerCase();
|
||||||
|
return beneficiary.name.toLowerCase().contains(lowerQuery) ||
|
||||||
|
beneficiary.accountNo.toLowerCase().contains(lowerQuery);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildShimmerList() {
|
Widget _buildShimmerList() {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: 6,
|
itemCount: 6,
|
||||||
@@ -63,15 +89,17 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBeneficiaryList() {
|
Widget _buildBeneficiaryList() {
|
||||||
if (_beneficiaries.isEmpty) {
|
if (_filteredBeneficiaries.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(AppLocalizations.of(context).noBeneficiaryFound));
|
child: Text(AppLocalizations.of(context).noBeneficiaryFound));
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: _beneficiaries.length,
|
itemCount: _filteredBeneficiaries.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = _beneficiaries[index];
|
final item = _filteredBeneficiaries[index];
|
||||||
return ListTile(
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
child: ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@@ -97,6 +125,7 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -109,7 +138,47 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context).beneficiaries),
|
title: Text(AppLocalizations.of(context).beneficiaries),
|
||||||
),
|
),
|
||||||
body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText:
|
||||||
|
AppLocalizations.of(context).searchByNameOrAccountHint,
|
||||||
|
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
floatingActionButton: Padding(
|
floatingActionButton: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ class _BlockCardScreen extends State<BlockCardScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).blockCard,
|
AppLocalizations.of(context).blockCard,
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@@ -100,18 +100,21 @@ class _BlockCardScreen extends State<BlockCardScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: const OutlineInputBorder(
|
enabledBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black),
|
borderSide: BorderSide(color: Colors.black),
|
||||||
),
|
),
|
||||||
focusedBorder: const OutlineInputBorder(
|
focusedBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
borderSide:
|
||||||
|
BorderSide(color: Colors.black, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
validator: (value) => value != null && value.length == 3
|
validator: (value) =>
|
||||||
|
value != null && value.length == 3
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context).cvv3Digits,
|
: AppLocalizations.of(context).cvv3Digits,
|
||||||
),
|
),
|
||||||
@@ -128,15 +131,18 @@ class _BlockCardScreen extends State<BlockCardScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: const OutlineInputBorder(
|
enabledBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black),
|
borderSide: BorderSide(color: Colors.black),
|
||||||
),
|
),
|
||||||
focusedBorder: const OutlineInputBorder(
|
focusedBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
borderSide:
|
||||||
|
BorderSide(color: Colors.black, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: (value) => value != null && value.isNotEmpty
|
validator: (value) => value != null &&
|
||||||
|
value.isNotEmpty
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context).selectExpiryDate,
|
: AppLocalizations.of(context).selectExpiryDate,
|
||||||
),
|
),
|
||||||
@@ -188,6 +194,22 @@ class _BlockCardScreen extends State<BlockCardScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class CardDetailsScreen extends StatelessWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("My Cards"),
|
title: const Text("My Cards"),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: const [
|
children: const [
|
||||||
@@ -31,6 +33,8 @@ class CardDetailsScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,12 +90,12 @@ class CardTile extends StatelessWidget {
|
|||||||
const Text(
|
const Text(
|
||||||
"Kangra Central Co-operative Bank",
|
"Kangra Central Co-operative Bank",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Color.fromARGB(255, 238, 237, 237),
|
||||||
fontSize: 18,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.visible,
|
||||||
maxLines: 1,
|
maxLines: 2,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: unused_import
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kmobile/features/card/screens/block_card_screen.dart';
|
import 'package:kmobile/features/card/screens/block_card_screen.dart';
|
||||||
import 'package:kmobile/features/card/screens/card_details_screen.dart';
|
import 'package:kmobile/features/card/screens/card_details_screen.dart';
|
||||||
@@ -17,22 +19,22 @@ class _CardManagementScreen extends State<CardManagementScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).cardManagement,
|
AppLocalizations.of(context).cardManagement,
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
ListView(
|
||||||
children: [
|
children: [
|
||||||
CardManagementTile(
|
CardManagementTile(
|
||||||
icon: Symbols.add,
|
icon: Symbols.add,
|
||||||
label: AppLocalizations.of(context).applyDebitCard,
|
label: AppLocalizations.of(context).applyDebitCard,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
|
disabled: true, // Add this
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
CardManagementTile(
|
CardManagementTile(
|
||||||
icon: Symbols.remove_moderator,
|
icon: Symbols.remove_moderator,
|
||||||
label: AppLocalizations.of(context).blockUnblockCard,
|
label: AppLocalizations.of(context).blockUnblockCard,
|
||||||
@@ -44,8 +46,9 @@ class _CardManagementScreen extends State<CardManagementScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
disabled: true,
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
CardManagementTile(
|
CardManagementTile(
|
||||||
icon: Symbols.password_2,
|
icon: Symbols.password_2,
|
||||||
label: AppLocalizations.of(context).changeCardPin,
|
label: AppLocalizations.of(context).changeCardPin,
|
||||||
@@ -57,8 +60,9 @@ class _CardManagementScreen extends State<CardManagementScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
disabled: false,
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
CardManagementTile(
|
CardManagementTile(
|
||||||
icon: Symbols.payment_card,
|
icon: Symbols.payment_card,
|
||||||
label: AppLocalizations.of(context).viewCardDeatils,
|
label: AppLocalizations.of(context).viewCardDeatils,
|
||||||
@@ -70,8 +74,25 @@ class _CardManagementScreen extends State<CardManagementScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
disabled: false,
|
||||||
|
),
|
||||||
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -82,21 +103,36 @@ class CardManagementTile extends StatelessWidget {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final bool disabled;
|
||||||
|
|
||||||
const CardManagementTile({
|
const CardManagementTile({
|
||||||
super.key,
|
super.key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.disabled = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(icon),
|
leading: Icon(
|
||||||
title: Text(label),
|
icon,
|
||||||
trailing: const Icon(Symbols.arrow_right, size: 20),
|
color: disabled ? theme.disabledColor : null,
|
||||||
onTap: onTap,
|
),
|
||||||
|
title: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: disabled ? theme.disabledColor : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Symbols.arrow_right,
|
||||||
|
size: 20,
|
||||||
|
color: disabled ? theme.disabledColor : null,
|
||||||
|
),
|
||||||
|
onTap: disabled ? null : onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).cardDetails,
|
AppLocalizations.of(context).changeCardPin,
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@@ -66,12 +66,8 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: const OutlineInputBorder(
|
enabledBorder: const OutlineInputBorder(),
|
||||||
borderSide: BorderSide(color: Colors.black),
|
focusedBorder: const OutlineInputBorder(),
|
||||||
),
|
|
||||||
focusedBorder: const OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
@@ -90,18 +86,16 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor:
|
||||||
enabledBorder: const OutlineInputBorder(
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
borderSide: BorderSide(color: Colors.black),
|
enabledBorder: const OutlineInputBorder(),
|
||||||
),
|
focusedBorder: const OutlineInputBorder(),
|
||||||
focusedBorder: const OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
validator: (value) => value != null && value.length == 3
|
validator: (value) =>
|
||||||
|
value != null && value.length == 3
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context).cvv3Digits,
|
: AppLocalizations.of(context).cvv3Digits,
|
||||||
),
|
),
|
||||||
@@ -118,15 +112,13 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor:
|
||||||
enabledBorder: const OutlineInputBorder(
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
borderSide: BorderSide(color: Colors.black),
|
enabledBorder: const OutlineInputBorder(),
|
||||||
|
focusedBorder: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
focusedBorder: const OutlineInputBorder(
|
validator: (value) => value != null &&
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
value.isNotEmpty
|
||||||
),
|
|
||||||
),
|
|
||||||
validator: (value) => value != null && value.isNotEmpty
|
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context).selectExpiryDate,
|
: AppLocalizations.of(context).selectExpiryDate,
|
||||||
),
|
),
|
||||||
@@ -143,12 +135,8 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: const OutlineInputBorder(
|
enabledBorder: const OutlineInputBorder(),
|
||||||
borderSide: BorderSide(color: Colors.black),
|
focusedBorder: const OutlineInputBorder(),
|
||||||
),
|
|
||||||
focusedBorder: const OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
@@ -178,6 +166,22 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).cardPin,
|
AppLocalizations.of(context).cardPin,
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@@ -133,6 +133,22 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
361
lib/features/cheque/screens/cheque_enquiry_screen.dart
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/cheque_service.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
import 'package:kmobile/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class ChequeEnquiryScreen extends StatefulWidget {
|
||||||
|
final List<User> users;
|
||||||
|
final int selectedIndex;
|
||||||
|
const ChequeEnquiryScreen({
|
||||||
|
super.key,
|
||||||
|
required this.users,
|
||||||
|
required this.selectedIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChequeEnquiryScreen> createState() => _ChequeEnquiryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChequeEnquiryScreenState extends State<ChequeEnquiryScreen> {
|
||||||
|
User? _selectedAccount;
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
var service = getIt<ChequeService>();
|
||||||
|
bool _isLoading = true;
|
||||||
|
List<Cheque> _allCheques = [];
|
||||||
|
Map<String, List<Cheque>> _groupedCheques = {};
|
||||||
|
List<User> _filteredUsers = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_filteredUsers = widget.users
|
||||||
|
.where((user) => ['SA', 'SB', 'CA', 'CC'].contains(user.accountType))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (widget.users.isNotEmpty && widget.selectedIndex < widget.users.length) {
|
||||||
|
if (_filteredUsers.isNotEmpty) {
|
||||||
|
if (_filteredUsers.contains(widget.users[widget.selectedIndex])) {
|
||||||
|
_selectedAccount = widget.users[widget.selectedIndex];
|
||||||
|
} else {
|
||||||
|
_selectedAccount = _filteredUsers.first;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_selectedAccount = widget.users[widget.selectedIndex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_filteredUsers.isNotEmpty) {
|
||||||
|
_selectedAccount = _filteredUsers.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadCheques();
|
||||||
|
_searchController.addListener(() {
|
||||||
|
_filterCheques(_searchController.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCheques() async {
|
||||||
|
if (_selectedAccount == null) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_groupedCheques = {};
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
String instrType;
|
||||||
|
switch (_selectedAccount!.accountType) {
|
||||||
|
case 'SA':
|
||||||
|
case 'SB':
|
||||||
|
instrType = '10';
|
||||||
|
break;
|
||||||
|
case 'CA':
|
||||||
|
instrType = '11';
|
||||||
|
break;
|
||||||
|
case 'CC':
|
||||||
|
instrType = '13';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
instrType = '10';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final data = await service.ChequeEnquiry(
|
||||||
|
accountNumber: _selectedAccount!.accountNo!, instrType: instrType);
|
||||||
|
_allCheques = data;
|
||||||
|
_groupedCheques.clear();
|
||||||
|
for (var cheque in _allCheques) {
|
||||||
|
if (cheque.type != null) {
|
||||||
|
if (!_groupedCheques.containsKey(cheque.type)) {
|
||||||
|
_groupedCheques[cheque.type!] = [];
|
||||||
|
}
|
||||||
|
_groupedCheques[cheque.type!]!.add(cheque);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_groupedCheques = {};
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to fetch cheque status: ${e.toString()}'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _filterCheques(String query) {
|
||||||
|
_groupedCheques.clear();
|
||||||
|
List<Cheque> filteredCheques;
|
||||||
|
if (query.isEmpty) {
|
||||||
|
filteredCheques = _allCheques;
|
||||||
|
} else {
|
||||||
|
filteredCheques = _allCheques.where((cheque) {
|
||||||
|
final lowerQuery = query.toLowerCase();
|
||||||
|
return (cheque.ChequeNumber?.toLowerCase().contains(lowerQuery) ??
|
||||||
|
false) ||
|
||||||
|
(cheque.status?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||||
|
(cheque.fromCheque?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||||
|
(cheque.toCheque?.toLowerCase().contains(lowerQuery) ?? false);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var cheque in filteredCheques) {
|
||||||
|
if (cheque.type != null) {
|
||||||
|
if (!_groupedCheques.containsKey(cheque.type)) {
|
||||||
|
_groupedCheques[cheque.type!] = [];
|
||||||
|
}
|
||||||
|
_groupedCheques[cheque.type!]!.add(cheque);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context).chequeEnquiryTitle),
|
||||||
|
centerTitle: false,
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).accountNumber,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
if (_selectedAccount != null)
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButton<User>(
|
||||||
|
value: _selectedAccount,
|
||||||
|
onChanged: (User? newUser) {
|
||||||
|
if (newUser != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedAccount = newUser;
|
||||||
|
_loadCheques();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: _filteredUsers.map((user) {
|
||||||
|
return DropdownMenuItem<User>(
|
||||||
|
value: user,
|
||||||
|
child: Text(user.accountNo.toString()),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Text('No accounts found'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context)
|
||||||
|
.searchByChequeDetailsHint,
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
border: InputBorder
|
||||||
|
.none, // Remove border to make it look like it's inside the card
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _groupedCheques.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(AppLocalizations.of(context)
|
||||||
|
.noChequeStatusFound))
|
||||||
|
: ListView(
|
||||||
|
children: _groupedCheques.entries.map((entry) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...entry.value.map((cheque) =>
|
||||||
|
ChequeStatusTile(cheque: cheque)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChequeStatusTile extends StatelessWidget {
|
||||||
|
final Cheque cheque;
|
||||||
|
|
||||||
|
const ChequeStatusTile({
|
||||||
|
super.key,
|
||||||
|
required this.cheque,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
switch (cheque.type) {
|
||||||
|
case 'CI':
|
||||||
|
return _buildCiTile(context);
|
||||||
|
case 'PR':
|
||||||
|
return _buildPrTile(context);
|
||||||
|
case 'ST':
|
||||||
|
return _buildStTile(context);
|
||||||
|
default:
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCiTile(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(AppLocalizations.of(context).chequebookIssuedLabel,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildInfoRow('Branch Code:', cheque.branchCode),
|
||||||
|
_buildInfoRow('From Cheque:', cheque.fromCheque),
|
||||||
|
_buildInfoRow('To Cheque:', cheque.toCheque),
|
||||||
|
_buildInfoRow('Date:', cheque.Date),
|
||||||
|
_buildInfoRow('Cheques Count:', cheque.Chequescount),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPrTile(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(AppLocalizations.of(context).presentedChequeLabel,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildInfoRow('Branch Code:', cheque.branchCode),
|
||||||
|
_buildInfoRow('Cheque Number:', cheque.ChequeNumber),
|
||||||
|
_buildInfoRow('Date:', cheque.Date),
|
||||||
|
_buildInfoRow('Transaction Code:', cheque.transactionCode),
|
||||||
|
_buildInfoRow('Amount:', '₹ ${cheque.amount.toString()}'),
|
||||||
|
_buildInfoRow('Status:', cheque.status),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStTile(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(AppLocalizations.of(context).stopChequeLabel,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildInfoRow('Branch Code:', cheque.branchCode),
|
||||||
|
_buildInfoRow('From Cheque:', cheque.fromCheque),
|
||||||
|
_buildInfoRow('To Cheque:', cheque.toCheque),
|
||||||
|
_buildInfoRow('Stop Issue Date:', cheque.stopIssueDate),
|
||||||
|
_buildInfoRow('Stop Expiry Date:', cheque.StopExpiryDate),
|
||||||
|
_buildInfoRow('Cheques Count:', cheque.Chequescount),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(String label, String? value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
Text(value ?? ''),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +1,194 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kmobile/features/enquiry/screens/enquiry_screen.dart';
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/features/cheque/screens/cheque_enquiry_screen.dart';
|
||||||
|
import 'package:kmobile/features/cheque/screens/positive_pay_screen.dart';
|
||||||
|
import 'package:kmobile/features/cheque/screens/revoke_stop_screen.dart';
|
||||||
|
import 'package:kmobile/features/cheque/screens/stop_cheque_screen.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class ChequeManagementScreen extends StatefulWidget {
|
class ChequeManagementScreen extends StatefulWidget {
|
||||||
const ChequeManagementScreen({super.key});
|
final List<User> users;
|
||||||
|
final int selectedIndex;
|
||||||
|
const ChequeManagementScreen({
|
||||||
|
super.key,
|
||||||
|
required this.users,
|
||||||
|
required this.selectedIndex,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChequeManagementScreen> createState() => _ChequeManagementScreen();
|
State<ChequeManagementScreen> createState() => _ChequeManagementScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChequeManagementScreen extends State<ChequeManagementScreen> {
|
class _ChequeManagementScreen extends State<ChequeManagementScreen> {
|
||||||
|
List<User> get users => widget.users;
|
||||||
|
int get selectedAccountIndex => widget.selectedIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).chequeManagement,
|
AppLocalizations.of(context).chequeManagement,
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 15),
|
Padding(
|
||||||
ChequeManagementTile(
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
icon: Symbols.add,
|
child: Column(
|
||||||
label: AppLocalizations.of(context).requestChequeBook,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
onTap: () {},
|
children: [
|
||||||
),
|
Expanded(
|
||||||
const Divider(height: 1),
|
child: ChequeManagementCardTile(
|
||||||
ChequeManagementTile(
|
icon: Symbols.payments,
|
||||||
icon: Symbols.data_alert,
|
label: AppLocalizations.of(context).chequeEnquiryTitle,
|
||||||
label: AppLocalizations.of(context).enquiry,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const EnquiryScreen()),
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ChequeEnquiryScreen(
|
||||||
|
users: users,
|
||||||
|
selectedIndex: selectedAccountIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
ChequeManagementTile(
|
|
||||||
icon: Symbols.approval_delegation,
|
|
||||||
label: AppLocalizations.of(context).chequeDeposit,
|
|
||||||
onTap: () {},
|
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Expanded(
|
||||||
ChequeManagementTile(
|
child: ChequeManagementCardTile(
|
||||||
icon: Symbols.front_hand,
|
icon: Symbols.block_sharp,
|
||||||
label: AppLocalizations.of(context).stopCheque,
|
label: AppLocalizations.of(context).stopCheque,
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => StopChequeScreen(
|
||||||
|
users: users,
|
||||||
|
selectedIndex: selectedAccountIndex,
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
),
|
||||||
ChequeManagementTile(
|
);
|
||||||
icon: Symbols.cancel_presentation,
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ChequeManagementCardTile(
|
||||||
|
icon: Symbols.stop_circle,
|
||||||
label: AppLocalizations.of(context).revokeStop,
|
label: AppLocalizations.of(context).revokeStop,
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => RevokeStopChequeScreen(
|
||||||
|
users: users,
|
||||||
|
selectedIndex: selectedAccountIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ChequeManagementCardTile(
|
||||||
|
icon: Symbols.check_circle, // Using check_circle for Positive Pay
|
||||||
|
label: AppLocalizations.of(context).positivePayTitle,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PositivePayScreen(
|
||||||
|
users: users,
|
||||||
|
selectedIndex: selectedAccountIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
ChequeManagementTile(
|
|
||||||
icon: Symbols.payments,
|
|
||||||
label: AppLocalizations.of(context).positivePay,
|
|
||||||
onTap: () {},
|
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChequeManagementTile extends StatelessWidget {
|
class ChequeManagementCardTile extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final bool disable;
|
||||||
|
|
||||||
const ChequeManagementTile({
|
const ChequeManagementCardTile({
|
||||||
super.key,
|
super.key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.disable = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
final theme = Theme.of(context);
|
||||||
leading: Icon(icon),
|
return Card(
|
||||||
title: Text(label),
|
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
trailing: const Icon(Symbols.arrow_right, size: 20),
|
shape: RoundedRectangleBorder(
|
||||||
onTap: onTap,
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
),
|
||||||
|
elevation: 4, // Add some elevation for better visual separation
|
||||||
|
child: InkWell(
|
||||||
|
onTap:
|
||||||
|
disable ? null : onTap, // Disable InkWell if the tile is disabled
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: 48, // Make icon larger
|
||||||
|
color: disable
|
||||||
|
? theme.disabledColor
|
||||||
|
: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: disable
|
||||||
|
? theme.disabledColor
|
||||||
|
: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
334
lib/features/cheque/screens/positive_pay_screen.dart
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/cheque_service.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
|
||||||
|
import 'package:kmobile/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class PositivePayScreen extends StatefulWidget {
|
||||||
|
final List<User> users;
|
||||||
|
final int selectedIndex;
|
||||||
|
const PositivePayScreen({
|
||||||
|
super.key,
|
||||||
|
required this.users,
|
||||||
|
required this.selectedIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PositivePayScreen> createState() => _PositivePayScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PositivePayScreenState extends State<PositivePayScreen> {
|
||||||
|
User? _selectedAccount;
|
||||||
|
List<User> _filteredUsers = [];
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _chequeNumberController = TextEditingController();
|
||||||
|
final _chequeDateController = TextEditingController();
|
||||||
|
final _amountController = TextEditingController();
|
||||||
|
final _payeeController = TextEditingController();
|
||||||
|
final _chequeService = getIt<ChequeService>();
|
||||||
|
String? _ciFromCheque;
|
||||||
|
String? _ciToCheque;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_filteredUsers = widget.users
|
||||||
|
.where((user) => ['SA', 'SB', 'CA', 'CC'].contains(user.accountType))
|
||||||
|
.toList();
|
||||||
|
// Pre-fill the account number if possible
|
||||||
|
if (widget.users.isNotEmpty && widget.selectedIndex < widget.users.length) {
|
||||||
|
if (_filteredUsers.isNotEmpty) {
|
||||||
|
if (_filteredUsers.contains(widget.users[widget.selectedIndex])) {
|
||||||
|
_selectedAccount = widget.users[widget.selectedIndex];
|
||||||
|
} else {
|
||||||
|
_selectedAccount = _filteredUsers.first;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_selectedAccount = widget.users[widget.selectedIndex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_filteredUsers.isNotEmpty) {
|
||||||
|
_selectedAccount = _filteredUsers.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_loadChequeDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadChequeDetails() async {
|
||||||
|
if (_selectedAccount == null) return;
|
||||||
|
|
||||||
|
String instrType;
|
||||||
|
switch (_selectedAccount!.accountType) {
|
||||||
|
case 'SA':
|
||||||
|
case 'SB':
|
||||||
|
instrType = '10';
|
||||||
|
break;
|
||||||
|
case 'CA':
|
||||||
|
instrType = '11';
|
||||||
|
break;
|
||||||
|
case 'CC':
|
||||||
|
instrType = '13';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
instrType = '10';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final data = await _chequeService.ChequeEnquiry(
|
||||||
|
accountNumber: _selectedAccount!.accountNo!,
|
||||||
|
instrType: instrType,
|
||||||
|
);
|
||||||
|
final ciCheque = data.where((cheque) => cheque.type == 'CI').toList();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
if (ciCheque.isNotEmpty) {
|
||||||
|
_ciFromCheque = ciCheque.first.fromCheque;
|
||||||
|
_ciToCheque = ciCheque.first.toCheque;
|
||||||
|
} else {
|
||||||
|
_ciFromCheque = null;
|
||||||
|
_ciToCheque = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_ciFromCheque = null;
|
||||||
|
_ciToCheque = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_chequeNumberController.dispose();
|
||||||
|
_chequeDateController.dispose();
|
||||||
|
_amountController.dispose();
|
||||||
|
_payeeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showResponseDialog(String title, String message) async {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false, // user must tap button!
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ListBody(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(message),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: Text(AppLocalizations.of(context).closeButton),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
AppLocalizations.of(context).positivePay, // Will be replaced by localization
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
DropdownButtonFormField<User>(
|
||||||
|
value: _selectedAccount,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).accountNumber,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: _filteredUsers.map((user) {
|
||||||
|
return DropdownMenuItem<User>(
|
||||||
|
value: user,
|
||||||
|
child: Text(user.accountNo.toString()),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (User? newUser) {
|
||||||
|
setState(() {
|
||||||
|
_selectedAccount = newUser;
|
||||||
|
_loadChequeDetails();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null) {
|
||||||
|
return AppLocalizations.of(context).accountNumberRequired;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
TextFormField(
|
||||||
|
controller: _chequeNumberController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).chequeNumberLabel,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return AppLocalizations.of(context).pleaseEnterChequeNumber;
|
||||||
|
}
|
||||||
|
final chequeNumber = int.tryParse(value);
|
||||||
|
final fromCheque = int.tryParse(_ciFromCheque ?? '');
|
||||||
|
final toCheque = int.tryParse(_ciToCheque ?? '');
|
||||||
|
if (chequeNumber == null) {
|
||||||
|
return AppLocalizations.of(context).invalidChequeNumberFormatError;
|
||||||
|
}
|
||||||
|
if (fromCheque != null && toCheque != null) {
|
||||||
|
if (chequeNumber < fromCheque || chequeNumber > toCheque) {
|
||||||
|
return AppLocalizations.of(context).chequeNumberRangeError(
|
||||||
|
_ciFromCheque!, _ciToCheque!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
TextFormField(
|
||||||
|
controller: _chequeDateController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).chequeIssuedDate,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: const Icon(Icons.calendar_today),
|
||||||
|
),
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () async {
|
||||||
|
DateTime? pickedDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (pickedDate != null) {
|
||||||
|
// Format the date as you wish
|
||||||
|
String formattedDate = "${pickedDate.year}-${pickedDate.month.toString().padLeft(2, '0')}-${pickedDate.day.toString().padLeft(2, '0')}";
|
||||||
|
_chequeDateController.text = formattedDate;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return AppLocalizations.of(context).pleaseSelectChequeIssuedDate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
TextFormField(
|
||||||
|
controller: _payeeController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).payeeName,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
TextFormField(
|
||||||
|
controller: _amountController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).amountLabel,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixText: '₹ ',
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return AppLocalizations.of(context).pleaseEnterAmount;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32.0),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
// Process data
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => TransactionPinScreen(
|
||||||
|
onPinCompleted: (ctx, pin) async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
try {
|
||||||
|
final response = await _chequeService.registerPPS(
|
||||||
|
cheque_no: _chequeNumberController.text,
|
||||||
|
account_number:
|
||||||
|
_selectedAccount!.accountNo!,
|
||||||
|
issue_date:
|
||||||
|
_chequeDateController.text,
|
||||||
|
amount: _amountController.text,
|
||||||
|
payee_name: _payeeController.text,
|
||||||
|
tpin: pin,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
String responseString = response.toString();
|
||||||
|
if(responseString == 'PPS Registered Successfully'){
|
||||||
|
_showResponseDialog('REGISTRATION SUCCESFUL',
|
||||||
|
'Your Positive Pay Request has been registered succesfully');
|
||||||
|
}
|
||||||
|
if(responseString.contains('Cheque already registered')){
|
||||||
|
_showResponseDialog('ERROR',
|
||||||
|
'Your Request for the selected cheque number has already been registered');
|
||||||
|
}
|
||||||
|
if(responseString.contains('INCORRECT_TPIN')){
|
||||||
|
_showResponseDialog('Invalid TPIN',
|
||||||
|
'The TPIN you entered is incorrect. Please try again.');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
try {
|
||||||
|
final errorBodyString =
|
||||||
|
e.toString().split('Exception: ')[1];
|
||||||
|
final errorBody = jsonDecode(errorBodyString);
|
||||||
|
if (errorBody.containsKey('error') &&
|
||||||
|
errorBody['error'] == 'INCORRECT_TPIN') {
|
||||||
|
_showResponseDialog('Invalid TPIN',
|
||||||
|
'The TPIN you entered is incorrect. Please try again.');
|
||||||
|
} else {
|
||||||
|
_showResponseDialog(
|
||||||
|
'Error', 'Internal Server Error');
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
_showResponseDialog(
|
||||||
|
'Error', 'Internal Server Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(AppLocalizations.of(context).proceedButton),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
338
lib/features/cheque/screens/revoke _stop_multiple_screen.dart
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/cheque_service.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
|
||||||
|
import 'package:kmobile/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class RevokeStopMultipleChequesScreen extends StatefulWidget {
|
||||||
|
final User selectedAccount;
|
||||||
|
final String date;
|
||||||
|
final String instrType;
|
||||||
|
final String fromCheque;
|
||||||
|
final String toCheque;
|
||||||
|
|
||||||
|
const RevokeStopMultipleChequesScreen(
|
||||||
|
{super.key,
|
||||||
|
required this.selectedAccount,
|
||||||
|
required this.date,
|
||||||
|
required this.instrType,
|
||||||
|
required this.fromCheque,
|
||||||
|
required this.toCheque});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RevokeStopMultipleChequesScreen> createState() =>
|
||||||
|
_RevokeStopMultipleChequesScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RevokeStopMultipleChequesScreenState extends State<RevokeStopMultipleChequesScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _stopFromChequeNoController = TextEditingController();
|
||||||
|
final _stopToChequeNoController = TextEditingController();
|
||||||
|
final _stopIssueDateController = TextEditingController();
|
||||||
|
final _stopExpiryDateController = TextEditingController();
|
||||||
|
final _stopAmountController = TextEditingController();
|
||||||
|
final _chequeService = getIt<ChequeService>();
|
||||||
|
|
||||||
|
String? _selectedComment;
|
||||||
|
final _otherCommentController = TextEditingController();
|
||||||
|
bool _showOtherCommentField = false;
|
||||||
|
final List<String> _commentOptions = [
|
||||||
|
'Cheque Found',
|
||||||
|
'Cheque Fixed',
|
||||||
|
'Other'
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<void> _selectDate(TextEditingController controller) async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime.now(),
|
||||||
|
lastDate: DateTime(2101),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
controller.text =
|
||||||
|
'${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showResponseDialog(String title, String message) async {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false, // user must tap button!
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ListBody(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(message),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Close'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context).revokeMultipleStops),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
title: Text(widget.selectedAccount.accountNo!),
|
||||||
|
subtitle:
|
||||||
|
Text(AppLocalizations.of(context).accountNumberTitle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stopFromChequeNoController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).fromChequeNumberHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return AppLocalizations.of(context)
|
||||||
|
.pleaseEnterChequeNumberError;
|
||||||
|
}
|
||||||
|
final chequeNumber = int.tryParse(value);
|
||||||
|
final fromCheque = int.tryParse(widget.fromCheque);
|
||||||
|
final toCheque = int.tryParse(widget.toCheque);
|
||||||
|
if (chequeNumber == null ||
|
||||||
|
fromCheque == null ||
|
||||||
|
toCheque == null) {
|
||||||
|
return AppLocalizations.of(context)
|
||||||
|
.invalidChequeNumberFormatError;
|
||||||
|
}
|
||||||
|
if (chequeNumber < fromCheque || chequeNumber > toCheque) {
|
||||||
|
return AppLocalizations.of(context).chequeNumberRangeError(
|
||||||
|
widget.fromCheque, widget.toCheque);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stopToChequeNoController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).toChequeNumberHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return AppLocalizations.of(context)
|
||||||
|
.pleaseEnterChequeNumberError;
|
||||||
|
}
|
||||||
|
final chequeNumber = int.tryParse(value);
|
||||||
|
final fromCheque = int.tryParse(widget.fromCheque);
|
||||||
|
final toCheque = int.tryParse(widget.toCheque);
|
||||||
|
if (chequeNumber == null ||
|
||||||
|
fromCheque == null ||
|
||||||
|
toCheque == null) {
|
||||||
|
return AppLocalizations.of(context)
|
||||||
|
.invalidChequeNumberFormatError;
|
||||||
|
}
|
||||||
|
if (chequeNumber < fromCheque || chequeNumber > toCheque) {
|
||||||
|
return AppLocalizations.of(context).chequeNumberRangeError(
|
||||||
|
widget.fromCheque, widget.toCheque);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: widget.instrType,
|
||||||
|
readOnly: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).instrumentTypeLabel,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stopIssueDateController,
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => _selectDate(_stopIssueDateController),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).revokeIssueDate,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.calendar_today),
|
||||||
|
onPressed: () => _selectDate(_stopIssueDateController),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.datetime,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stopExpiryDateController,
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => _selectDate(_stopExpiryDateController),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).revokeExpiryDate,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.calendar_today),
|
||||||
|
onPressed: () => _selectDate(_stopExpiryDateController),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.datetime,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stopAmountController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).revokeAmount,
|
||||||
|
prefixText: '₹ ',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedComment,
|
||||||
|
items: _commentOptions.map((String value) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(value),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedComment = newValue;
|
||||||
|
_showOtherCommentField = newValue == 'Other';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).revokeComment,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_showOtherCommentField)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _otherCommentController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Other Reasons :",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => TransactionPinScreen(
|
||||||
|
onPinCompleted: (ctx, pin) async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
try {
|
||||||
|
final response = await _chequeService.revokeStop(
|
||||||
|
accountno: widget.selectedAccount.accountNo!,
|
||||||
|
removeFromChequeNo:
|
||||||
|
_stopFromChequeNoController.text,
|
||||||
|
instrType: widget.instrType,
|
||||||
|
removeToChequeNo:
|
||||||
|
_stopToChequeNoController.text,
|
||||||
|
removeIssueDate: _stopIssueDateController.text,
|
||||||
|
removeExpiryDate: _stopExpiryDateController.text,
|
||||||
|
removeAmount: _stopAmountController.text,
|
||||||
|
removeComment: _selectedComment == 'Other'
|
||||||
|
? _otherCommentController.text
|
||||||
|
: _selectedComment ?? '',
|
||||||
|
tpin: pin,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
final decodedResponse = jsonDecode(response);
|
||||||
|
String responseString = response.toString(); // used as the case only for incorrect TPIN
|
||||||
|
final status = decodedResponse['status'];
|
||||||
|
final message = decodedResponse['message'];
|
||||||
|
final code = decodedResponse['code'];
|
||||||
|
if (status == 'SUCCESS') {
|
||||||
|
_showResponseDialog('Success', message);
|
||||||
|
} if (status == 'ERROR') {
|
||||||
|
String errMessage = "error";
|
||||||
|
if(code == '0172') {
|
||||||
|
errMessage = 'The selected Cheque is not stopped';
|
||||||
|
} else if(code == '0748') {
|
||||||
|
errMessage = 'The selected Cheque is already presented';
|
||||||
|
}
|
||||||
|
_showResponseDialog('Error', errMessage);
|
||||||
|
}
|
||||||
|
if(responseString.contains('INCORRECT_TPIN')){
|
||||||
|
_showResponseDialog('Invalid TPIN',
|
||||||
|
'The TPIN you entered is incorrect. Please try again.');
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
try {
|
||||||
|
final errorBodyString =
|
||||||
|
e.toString().split('Exception: ')[1];
|
||||||
|
final errorBody = jsonDecode(errorBodyString);
|
||||||
|
if (errorBody.containsKey('error') &&
|
||||||
|
errorBody['error'] == 'INCORRECT_TPIN') {
|
||||||
|
_showResponseDialog('Invalid TPIN',
|
||||||
|
'The TPIN you entered is incorrect. Please try again.');
|
||||||
|
} else {
|
||||||
|
_showResponseDialog(
|
||||||
|
'Error', 'Internal Server Error');
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
_showResponseDialog(
|
||||||
|
'Error', 'Internal Server Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(AppLocalizations.of(context).revokeStopButton),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
361
lib/features/cheque/screens/revoke_stop_screen.dart
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/cheque_service.dart';
|
||||||
|
import 'package:kmobile/features/cheque/screens/revoke%20_stop_multiple_screen.dart';
|
||||||
|
import 'package:kmobile/features/cheque/screens/revoke_stop_single_screen.dart';
|
||||||
|
import 'package:kmobile/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class RevokeStopChequeScreen extends StatefulWidget {
|
||||||
|
final List<User> users;
|
||||||
|
final int selectedIndex;
|
||||||
|
|
||||||
|
const RevokeStopChequeScreen(
|
||||||
|
{
|
||||||
|
super.key,
|
||||||
|
required this.users,
|
||||||
|
required this.selectedIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RevokeStopChequeScreen> createState() => _RevokeStopChequeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RevokeStopChequeScreenState extends State<RevokeStopChequeScreen> {
|
||||||
|
User? _selectedAccount;
|
||||||
|
var service = getIt<ChequeService>();
|
||||||
|
bool _isLoading = true;
|
||||||
|
List<Cheque> _stCheques = [];
|
||||||
|
List<User> _filteredUsers = [];
|
||||||
|
String? _ciFromCheque;
|
||||||
|
String? _ciToCheque;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_filteredUsers = widget.users
|
||||||
|
.where((user) => ['SA', 'SB', 'CA', 'CC'].contains(user.accountType))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (widget.users.isNotEmpty && widget.selectedIndex < widget.users.length) {
|
||||||
|
if (_filteredUsers.isNotEmpty) {
|
||||||
|
if (_filteredUsers.contains(widget.users[widget.selectedIndex])) {
|
||||||
|
_selectedAccount = widget.users[widget.selectedIndex];
|
||||||
|
} else {
|
||||||
|
_selectedAccount = _filteredUsers.first;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_selectedAccount = widget.users[widget.selectedIndex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_filteredUsers.isNotEmpty) {
|
||||||
|
_selectedAccount = _filteredUsers.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadCheques();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCheques() async {
|
||||||
|
if (_selectedAccount == null) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_stCheques = [];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
String instrType;
|
||||||
|
switch (_selectedAccount!.accountType) {
|
||||||
|
case 'SA':
|
||||||
|
case 'SB':
|
||||||
|
instrType = '10';
|
||||||
|
break;
|
||||||
|
case 'CA':
|
||||||
|
instrType = '11';
|
||||||
|
break;
|
||||||
|
case 'CC':
|
||||||
|
instrType = '13';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
instrType = '10';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final data = await service.ChequeEnquiry(
|
||||||
|
accountNumber: _selectedAccount!.accountNo!, instrType: instrType);
|
||||||
|
final stCheques = data.where((cheque) => cheque.type == 'ST').toList();
|
||||||
|
final ciCheque = data.where((cheque) => cheque.type == 'CI').toList();
|
||||||
|
setState(() {
|
||||||
|
_stCheques = stCheques;
|
||||||
|
if (ciCheque.isNotEmpty) {
|
||||||
|
_ciFromCheque = ciCheque.first.fromCheque;
|
||||||
|
_ciToCheque = ciCheque.first.toCheque;
|
||||||
|
} else {
|
||||||
|
_ciFromCheque = null;
|
||||||
|
_ciToCheque = null;
|
||||||
|
}
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_stCheques = [];
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to fetch cheque status: ${e.toString()}'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getAccountTypeDisplayName(String accountType) {
|
||||||
|
switch (accountType.toLowerCase()) {
|
||||||
|
case 'sa':
|
||||||
|
return AppLocalizations.of(context).savingsAccount;
|
||||||
|
case 'sb':
|
||||||
|
return AppLocalizations.of(context).savingsAccount;
|
||||||
|
case 'ca':
|
||||||
|
return "Current Account";
|
||||||
|
case 'cc':
|
||||||
|
return "Cash Credit Account";
|
||||||
|
default:
|
||||||
|
return accountType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context).revokeStop),
|
||||||
|
centerTitle: false,
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(children: [
|
||||||
|
Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).accountNumber,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
if (_selectedAccount != null)
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButton<User>(
|
||||||
|
value: _selectedAccount,
|
||||||
|
onChanged: (User? newUser) {
|
||||||
|
if (newUser != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedAccount = newUser;
|
||||||
|
_loadCheques();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: _filteredUsers.map((user) {
|
||||||
|
return DropdownMenuItem<User>(
|
||||||
|
value: user,
|
||||||
|
child: Text(user.accountNo.toString()),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(AppLocalizations.of(context).noAccountsFound),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
elevation: 4,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (_selectedAccount != null &&
|
||||||
|
_stCheques.isNotEmpty) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
RevokeStopSingleChequeScreen(
|
||||||
|
selectedAccount: _selectedAccount!,
|
||||||
|
date: _stCheques.first.Date!,
|
||||||
|
instrType: _stCheques.first.InstrType!,
|
||||||
|
fromCheque: _ciFromCheque ?? _stCheques.first.fromCheque!,
|
||||||
|
toCheque: _ciToCheque ?? _stCheques.first.toCheque!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("No stopped cheques present"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
AppLocalizations.of(context).revokeSingleStopTitle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
elevation: 4,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (_selectedAccount != null &&
|
||||||
|
_stCheques.isNotEmpty) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
RevokeStopMultipleChequesScreen(
|
||||||
|
selectedAccount: _selectedAccount!,
|
||||||
|
date: _stCheques.first.Date!,
|
||||||
|
instrType: _stCheques.first.InstrType!,
|
||||||
|
fromCheque: _ciFromCheque ?? _stCheques.first.fromCheque!,
|
||||||
|
toCheque: _ciToCheque ?? _stCheques.first.toCheque!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context)
|
||||||
|
.pleaseSelectAccountFirst),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
AppLocalizations.of(context).revokeMultipleStops,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _stCheques.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(AppLocalizations.of(context)
|
||||||
|
.noChequeIssuedStatus))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: _stCheques.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildSTTile(context, _stCheques[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSTTile(BuildContext context, Cheque cheque) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(AppLocalizations.of(context).stopChequeLabel,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildInfoRow('From Cheque:', cheque.fromCheque),
|
||||||
|
_buildInfoRow('To Cheque:', cheque.toCheque),
|
||||||
|
_buildInfoRow('Account Type:',
|
||||||
|
_getAccountTypeDisplayName(_selectedAccount!.accountType!)),
|
||||||
|
_buildInfoRow('Branch Code:', cheque.branchCode),
|
||||||
|
_buildInfoRow('Stop Issue Date:', cheque.stopIssueDate),
|
||||||
|
_buildInfoRow('Stop Expiry Date:', cheque.StopExpiryDate),
|
||||||
|
_buildInfoRow('Cheques Count:', cheque.Chequescount),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(String label, String? value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
Text(value ?? ''),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
304
lib/features/cheque/screens/revoke_stop_single_screen.dart
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/cheque_service.dart';
|
||||||
|
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
|
||||||
|
import 'package:kmobile/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class RevokeStopSingleChequeScreen extends StatefulWidget {
|
||||||
|
final User selectedAccount;
|
||||||
|
final String date;
|
||||||
|
final String instrType;
|
||||||
|
final String fromCheque;
|
||||||
|
final String toCheque;
|
||||||
|
|
||||||
|
const RevokeStopSingleChequeScreen(
|
||||||
|
{super.key,
|
||||||
|
required this.selectedAccount,
|
||||||
|
required this.date,
|
||||||
|
required this.instrType,
|
||||||
|
required this.fromCheque,
|
||||||
|
required this.toCheque});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RevokeStopSingleChequeScreen> createState() => _RevokeStopSingleChequeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RevokeStopSingleChequeScreenState extends State<RevokeStopSingleChequeScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _stopFromChequeNoController = TextEditingController();
|
||||||
|
final _stopIssueDateController = TextEditingController();
|
||||||
|
final _stopExpiryDateController = TextEditingController();
|
||||||
|
final _stopAmountController = TextEditingController();
|
||||||
|
final _chequeService = getIt<ChequeService>();
|
||||||
|
|
||||||
|
String? _selectedComment;
|
||||||
|
final _otherCommentController = TextEditingController();
|
||||||
|
bool _showOtherCommentField = false;
|
||||||
|
final List<String> _commentOptions = [
|
||||||
|
'Cheque Found',
|
||||||
|
'Cheque Fixed',
|
||||||
|
'Other'
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<void> _selectDate(TextEditingController controller) async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime.now(),
|
||||||
|
lastDate: DateTime(2101),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
controller.text =
|
||||||
|
'${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showResponseDialog(String title, String message) async {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false, // user must tap button!
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ListBody(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(message),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: Text(AppLocalizations.of(context).closeButton),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context).revokeSingleStopTitle)),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
title: Text(widget.selectedAccount.accountNo!),
|
||||||
|
subtitle:
|
||||||
|
Text(AppLocalizations.of(context).accountNumberLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stopFromChequeNoController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).chequeNumberLabel,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return AppLocalizations.of(context)
|
||||||
|
.pleaseEnterChequeNumberError;
|
||||||
|
}
|
||||||
|
final chequeNumber = int.tryParse(value);
|
||||||
|
final fromCheque = int.tryParse(widget.fromCheque);
|
||||||
|
final toCheque = int.tryParse(widget.toCheque);
|
||||||
|
if (chequeNumber == null ||
|
||||||
|
fromCheque == null ||
|
||||||
|
toCheque == null) {
|
||||||
|
return AppLocalizations.of(context)
|
||||||
|
.invalidChequeNumberFormatError;
|
||||||
|
}
|
||||||
|
if (chequeNumber < fromCheque || chequeNumber > toCheque) {
|
||||||
|
return AppLocalizations.of(context).chequeNumberRangeError(
|
||||||
|
widget.fromCheque, widget.toCheque);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: widget.instrType,
|
||||||
|
readOnly: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).instrumentTypeLabel,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stopIssueDateController,
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => _selectDate(_stopIssueDateController),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).revokeIssueDate,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.calendar_today),
|
||||||
|
onPressed: () => _selectDate(_stopIssueDateController),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.datetime,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stopExpiryDateController,
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => _selectDate(_stopExpiryDateController),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).revokeExpiryDate,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.calendar_today),
|
||||||
|
onPressed: () => _selectDate(_stopExpiryDateController),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.datetime,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _stopAmountController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).revokeAmount,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedComment,
|
||||||
|
items: _commentOptions.map((String value) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(value),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedComment = newValue;
|
||||||
|
_showOtherCommentField = newValue == 'Other';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).revokeComment,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_showOtherCommentField)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _otherCommentController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Other Reasons :",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => TransactionPinScreen(
|
||||||
|
onPinCompleted: (ctx, pin) async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
try {
|
||||||
|
final response = await _chequeService.revokeStop(
|
||||||
|
accountno: widget.selectedAccount.accountNo!,
|
||||||
|
removeFromChequeNo:
|
||||||
|
_stopFromChequeNoController.text,
|
||||||
|
instrType: widget.instrType,
|
||||||
|
removeToChequeNo:
|
||||||
|
_stopFromChequeNoController.text,
|
||||||
|
removeIssueDate: _stopIssueDateController.text,
|
||||||
|
removeExpiryDate: _stopExpiryDateController.text,
|
||||||
|
removeAmount: _stopAmountController.text,
|
||||||
|
removeComment: _selectedComment == 'Other'
|
||||||
|
? _otherCommentController.text
|
||||||
|
: _selectedComment ?? '',
|
||||||
|
tpin: pin,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
final decodedResponse = jsonDecode(response);
|
||||||
|
String responseString = response.toString(); // used as the case only for incorrect TPIN
|
||||||
|
final status = decodedResponse['status'];
|
||||||
|
final message = decodedResponse['message'];
|
||||||
|
final code = decodedResponse['code'];
|
||||||
|
if (status == 'SUCCESS') {
|
||||||
|
_showResponseDialog('Success', message);
|
||||||
|
} if (status == 'ERROR') {
|
||||||
|
String errMessage = "error";
|
||||||
|
if(code == '0172') {
|
||||||
|
errMessage = 'The selected Cheque is not stopped';
|
||||||
|
} else if(code == '0748') {
|
||||||
|
errMessage = 'The selected Cheque is already presented';
|
||||||
|
}
|
||||||
|
_showResponseDialog('Error', errMessage);
|
||||||
|
}
|
||||||
|
if(responseString.contains('INCORRECT_TPIN')){
|
||||||
|
_showResponseDialog('Invalid TPIN',
|
||||||
|
'The TPIN you entered is incorrect. Please try again.');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
try {
|
||||||
|
final errorBodyString =
|
||||||
|
e.toString().split('Exception: ')[1];
|
||||||
|
final errorBody = jsonDecode(errorBodyString);
|
||||||
|
if (errorBody.containsKey('error') &&
|
||||||
|
errorBody['error'] == 'INCORRECT_TPIN') {
|
||||||
|
_showResponseDialog('Invalid TPIN',
|
||||||
|
'The TPIN you entered is incorrect. Please try again.');
|
||||||
|
} else {
|
||||||
|
_showResponseDialog(
|
||||||
|
'Error', 'Internal Server Error');
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
_showResponseDialog(
|
||||||
|
'Error', 'Internal Server Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(AppLocalizations.of(context).revokeStopButton),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||