implement create tender screen
This commit is contained in:
parent
12fbef19c5
commit
7e427237c3
@ -1,15 +1,20 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:razorpay_flutter/razorpay_flutter.dart';
|
import 'package:razorpay_flutter/razorpay_flutter.dart';
|
||||||
import 'package:marco/helpers/services/payment_service.dart';
|
import 'package:marco/helpers/services/payment_service.dart';
|
||||||
|
import 'package:marco/helpers/services/tenant_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
class PaymentController with ChangeNotifier {
|
class PaymentController with ChangeNotifier {
|
||||||
Razorpay? _razorpay;
|
Razorpay? _razorpay;
|
||||||
final PaymentService _paymentService = PaymentService();
|
final PaymentService _paymentService = PaymentService();
|
||||||
|
final TenantService _tenantService = TenantService();
|
||||||
|
|
||||||
bool isProcessing = false;
|
bool isProcessing = false;
|
||||||
//BuildContext? _context;
|
|
||||||
|
// Pending values to use after payment verification
|
||||||
|
String? _pendingTenantEnquireId;
|
||||||
|
String? _pendingPlanId;
|
||||||
|
|
||||||
/// ==============================
|
/// ==============================
|
||||||
/// START PAYMENT (Safe init)
|
/// START PAYMENT (Safe init)
|
||||||
@ -18,11 +23,17 @@ class PaymentController with ChangeNotifier {
|
|||||||
required double amount,
|
required double amount,
|
||||||
required String description,
|
required String description,
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
String? tenantEnquireId,
|
||||||
|
String? planId,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
isProcessing = true;
|
isProcessing = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
// Save pending ids for post-payment subscription call
|
||||||
|
_pendingTenantEnquireId = tenantEnquireId;
|
||||||
|
_pendingPlanId = planId;
|
||||||
|
|
||||||
// Step 1: Create payment order
|
// Step 1: Create payment order
|
||||||
final result = await _paymentService.createOrder(amount);
|
final result = await _paymentService.createOrder(amount);
|
||||||
logSafe("🧩 Raw response in PaymentController: $result");
|
logSafe("🧩 Raw response in PaymentController: $result");
|
||||||
@ -35,7 +46,7 @@ class PaymentController with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Handle both wrapped and unwrapped formats
|
// Step 3: Handle both wrapped and unwrapped formats
|
||||||
final data = result['data'] ?? result;
|
final data = result['data'] ?? result;
|
||||||
final orderId = data?['orderId'];
|
final orderId = data?['orderId'];
|
||||||
final key = data?['key'];
|
final key = data?['key'];
|
||||||
@ -48,18 +59,18 @@ class PaymentController with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Initialize Razorpay if needed
|
// Step 4: Initialize Razorpay if needed
|
||||||
_razorpay ??= Razorpay();
|
_razorpay ??= Razorpay();
|
||||||
_razorpay!.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
|
_razorpay!.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
|
||||||
_razorpay!.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
|
_razorpay!.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
|
||||||
_razorpay!.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
|
_razorpay!.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
|
||||||
|
|
||||||
// Step 4: Open Razorpay checkout
|
// Step 5: Open Razorpay checkout
|
||||||
final options = {
|
final options = {
|
||||||
'key': key,
|
'key': key,
|
||||||
'amount': (amount * 100).toInt(), // Razorpay expects amount in paise
|
'amount': (amount * 100).toInt(), // Razorpay expects amount in paise
|
||||||
'name': 'Marco',
|
'name': 'Marco',
|
||||||
'description': 'Subscription Payment',
|
'description': description,
|
||||||
'order_id': orderId,
|
'order_id': orderId,
|
||||||
'prefill': {'contact': '9999999999', 'email': 'test@marco.com'},
|
'prefill': {'contact': '9999999999', 'email': 'test@marco.com'},
|
||||||
};
|
};
|
||||||
@ -94,6 +105,7 @@ class PaymentController with ChangeNotifier {
|
|||||||
signature: response.signature!,
|
signature: response.signature!,
|
||||||
)
|
)
|
||||||
.timeout(const Duration(seconds: 15));
|
.timeout(const Duration(seconds: 15));
|
||||||
|
logSafe("🧩 Verification result: $verificationResult");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error);
|
logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
@ -101,25 +113,27 @@ class PaymentController with ChangeNotifier {
|
|||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// ✅ Handle backend verification response properly
|
// Handle backend verification response properly
|
||||||
if (verificationResult != null) {
|
if (verificationResult != null) {
|
||||||
// Example backend response: { "verified": true, "message": "Payment verified" }
|
|
||||||
final isVerified = verificationResult['verified'] == true;
|
final isVerified = verificationResult['verified'] == true;
|
||||||
final msg = verificationResult['message'] ?? '';
|
final msg = verificationResult['message'] ?? '';
|
||||||
|
|
||||||
if (isVerified) {
|
if (isVerified) {
|
||||||
|
// If we have pending tenant and plan IDs, call subscription API
|
||||||
|
await _maybeSubscribeTenantAfterPayment(
|
||||||
|
verificationResult: verificationResult,
|
||||||
|
razorpayResponse: response,
|
||||||
|
);
|
||||||
|
|
||||||
_showDialog(
|
_showDialog(
|
||||||
title: "Payment Successful 🎉",
|
title: "Payment Successful 🎉",
|
||||||
message:
|
message: msg.isNotEmpty ? msg : "Your payment was verified successfully.",
|
||||||
msg.isNotEmpty ? msg : "Your payment was verified successfully.",
|
|
||||||
success: true,
|
success: true,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_showDialog(
|
_showDialog(
|
||||||
title: "Verification Failed ❌",
|
title: "Verification Failed ❌",
|
||||||
message: msg.isNotEmpty
|
message: msg.isNotEmpty ? msg : "Payment completed but verification failed.",
|
||||||
? msg
|
|
||||||
: "Payment completed but verification failed.",
|
|
||||||
success: false,
|
success: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -134,6 +148,61 @@ class PaymentController with ChangeNotifier {
|
|||||||
_cleanup();
|
_cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _maybeSubscribeTenantAfterPayment({
|
||||||
|
required Map<String, dynamic> verificationResult,
|
||||||
|
required PaymentSuccessResponse razorpayResponse,
|
||||||
|
}) async {
|
||||||
|
if (_pendingTenantEnquireId == null || _pendingPlanId == null) {
|
||||||
|
logSafe("ℹ️ No pending tenant/plan id to subscribe.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine paymentDetailId — prefer backend value if provided
|
||||||
|
final paymentDetailId = verificationResult['paymentDetailId'] ?? verificationResult['paymentId'] ?? razorpayResponse.paymentId;
|
||||||
|
|
||||||
|
final subscribePayload = {
|
||||||
|
"tenantEnquireId": _pendingTenantEnquireId,
|
||||||
|
"paymentDetailId": paymentDetailId,
|
||||||
|
"planId": _pendingPlanId,
|
||||||
|
};
|
||||||
|
|
||||||
|
logSafe("🟢 Subscribing tenant automatically: $subscribePayload");
|
||||||
|
|
||||||
|
final subResp = await _tenantService.subscribeTenant(subscribePayload);
|
||||||
|
|
||||||
|
// Clear pending values immediately to avoid double-invoke
|
||||||
|
_pendingTenantEnquireId = null;
|
||||||
|
_pendingPlanId = null;
|
||||||
|
|
||||||
|
if (subResp == null) {
|
||||||
|
logSafe("❌ subscribeTenant returned null");
|
||||||
|
_showDialog(
|
||||||
|
title: "Subscription Failed",
|
||||||
|
message: "Failed to call subscription API. Please contact support.",
|
||||||
|
success: false,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = subResp['data'] ?? subResp;
|
||||||
|
// backend success flag might be in different places; check robustly
|
||||||
|
final bool success = (data is Map && (data['success'] == true || subResp['success'] == true)) || (subResp['statusCode'] == 200);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
_showDialog(
|
||||||
|
title: "Subscription Active ✅",
|
||||||
|
message: data['message'] ?? "Tenant subscribed successfully.",
|
||||||
|
success: true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_showDialog(
|
||||||
|
title: "Subscription Failed",
|
||||||
|
message: data['message'] ?? "Subscription API did not confirm success.",
|
||||||
|
success: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handlePaymentError(PaymentFailureResponse response) {
|
void _handlePaymentError(PaymentFailureResponse response) {
|
||||||
logSafe("❌ Payment Failed: ${response.message}");
|
logSafe("❌ Payment Failed: ${response.message}");
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class SubscriptionController extends GetxController {
|
|||||||
var isLoading = true.obs;
|
var isLoading = true.obs;
|
||||||
|
|
||||||
// Frequency tabs
|
// Frequency tabs
|
||||||
final frequencies = ['monthly', 'quarterly', 'halfyearly', 'yearly'];
|
final frequencies = ['monthly', 'quarterly', 'half-yearly', 'yearly'];
|
||||||
var selectedFrequency = 'monthly'.obs;
|
var selectedFrequency = 'monthly'.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -104,4 +104,9 @@ class ApiEndpoints {
|
|||||||
// Payment Module API Endpoints
|
// Payment Module API Endpoints
|
||||||
static const String createOrder = "/payment/create-order";
|
static const String createOrder = "/payment/create-order";
|
||||||
static const String verifyPayment = "/payment/verify-payment";
|
static const String verifyPayment = "/payment/verify-payment";
|
||||||
|
|
||||||
|
// Tenant endpoints
|
||||||
|
static const String createTenantSelf = '/Tenant/self/create';
|
||||||
|
static const String tenantSubscribe = '/Tenant/self/subscription';
|
||||||
|
static const String tenantRenewSubscription = '/Tenant/renew/subscription';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -283,16 +283,20 @@ class ApiService {
|
|||||||
Map<String, String>? additionalHeaders,
|
Map<String, String>? additionalHeaders,
|
||||||
Duration customTimeout = extendedTimeout,
|
Duration customTimeout = extendedTimeout,
|
||||||
bool hasRetried = false,
|
bool hasRetried = false,
|
||||||
|
bool requireAuth = true, // ✅ added
|
||||||
}) async {
|
}) async {
|
||||||
String? token = await _getToken();
|
String? token;
|
||||||
if (token == null) return null;
|
|
||||||
|
if (requireAuth) {
|
||||||
|
token = await _getToken();
|
||||||
|
if (token == null) return null;
|
||||||
|
}
|
||||||
|
|
||||||
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
||||||
logSafe(
|
|
||||||
"PUT $uri\nHeaders: ${_headers(token)}\nBody: $body",
|
|
||||||
);
|
|
||||||
final headers = {
|
final headers = {
|
||||||
..._headers(token),
|
'Content-Type': 'application/json',
|
||||||
|
if (requireAuth && token != null) ..._headers(token),
|
||||||
if (additionalHeaders != null) ...additionalHeaders,
|
if (additionalHeaders != null) ...additionalHeaders,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -305,19 +309,23 @@ class ApiService {
|
|||||||
.put(uri, headers: headers, body: jsonEncode(body))
|
.put(uri, headers: headers, body: jsonEncode(body))
|
||||||
.timeout(customTimeout);
|
.timeout(customTimeout);
|
||||||
|
|
||||||
if (response.statusCode == 401 && !hasRetried) {
|
if (response.statusCode == 401 && requireAuth && !hasRetried) {
|
||||||
logSafe("Unauthorized PUT. Attempting token refresh...");
|
logSafe("⚠️ Unauthorized PUT. Attempting token refresh...");
|
||||||
if (await AuthService.refreshToken()) {
|
if (await AuthService.refreshToken()) {
|
||||||
return await _putRequest(endpoint, body,
|
return await _putRequest(
|
||||||
additionalHeaders: additionalHeaders,
|
endpoint,
|
||||||
customTimeout: customTimeout,
|
body,
|
||||||
hasRetried: true);
|
additionalHeaders: additionalHeaders,
|
||||||
|
customTimeout: customTimeout,
|
||||||
|
hasRetried: true,
|
||||||
|
requireAuth: requireAuth,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("HTTP PUT Exception: $e", level: LogLevel.error);
|
logSafe("❌ HTTP PUT Exception: $e", level: LogLevel.error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2247,6 +2255,59 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create tenant (self)
|
||||||
|
static Future<Map<String, dynamic>?> createTenantSelf(
|
||||||
|
Map<String, dynamic> payload) async {
|
||||||
|
try {
|
||||||
|
final response = await _postRequest(
|
||||||
|
ApiEndpoints.createTenantSelf,
|
||||||
|
payload,
|
||||||
|
requireAuth: false, // likely public
|
||||||
|
);
|
||||||
|
if (response == null) return null;
|
||||||
|
return _parseResponse(response, label: "Create Tenant Self");
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("❌ Exception in createTenantSelf: $e", level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe tenant (after successful payment)
|
||||||
|
static Future<Map<String, dynamic>?> subscribeTenant(
|
||||||
|
Map<String, dynamic> payload) async {
|
||||||
|
try {
|
||||||
|
final response = await _postRequest(
|
||||||
|
ApiEndpoints.tenantSubscribe,
|
||||||
|
payload,
|
||||||
|
requireAuth:
|
||||||
|
true, // likely needs auth if user logged in; set according to backend
|
||||||
|
);
|
||||||
|
if (response == null) return null;
|
||||||
|
return _parseResponse(response, label: "Tenant Subscribe");
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("❌ Exception in subscribeTenant: $e", level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renew tenant subscription (PUT)
|
||||||
|
static Future<Map<String, dynamic>?> renewTenantSubscription(
|
||||||
|
Map<String, dynamic> payload) async {
|
||||||
|
try {
|
||||||
|
final response = await _putRequest(
|
||||||
|
ApiEndpoints.tenantRenewSubscription,
|
||||||
|
payload,
|
||||||
|
requireAuth: true,
|
||||||
|
);
|
||||||
|
if (response == null) return null;
|
||||||
|
return _parseResponse(response, label: "Renew Tenant Subscription");
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("❌ Exception in renewTenantSubscription: $e",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === Employee APIs ===
|
// === Employee APIs ===
|
||||||
/// Search employees by first name and last name only (not middle name)
|
/// Search employees by first name and last name only (not middle name)
|
||||||
/// Returns a list of up to 10 employee records matching the search string.
|
/// Returns a list of up to 10 employee records matching the search string.
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import 'package:marco/helpers/services/api_endpoints.dart';
|
|||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/tenant/tenant_list_model.dart';
|
import 'package:marco/model/tenant/tenant_list_model.dart';
|
||||||
|
|
||||||
/// Abstract interface for tenant service functionality
|
/// Abstract interface for tenant service functionality
|
||||||
@ -130,7 +131,7 @@ class TenantService implements ITenantService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Register FCM token after tenant selection
|
// 🔹 Register FCM token after tenant selection
|
||||||
final fcmToken = LocalStorage.getFcmToken();
|
final fcmToken = LocalStorage.getFcmToken();
|
||||||
if (fcmToken?.isNotEmpty ?? false) {
|
if (fcmToken?.isNotEmpty ?? false) {
|
||||||
final success = await AuthService.registerDeviceToken(fcmToken!);
|
final success = await AuthService.registerDeviceToken(fcmToken!);
|
||||||
logSafe(
|
logSafe(
|
||||||
@ -160,4 +161,44 @@ class TenantService implements ITenantService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> createTenant(
|
||||||
|
Map<String, dynamic> payload) async {
|
||||||
|
try {
|
||||||
|
logSafe("🟢 Creating tenant: $payload");
|
||||||
|
final resp = await ApiService.createTenantSelf(payload);
|
||||||
|
logSafe("🧩 createTenant response: $resp");
|
||||||
|
return resp;
|
||||||
|
} catch (e, s) {
|
||||||
|
logSafe("❌ Exception in createTenant: $e\n$s", level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> subscribeTenant(
|
||||||
|
Map<String, dynamic> payload) async {
|
||||||
|
try {
|
||||||
|
logSafe("🟢 Subscribing tenant: $payload");
|
||||||
|
final resp = await ApiService.subscribeTenant(payload);
|
||||||
|
logSafe("🧩 subscribeTenant response: $resp");
|
||||||
|
return resp;
|
||||||
|
} catch (e, s) {
|
||||||
|
logSafe("❌ Exception in subscribeTenant: $e\n$s", level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> renewSubscription(
|
||||||
|
Map<String, dynamic> payload) async {
|
||||||
|
try {
|
||||||
|
logSafe("🟢 Renewing subscription: $payload");
|
||||||
|
final resp = await ApiService.renewTenantSubscription(payload);
|
||||||
|
logSafe("🧩 renewSubscription response: $resp");
|
||||||
|
return resp;
|
||||||
|
} catch (e, s) {
|
||||||
|
logSafe("❌ Exception in renewSubscription: $e\n$s",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import 'package:marco/view/document/user_document_screen.dart';
|
|||||||
import 'package:marco/view/tenant/tenant_selection_screen.dart';
|
import 'package:marco/view/tenant/tenant_selection_screen.dart';
|
||||||
import 'package:marco/view/payment/payment_screen.dart';
|
import 'package:marco/view/payment/payment_screen.dart';
|
||||||
import 'package:marco/view/subscriptions/subscriptions_screen.dart';
|
import 'package:marco/view/subscriptions/subscriptions_screen.dart';
|
||||||
|
import 'package:marco/view/tenant/tenant_create_screen.dart';
|
||||||
|
|
||||||
class AuthMiddleware extends GetMiddleware {
|
class AuthMiddleware extends GetMiddleware {
|
||||||
@override
|
@override
|
||||||
@ -34,6 +35,7 @@ class AuthMiddleware extends GetMiddleware {
|
|||||||
'/subscription',
|
'/subscription',
|
||||||
'/payment',
|
'/payment',
|
||||||
'/select-tenant',
|
'/select-tenant',
|
||||||
|
'/create-tenant',
|
||||||
];
|
];
|
||||||
|
|
||||||
// ✅ Allow any route that starts with these public routes
|
// ✅ Allow any route that starts with these public routes
|
||||||
@ -110,6 +112,11 @@ getPageRoute() {
|
|||||||
middlewares: [AuthMiddleware()]),
|
middlewares: [AuthMiddleware()]),
|
||||||
// Payment
|
// Payment
|
||||||
GetPage(name: '/payment', page: () => PaymentScreen()),
|
GetPage(name: '/payment', page: () => PaymentScreen()),
|
||||||
|
GetPage(
|
||||||
|
name: '/create-tenant',
|
||||||
|
page: () => const TenantCreateScreen(),
|
||||||
|
),
|
||||||
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: '/subscription',
|
name: '/subscription',
|
||||||
page: () => SubscriptionScreen(),
|
page: () => SubscriptionScreen(),
|
||||||
|
|||||||
@ -10,7 +10,7 @@ class PaymentScreen extends StatefulWidget {
|
|||||||
const PaymentScreen({
|
const PaymentScreen({
|
||||||
super.key,
|
super.key,
|
||||||
this.amount = 0.0,
|
this.amount = 0.0,
|
||||||
this.description = "No description",
|
this.description = "No description",
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -66,7 +66,10 @@ class _PaymentScreenState extends State<PaymentScreen> {
|
|||||||
final controller = _controller!;
|
final controller = _controller!;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Payment", style: TextStyle(color: Colors.black),),
|
title: const Text(
|
||||||
|
"Payment",
|
||||||
|
style: TextStyle(color: Colors.black),
|
||||||
|
),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
@ -90,10 +93,18 @@ class _PaymentScreenState extends State<PaymentScreen> {
|
|||||||
Center(
|
Center(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
// Extract optional IDs passed via Get.arguments
|
||||||
|
final String tenantEnquireId =
|
||||||
|
(args['tenantEnquireId'] ?? '') as String;
|
||||||
|
final String planId = (args['planId'] ?? '') as String;
|
||||||
|
|
||||||
await controller.startPayment(
|
await controller.startPayment(
|
||||||
amount: finalAmount,
|
amount: finalAmount,
|
||||||
description: finalDescription,
|
description: finalDescription,
|
||||||
context: context,
|
context: context,
|
||||||
|
tenantEnquireId:
|
||||||
|
tenantEnquireId.isNotEmpty ? tenantEnquireId : null,
|
||||||
|
planId: planId.isNotEmpty ? planId : null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|||||||
@ -16,7 +16,7 @@ class SubscriptionScreen extends StatelessWidget {
|
|||||||
title: const Text(
|
title: const Text(
|
||||||
'Subscription Plans',
|
'Subscription Plans',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@ -106,11 +106,11 @@ class SubscriptionScreen extends StatelessWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Get.toNamed('/payment', arguments: {
|
Get.toNamed('/create-tenant', arguments: {
|
||||||
'amount': plan['price'] ?? 0,
|
|
||||||
'description':
|
|
||||||
plan['planName'] ?? 'Subscription',
|
|
||||||
'planId': plan['id'] ?? '',
|
'planId': plan['id'] ?? '',
|
||||||
|
'amount': plan['price'] ?? 0,
|
||||||
|
'planName':
|
||||||
|
plan['planName'] ?? 'Subscription',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: MyText.bodyMedium(
|
child: MyText.bodyMedium(
|
||||||
|
|||||||
263
lib/view/tenant/tenant_create_screen.dart
Normal file
263
lib/view/tenant/tenant_create_screen.dart
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:marco/helpers/services/tenant_service.dart';
|
||||||
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
|
class TenantCreateScreen extends StatefulWidget {
|
||||||
|
const TenantCreateScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TenantCreateScreen> createState() => _TenantCreateScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TenantCreateScreenState extends State<TenantCreateScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _firstNameCtrl = TextEditingController();
|
||||||
|
final _lastNameCtrl = TextEditingController();
|
||||||
|
final _orgNameCtrl = TextEditingController();
|
||||||
|
final _emailCtrl = TextEditingController();
|
||||||
|
final _contactCtrl = TextEditingController();
|
||||||
|
final _billingCtrl = TextEditingController();
|
||||||
|
final _orgSizeCtrl = TextEditingController();
|
||||||
|
final _industryIdCtrl = TextEditingController();
|
||||||
|
final _referenceCtrl = TextEditingController();
|
||||||
|
|
||||||
|
final TenantService _tenantService = TenantService();
|
||||||
|
bool _loading = false;
|
||||||
|
|
||||||
|
// Values from subscription screen
|
||||||
|
late final String planId;
|
||||||
|
late final double amount;
|
||||||
|
late final String planName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final args = (Get.arguments ?? {}) as Map<String, dynamic>;
|
||||||
|
planId = args['planId'] ?? '';
|
||||||
|
amount = (args['amount'] ?? 0).toDouble();
|
||||||
|
planName = args['planName'] ?? 'Subscription';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_firstNameCtrl.dispose();
|
||||||
|
_lastNameCtrl.dispose();
|
||||||
|
_orgNameCtrl.dispose();
|
||||||
|
_emailCtrl.dispose();
|
||||||
|
_contactCtrl.dispose();
|
||||||
|
_billingCtrl.dispose();
|
||||||
|
_orgSizeCtrl.dispose();
|
||||||
|
_industryIdCtrl.dispose();
|
||||||
|
_referenceCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
final payload = {
|
||||||
|
"firstName": _firstNameCtrl.text.trim(),
|
||||||
|
"lastName": _lastNameCtrl.text.trim(),
|
||||||
|
"organizationName": _orgNameCtrl.text.trim(),
|
||||||
|
"email": _emailCtrl.text.trim(),
|
||||||
|
"contactNumber": _contactCtrl.text.trim(),
|
||||||
|
"billingAddress": _billingCtrl.text.trim(),
|
||||||
|
"organizationSize": _orgSizeCtrl.text.trim(),
|
||||||
|
"industryId": _industryIdCtrl.text.trim(),
|
||||||
|
"reference": _referenceCtrl.text.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setState(() => _loading = true);
|
||||||
|
final resp = await _tenantService.createTenant(payload);
|
||||||
|
setState(() => _loading = false);
|
||||||
|
|
||||||
|
if (resp == null) {
|
||||||
|
Get.snackbar("Error", "Failed to create tenant. Try again later.",
|
||||||
|
backgroundColor: Colors.red, colorText: Colors.white);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = resp['data'] ?? resp;
|
||||||
|
final tenantEnquireId =
|
||||||
|
data['tenantEnquireId'] ?? data['id'] ?? data['tenantId'];
|
||||||
|
|
||||||
|
if (tenantEnquireId == null) {
|
||||||
|
logSafe("❌ Create tenant response missing id: $resp");
|
||||||
|
Get.snackbar("Error", "Tenant created but server didn't return id.",
|
||||||
|
backgroundColor: Colors.red, colorText: Colors.white);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Go to payment screen with all details
|
||||||
|
Get.toNamed('/payment', arguments: {
|
||||||
|
'amount': amount,
|
||||||
|
'description': 'Subscription for ${_orgNameCtrl.text}',
|
||||||
|
'tenantEnquireId': tenantEnquireId,
|
||||||
|
'planId': planId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final divider = Divider(color: Colors.grey.shade300, height: 32);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Create Tenant"),
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Personal Info",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _firstNameCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "First Name *",
|
||||||
|
hintText: "e.g., John",
|
||||||
|
prefixIcon: Icon(Icons.person_outline),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: (v) => v == null || v.isEmpty ? "Required" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _lastNameCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Last Name *",
|
||||||
|
hintText: "e.g., Doe",
|
||||||
|
prefixIcon: Icon(Icons.person_outline),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: (v) => v == null || v.isEmpty ? "Required" : null,
|
||||||
|
),
|
||||||
|
divider,
|
||||||
|
const Text(
|
||||||
|
"Organization Details",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _orgNameCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Organization Name *",
|
||||||
|
hintText: "e.g., MarcoBMS",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: (v) => v == null || v.isEmpty ? "Required" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _billingCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Billing Address",
|
||||||
|
hintText: "e.g., 123 Main Street, Mumbai",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _orgSizeCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Organization Size",
|
||||||
|
hintText: "e.g., 1-10, 11-50",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _industryIdCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Industry ID (UUID)",
|
||||||
|
hintText: "e.g., 3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
divider,
|
||||||
|
const Text(
|
||||||
|
"Contact Info",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _emailCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Email",
|
||||||
|
hintText: "e.g., john.doe@example.com",
|
||||||
|
prefixIcon: Icon(Icons.email_outlined),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: (v) =>
|
||||||
|
v == null || !v.contains('@') ? "Invalid email" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _contactCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Contact Number",
|
||||||
|
hintText: "e.g., +91 9876543210",
|
||||||
|
prefixIcon: Icon(Icons.phone_outlined),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
divider,
|
||||||
|
const Text(
|
||||||
|
"Other Details",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _referenceCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Reference",
|
||||||
|
hintText: "e.g., Referral Name or Code",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: _loading ? null : _submit,
|
||||||
|
child: _loading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 22,
|
||||||
|
width: 22,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
"Create Tenant & Continue",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user