diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 4b5eb54..c0c360b 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -109,4 +109,5 @@ class ApiEndpoints { static const String createTenantSelf = '/Tenant/self/create'; static const String tenantSubscribe = '/Tenant/self/subscription'; static const String tenantRenewSubscription = '/Tenant/renew/subscription'; + static const String getIndustries = '/market/industries'; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 91feee9..17e2995 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -2308,6 +2308,21 @@ class ApiService { } } + /// Get list of industries (for tenant creation drop-down) + static Future?> getIndustries() async { + try { + final response = await _getRequest( + ApiEndpoints.getIndustries, + requireAuth: false, // usually public + ); + if (response == null) return null; + return _parseResponse(response, label: "Get Industries"); + } catch (e) { + logSafe("❌ Exception in getIndustries: $e", level: LogLevel.error); + return null; + } + } + // === Employee APIs === /// 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. diff --git a/lib/helpers/services/tenant_service.dart b/lib/helpers/services/tenant_service.dart index 5e2adfa..e6bd800 100644 --- a/lib/helpers/services/tenant_service.dart +++ b/lib/helpers/services/tenant_service.dart @@ -201,4 +201,25 @@ class TenantService implements ITenantService { return null; } } + + Future>?> getIndustries() async { + try { + logSafe("🟢 Fetching industries via ApiService..."); + + // ApiService.getIndustries() directly returns a List + final response = await ApiService.getIndustries(); + + // No need to dig into response["data"], because response itself is a List + if (response == null || response.isEmpty) { + logSafe("💡 ! No industries found (empty list)"); + return null; + } + + // Safely cast list of maps + return List>.from(response); + } catch (e, s) { + logSafe("❌ Exception in getIndustries: $e\n$s", level: LogLevel.error); + return null; + } + } } diff --git a/lib/view/payment/payment_screen.dart b/lib/view/payment/payment_screen.dart index 0d19a70..276cf55 100644 --- a/lib/view/payment/payment_screen.dart +++ b/lib/view/payment/payment_screen.dart @@ -67,7 +67,7 @@ class _PaymentScreenState extends State { return Scaffold( appBar: AppBar( title: const Text( - "Payment", + "Payment", style: TextStyle(color: Colors.black), ), backgroundColor: Colors.white, diff --git a/lib/view/tenant/tenant_create_screen.dart b/lib/view/tenant/tenant_create_screen.dart index 001a189..78b16b0 100644 --- a/lib/view/tenant/tenant_create_screen.dart +++ b/lib/view/tenant/tenant_create_screen.dart @@ -19,13 +19,15 @@ class _TenantCreateScreenState extends State { 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 + List> _industries = []; + String? _selectedIndustryId; + bool _loadingIndustries = true; + late final String planId; late final double amount; late final String planName; @@ -37,6 +39,21 @@ class _TenantCreateScreenState extends State { planId = args['planId'] ?? ''; amount = (args['amount'] ?? 0).toDouble(); planName = args['planName'] ?? 'Subscription'; + _fetchIndustries(); + } + + Future _fetchIndustries() async { + try { + setState(() => _loadingIndustries = true); + final list = await _tenantService.getIndustries(); + setState(() { + _industries = (list ?? []).whereType>().toList(); + _loadingIndustries = false; + }); + } catch (e, s) { + logSafe("❌ Failed to fetch industries: $e\n$s", level: LogLevel.error); + setState(() => _loadingIndustries = false); + } } @override @@ -48,13 +65,17 @@ class _TenantCreateScreenState extends State { _contactCtrl.dispose(); _billingCtrl.dispose(); _orgSizeCtrl.dispose(); - _industryIdCtrl.dispose(); _referenceCtrl.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate()) return; + if (_selectedIndustryId == null || _selectedIndustryId!.isEmpty) { + Get.snackbar("Error", "Please select industry", + backgroundColor: Colors.red, colorText: Colors.white); + return; + } final payload = { "firstName": _firstNameCtrl.text.trim(), @@ -64,32 +85,31 @@ class _TenantCreateScreenState extends State { "contactNumber": _contactCtrl.text.trim(), "billingAddress": _billingCtrl.text.trim(), "organizationSize": _orgSizeCtrl.text.trim(), - "industryId": _industryIdCtrl.text.trim(), + "industryId": _selectedIndustryId, "reference": _referenceCtrl.text.trim(), }; setState(() => _loading = true); final resp = await _tenantService.createTenant(payload); + if (!mounted) return; setState(() => _loading = false); - if (resp == null) { - Get.snackbar("Error", "Failed to create tenant. Try again later.", + if (resp == null || resp['success'] == false) { + Get.snackbar("Error", + resp?['message'] ?? "Failed to create tenant. Try again later.", backgroundColor: Colors.red, colorText: Colors.white); return; } - final data = resp['data'] ?? resp; + final data = Map.from(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); + logSafe("❌ Missing tenant ID in response: $resp"); return; } - // ✅ Go to payment screen with all details Get.toNamed('/payment', arguments: { 'amount': amount, 'description': 'Subscription for ${_orgNameCtrl.text}', @@ -100,161 +120,160 @@ class _TenantCreateScreenState extends State { @override Widget build(BuildContext context) { - final divider = Divider(color: Colors.grey.shade300, height: 32); - return Scaffold( + resizeToAvoidBottomInset: true, // ensures scroll works with keyboard appBar: AppBar( - title: const Text("Create Tenant"), + title: const Text( + "Create Tenant", + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + ), 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, + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.all(20), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Personal Info", + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w600)), + const Divider(height: 20), + _buildTextField( + _firstNameCtrl, "First Name *", Icons.person, + validator: (v) => v!.isEmpty ? "Required" : null), + _buildTextField(_lastNameCtrl, "Last Name *", + Icons.person_outline, + validator: (v) => v!.isEmpty ? "Required" : null), + const SizedBox(height: 12), + const Text("Organization", + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w600)), + const Divider(height: 20), + _buildTextField(_orgNameCtrl, "Organization Name *", + Icons.business, + validator: (v) => v!.isEmpty ? "Required" : null), + const SizedBox(height: 8), + _loadingIndustries + ? const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + )) + : Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: + Border.all(color: Colors.grey.shade400), + ), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 2), + child: DropdownButtonFormField( + isExpanded: true, + borderRadius: BorderRadius.circular(10), + dropdownColor: Colors.white, + icon: const Icon( + Icons.keyboard_arrow_down_rounded), + value: _selectedIndustryId, + decoration: const InputDecoration( + prefixIcon: + Icon(Icons.apartment_outlined), + labelText: "Industry *", + border: InputBorder.none, + ), + items: _industries.map((itm) { + final id = itm['id']?.toString() ?? ''; + final name = itm['name'] ?? + itm['displayName'] ?? + 'Unknown'; + return DropdownMenuItem( + value: id, child: Text(name)); + }).toList(), + onChanged: (v) => + setState(() => _selectedIndustryId = v), + validator: (v) => v == null || v.isEmpty + ? "Select industry" + : null, + ), + ), + const SizedBox(height: 16), + const Text("Contact Details", + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w600)), + const Divider(height: 20), + _buildTextField( + _emailCtrl, "Email *", Icons.email_outlined, + validator: (v) => v == null || !v.contains('@') + ? "Invalid email" + : null), + _buildTextField( + _contactCtrl, "Contact Number", Icons.phone), + const SizedBox(height: 16), + const Text("Additional Info", + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w600)), + const Divider(height: 20), + _buildTextField(_billingCtrl, "Billing Address", + Icons.home_outlined), + _buildTextField(_orgSizeCtrl, "Organization Size", + Icons.people_alt_outlined), + _buildTextField(_referenceCtrl, "Reference", + Icons.note_alt_outlined), + const Spacer(), + const SizedBox(height: 24), + Center( + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _loading ? null : _submit, + style: ElevatedButton.styleFrom( + padding: + const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + backgroundColor: Colors.deepPurple, + ), + child: _loading + ? const CircularProgressIndicator( + color: Colors.white) + : const Text("Create Tenant & Continue", + style: TextStyle(fontSize: 16)), + ), ), - ) - : const Text( - "Create Tenant & Continue", - style: TextStyle(fontSize: 16), ), + ]), ), ), ), - ], + ); + }, + ), + ), + ); + } + + Widget _buildTextField( + TextEditingController controller, String label, IconData icon, + {String? Function(String?)? validator}) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TextFormField( + controller: controller, + validator: validator, + decoration: InputDecoration( + prefixIcon: Icon(icon), + labelText: label, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), ), ), ),