site , signup and demo completed

export_module_ui_ux_enhancement_signup
WaseemAbbasi22 2 weeks ago
parent 527ba1eb19
commit f116c96578

@ -577,4 +577,15 @@ class URLs {
static get generateReport => "$_baseUrl/ReportPDF/GenerateReport";
//4104 attachment type lookup
// Demo Request APIs
static get getRoles => "$_baseUrl/Lookups/GetLookup?lookupEnum=4017"; // get roles for demo request
static get submitDemoRequest => "$_baseUrl/DemoRequest/CreateDemoRequest"; // post submit demo request
static get getDemoRequests => "$_baseUrl/DemoRequest/GetDemoRequests"; // get demo request history
// Application Demo Request APIs
static get getApplicationDemoRoles => "$_baseUrl/Account/GetRolesByAssetGroupId?assetGroupId=6"; // get roles for application demo request
static get submitApplicationDemoRequest => "$_baseUrl/DemoUser/CreateDemoUser"; // post submit application demo request// get application demo request history
static get sendDemoUserOtp => "$_baseUrl/DemoUser/EmailVerification"; // post send OTP to user
static get verifyDemoUserOtp => "$_baseUrl/DemoUser/VerifyOtp"; // post verify OTP
}

@ -47,7 +47,8 @@ extension BuildContextExtension on BuildContext {
context: this,
useSafeArea: true,
isScrollControlled: true,
isDismissible: true,
isDismissible: isDismissible ?? true,
enableDrag: isDismissible ?? true,
backgroundColor: Colors.transparent,
builder: (context) => SingleChildScrollView(padding: const EdgeInsets.all(0), child: childWidget.bottomSafeArea).bottomSheetContainerNew(context, title: title,showCancelButton: showCancelButton),
);

@ -171,12 +171,12 @@ extension WidgetExtensions on Widget {
title.bottomSheetHeadingTextStyle(context).expanded,
if (showCancelButton) ...[
8.width,
InkWell(
onTap: () => Navigator.pop(context),
child: Icon(
Icons.cancel_outlined,
color: context.isDarkNotListen ? AppColor.neutral30 : AppColor.black20,
),
IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close),
color: context.isDarkNotListen ? AppColor.neutral30 : AppColor.headingTextColor(context),
constraints: const BoxConstraints(),
onPressed: () => Navigator.pop(context),
)
]
],

@ -64,11 +64,10 @@ import 'package:test_sa/modules/signup/screens/signup_screen.dart';
import 'package:test_sa/modules/site_request_module/providers/site_request_provider.dart';
import 'package:test_sa/modules/site_request_module/screens/site_request_form_screen.dart';
import 'package:test_sa/modules/site_request_module/screens/site_request_history_screen.dart';
import 'package:test_sa/modules/applicationDemo/providers/application_demo_request_provider.dart';
import 'package:test_sa/modules/applicationDemo/screens/demo_request_form_screen.dart';
import 'package:test_sa/modules/tm_module/gas_refill/create_gas_refill_request.dart';
import 'package:test_sa/modules/tm_module/gas_refill/update_gas_refill_request.dart';
import 'package:test_sa/modules/traf_module/create_traf_request_page.dart';
import 'package:test_sa/modules/traf_module/traf_request_provider.dart';
import 'package:test_sa/modules/traf_module/update_traf_request_page.dart';
import 'package:test_sa/new_views/app_style/app_themes.dart';
import 'package:test_sa/new_views/pages/help_center_page.dart';
import 'package:test_sa/new_views/pages/land_page/land_page.dart';
@ -310,10 +309,9 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => PpmServiceProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => PpmChecklistStatusProvider(), lazy: true),
// TM MODULE PROVIDERS (11)
// TM MODULE PROVIDERS (10)
ChangeNotifierProvider(create: (_) => DeviceTransferProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => AssetTransferStatusProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => TrafRequestProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => TaskRequestProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => TaskTypeProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => TaskJobTypeOfAlertProvider(), lazy: true),
@ -358,6 +356,9 @@ class MyApp extends StatelessWidget {
// SITE REQUEST MODULE PROVIDERS (1)
ChangeNotifierProvider(create: (_) => SiteRequestProvider(), lazy: true),
// APPLICATION DEMO REQUEST MODULE PROVIDERS (1)
ChangeNotifierProvider(create: (_) => ApplicationDemoRequestProvider(), lazy: true),
// EMPLOYEE/USER PROVIDERS (2)
ChangeNotifierProvider(create: (_) => NurseProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => UserSearchProvider(), lazy: true),
@ -412,10 +413,7 @@ class MyApp extends StatelessWidget {
LandPage.routeName: (_) => const LandPage(),
GasRefillRequestForm.routeName: (_) => const GasRefillRequestForm(),
CreateTaskView.id: (_) => const CreateTaskView(),
CreateTRAFRequestPage.id: (_) => CreateTRAFRequestPage(),
UpdateTrafRequestPage.id: (_) => UpdateTrafRequestPage(),
CreateGasRefillRequest.id: (_) => const CreateGasRefillRequest(),
UpdateGasRefillRequest.id: (_) => const UpdateGasRefillRequest(),
CreateCMRequest.id: (_) => const CreateCMRequest(),
MyAssetsPage.id: (_) => const MyAssetsPage(),
SingleDepartmentPicker.id: (_) => const SingleDepartmentPicker(),
@ -441,6 +439,7 @@ class MyApp extends StatelessWidget {
CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(),
CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(),
CleanerSearchListView.routeName: (_) => const CleanerSearchListView(),
DemoRequestFormScreen.routeName: (_) => const DemoRequestFormScreen(),
},
),
),

@ -0,0 +1,18 @@
/// Enum for OTP delivery methods
enum OtpMethod {
sms('SMS'),
email('Email');
final String value;
const OtpMethod(this.value);
/// Get OTP method from string value
static OtpMethod fromString(String value) {
return OtpMethod.values.firstWhere(
(method) => method.value.toLowerCase() == value.toLowerCase(),
orElse: () => OtpMethod.sms,
);
}
}

@ -0,0 +1,117 @@
/// Model representing an application demo request
class ApplicationDemoRequestModel {
final String? id;
final String fullName;
final String emailAddress;
final String mobileNumber;
final String? companyName;
final int? companyId;
final List<String> roleIds;
final List<String> roleNames;
final String status; // Pending, Approved, Rejected
final DateTime requestDate;
ApplicationDemoRequestModel({
this.id,
required this.fullName,
required this.emailAddress,
required this.mobileNumber,
this.companyName,
this.companyId,
required this.roleIds,
required this.roleNames,
required this.status,
required this.requestDate,
});
/// Converts model to JSON for API submission
Map<String, dynamic> toJson() {
return {
'id': id ?? 0,
'fullName': fullName,
'emailAddress': emailAddress,
'mobileNumber': mobileNumber,
// 'companyName': companyId ?? 88,
'companyId': 88,
'demoRequestUserRoleIds': roleIds,
};
}
/// Creates model from API response JSON
factory ApplicationDemoRequestModel.fromJson(Map<String, dynamic> json) {
return ApplicationDemoRequestModel(
id: json['id']?.toString() ?? json['requestNumber']?.toString() ?? '',
fullName: json['fullName'] ?? '',
emailAddress: json['emailAddress'] ?? json['email'] ?? '',
mobileNumber: json['mobileNumber'] ?? json['mobile'] ?? '',
companyName: json['companyName'] ?? json['company'] ?? '',
companyId: json['companyId'],
roleIds: _parseList(json['roleIds'] ?? json['demoRequestUserRoleIds']),
roleNames: _parseList(json['roleNames'] ?? json['roles']),
status: json['status'] ?? 'Pending',
requestDate: _parseDate(json),
);
}
/// Helper method to parse list from various formats
static List<String> _parseList(dynamic value) {
if (value == null) return [];
if (value is List) {
return value.map((e) => e.toString()).toList();
}
if (value is String) {
if (value.isEmpty) return [];
return value.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
}
return [];
}
/// Helper method to parse date from various formats
static DateTime _parseDate(Map<String, dynamic> json) {
final dateValue = json['requestDate'] ?? json['createdDate'] ?? json['date'];
if (dateValue == null) return DateTime.now();
if (dateValue is String) {
try {
return DateTime.parse(dateValue);
} catch (e) {
return DateTime.now();
}
}
if (dateValue is DateTime) return dateValue;
return DateTime.now();
}
/// Creates a copy with optional field updates
ApplicationDemoRequestModel copyWith({
String? id,
String? fullName,
String? emailAddress,
String? mobileNumber,
String? companyName,
int? companyId,
List<String>? roleIds,
List<String>? roleNames,
String? status,
DateTime? requestDate,
}) {
return ApplicationDemoRequestModel(
id: id ?? this.id,
fullName: fullName ?? this.fullName,
emailAddress: emailAddress ?? this.emailAddress,
mobileNumber: mobileNumber ?? this.mobileNumber,
companyName: companyName ?? this.companyName,
companyId: companyId ?? this.companyId,
roleIds: roleIds ?? this.roleIds,
roleNames: roleNames ?? this.roleNames,
status: status ?? this.status,
requestDate: requestDate ?? this.requestDate,
);
}
}

@ -0,0 +1,21 @@
import 'package:test_sa/models/base.dart';
class ApplicationDemoRoleModel extends Base {
final int? value;
final String identifier;
ApplicationDemoRoleModel({
required String id,
required String name,
this.value,
}) : identifier = id.toString(),
super(identifier: id, name: name);
factory ApplicationDemoRoleModel.fromJson(Map<String, dynamic> json) {
return ApplicationDemoRoleModel(
id: json['id'] ?? '',
name: json['name'] ?? '',
value: json['fixedName'] != null ? int.tryParse(json['fixedName'].toString()) : null,
);
}
}

@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/modules/applicationDemo/models/application_demo_request_model.dart';
import 'package:test_sa/modules/applicationDemo/models/role_model.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
/// Provider for managing application demo request operations
class ApplicationDemoRequestProvider extends ChangeNotifier {
bool _loading = false;
bool _roleLoading = false;
List<ApplicationDemoRoleModel> _roles = [];
String? _errorMessage;
String? _successMessage;
// Getters
bool get loading => _loading;
bool get roleLoading => _roleLoading;
String? get errorMessage => _errorMessage;
String? get successMessage => _successMessage;
List<ApplicationDemoRoleModel> get roles => _roles;
// Setter
set loading(bool value) {
_loading = value;
notifyListeners();
}
set roleLoading(bool value) {
_loading = value;
notifyListeners();
}
/// Get headers without authentication token for public APIs
Map<String, String> get _unauthenticatedHeaders => {
'Content-Type': 'application/json',
'X-Timezone-Offset': DateTime.now().timeZoneOffset.toString().split(".").first,
};
/// Fetches available roles from the server (without authentication)
Future<int> fetchRoles() async {
if (roleLoading) return -2;
roleLoading = true;
_errorMessage = null;
notifyListeners();
try {
final response = await http.get(
Uri.parse(URLs.getApplicationDemoRoles),
headers: _unauthenticatedHeaders,
);
if (response.statusCode >= 200 && response.statusCode < 300) {
final List<dynamic> responseData = jsonDecode(response.body);
_roles = responseData.map((json) => ApplicationDemoRoleModel.fromJson(json)).toList();
roleLoading = false;
notifyListeners();
return response.statusCode;
} else {
final responseData = jsonDecode(response.body);
_errorMessage = responseData['message'] ?? 'Failed to fetch roles';
roleLoading = false;
notifyListeners();
return response.statusCode;
}
} catch (error) {
_errorMessage = 'Failed to fetch roles: $error';
roleLoading = false;
notifyListeners();
return -1;
}
}
/// Submits an application demo request to the server (without authentication)
Future<bool> submitApplicationDemoRequest(ApplicationDemoRequestModel request) async {
loading = true;
_errorMessage = null;
_successMessage = null;
notifyListeners();
try {
final requestBody = request.toJson();
final response = await http.post(
Uri.parse(URLs.submitApplicationDemoRequest),
headers: _unauthenticatedHeaders,
body: json.encode(requestBody),
);
if (response.statusCode >= 200 && response.statusCode < 300) {
final responseData = json.decode(response.body);
_successMessage = responseData['message'] ?? 'Your demo request has been submitted successfully';
loading = false;
notifyListeners();
return true;
} else {
final responseData = json.decode(response.body);
_errorMessage = responseData['message'] ?? 'Failed to submit request';
loading = false;
notifyListeners();
return false;
}
} catch (error) {
_errorMessage = 'Failed to submit request: $error';
loading = false;
notifyListeners();
return false;
}
}
/// Sends OTP to user via email or SMS
Future<bool> sendOtp({
required String fullName,
required String phoneNumber,
required String email,
required String typeSend, // "SMS" or "Email"
}) async {
loading = true;
_errorMessage = null;
_successMessage = null;
notifyListeners();
try {
final requestBody = {
'fullName': fullName,
'phoneNumber': phoneNumber,
'email': email,
'typeSend': typeSend,
};
final response = await http.post(
Uri.parse(URLs.sendDemoUserOtp),
headers: _unauthenticatedHeaders,
body: json.encode(requestBody),
);
if (response.statusCode >= 200 && response.statusCode < 300) {
final responseData = json.decode(response.body);
_successMessage = responseData['message'];
loading = false;
notifyListeners();
return true;
} else {
final responseData = json.decode(response.body);
_errorMessage = responseData['message'] ?? 'Failed to send OTP';
loading = false;
notifyListeners();
return false;
}
} catch (error) {
_errorMessage = 'Failed to send OTP: $error';
loading = false;
notifyListeners();
return false;
}
}
/// Verifies OTP code
Future<bool> verifyOtp({
String? email,
String? phoneNumber,
required String otpCode,
}) async {
loading = true;
_errorMessage = null;
_successMessage = null;
notifyListeners();
try {
final requestBody = <String, dynamic>{
'otpCode': otpCode,
};
// Add email or phoneNumber based on what's provided
if (email != null && email.isNotEmpty) {
requestBody['email'] = email;
}
if (phoneNumber != null && phoneNumber.isNotEmpty) {
requestBody['phoneNumber'] = phoneNumber;
}
final response = await http.post(
Uri.parse(URLs.verifyDemoUserOtp),
headers: _unauthenticatedHeaders,
body: json.encode(requestBody),
);
if (response.statusCode >= 200 && response.statusCode < 300) {
final responseData = json.decode(response.body);
_successMessage = responseData['message'];
loading = false;
notifyListeners();
return true;
} else {
final responseData = json.decode(response.body);
_errorMessage = responseData['message'] ?? 'Invalid OTP code';
loading = false;
notifyListeners();
return false;
}
} catch (error) {
_errorMessage = 'Failed to verify OTP: $error';
loading = false;
notifyListeners();
return false;
}
}
/// Resets the provider state
void reset() {
_loading = false;
_roles.clear();
_errorMessage = null;
notifyListeners();
}
}

@ -0,0 +1,309 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/validator/validator.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/applicationDemo/enums/otp_method.dart';
import 'package:test_sa/modules/applicationDemo/models/application_demo_request_model.dart';
import 'package:test_sa/modules/applicationDemo/models/role_model.dart';
import 'package:test_sa/modules/applicationDemo/providers/application_demo_request_provider.dart';
import 'package:test_sa/modules/applicationDemo/widgets/otp_method_selection_dialog.dart';
import 'package:test_sa/modules/applicationDemo/widgets/otp_verification_bottom_sheet.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
import 'package:test_sa/new_views/common_widgets/custom_app_bar.dart';
import 'package:test_sa/new_views/common_widgets/multiple_item_drop_down_menu.dart';
import 'package:test_sa/providers/loading_list_notifier.dart';
class DemoRequestFormScreen extends StatefulWidget {
static const String routeName = '/demo_request_form';
const DemoRequestFormScreen({Key? key}) : super(key: key);
@override
State<DemoRequestFormScreen> createState() => _DemoRequestFormScreenState();
}
class _DemoRequestFormScreenState extends State<DemoRequestFormScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _fullNameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _mobileController = TextEditingController();
final TextEditingController _companyNameController = TextEditingController();
List<ApplicationDemoRoleModel> _selectedRoles = [];
bool _isSubmitting = false;
@override
void initState() {
super.initState();
// Fetch roles when the screen initializes
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = Provider.of<ApplicationDemoRequestProvider>(context, listen: false);
provider.fetchRoles();
});
}
@override
void dispose() {
_fullNameController.dispose();
_emailController.dispose();
_mobileController.dispose();
_companyNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<ApplicationDemoRequestProvider>(context);
return Scaffold(
backgroundColor: AppColor.background(context),
appBar: const CustomAppBar(
title: 'Demo Request',
),
body: Form(
key: _formKey,
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppTextFormField(
controller: _fullNameController,
labelText: 'Full Name',
hintText: 'Enter your full name',
textInputType: TextInputType.name,
validator: (value) {
if (value == null || value.isEmpty) return 'Full Name is required';
return null;
},
),
16.height,
AppTextFormField(
controller: _emailController,
labelText: 'Email Address',
hintText: 'Enter your email address',
textInputType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) return 'Email is required';
if (!Validator.isEmail(value)) return 'Invalid email format';
return null;
},
),
16.height,
AppTextFormField(
controller: _mobileController,
labelText: 'Mobile Number',
hintText: 'Enter your mobile number',
textInputType: TextInputType.phone,
validator: (value) {
if (value == null || value.isEmpty) return 'Mobile Number is required';
if (!Validator.isPhoneNumber(value)) return 'Invalid mobile number';
return null;
},
),
16.height,
AppTextFormField(
controller: _companyNameController,
labelText: 'Company Name',
hintText: 'Enter your company name',
textInputType: TextInputType.text,
validator: (value) {
if (value == null || value.isEmpty) return 'Company Name is required';
return null;
},
),
16.height,
MultipleItemDropDownMenu<ApplicationDemoRoleModel, NullableLoadingProvider>(
context: context,
title: 'Select Roles',
initialValue: _selectedRoles,
staticData: provider.roles,
loading: provider.roleLoading,
showAsFullScreenDialog: true,
onSelect: (selectedRoles) {
setState(() {
_selectedRoles = selectedRoles ?? [];
});
},
),
],
).paddingAll(16).expanded,
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: 'Submit',
buttonColor: AppColor.primary10,
maxWidth: true,
loading: _isSubmitting,
onPressed: _isSubmitting ? null : _submitRequest,
),
),
],
),
),
);
}
Future<void> _submitRequest() async {
// Validate form fields
if (!_formKey.currentState!.validate()) {
return;
}
if (_selectedRoles.isEmpty) {
context.showConfirmDialog('Please select at least one role');
return;
}
showDialog(
context: context,
builder: (BuildContext context) {
return OtpMethodSelectionDialog(
onMethodSelected: (OtpMethod method) {
_submitRequestWithOtpMethod(method);
},
);
},
);
}
Future<void> _submitRequestWithOtpMethod(OtpMethod otpMethod) async {
setState(() {
_isSubmitting = true;
});
try {
final provider = Provider.of<ApplicationDemoRequestProvider>(context, listen: false);
final otpSent = await provider.sendOtp(
fullName: _fullNameController.text.trim(),
phoneNumber: _mobileController.text.trim(),
email: _emailController.text.trim(),
typeSend: otpMethod.value,
);
if (otpSent) {
if (mounted) {
setState(() {
_isSubmitting = false;
});
await context.showBottomSheet(
OtpVerificationBottomSheet(
otpMethod: otpMethod.value,
onOtpVerified: (String otp) async {
await _verifyOtpAndComplete(otp, otpMethod);
},
onResend: () async {
await provider.sendOtp(
fullName: _fullNameController.text.trim(),
phoneNumber: _mobileController.text.trim(),
email: _emailController.text.trim(),
typeSend: otpMethod.value,
);
},
onChangeMethod: () {
_submitRequest();
},
),
isDismissible: false,
title: 'Verify OTP',
showCancelButton: true,
);
}
} else {
if (mounted) {
setState(() {
_isSubmitting = false;
});
context.showConfirmDialog(
provider.errorMessage ?? 'Failed to send OTP. Please try again.',
);
}
}
} catch (e) {
if (mounted) {
setState(() {
_isSubmitting = false;
});
context.showConfirmDialog('An error occurred: $e');
}
}
}
Future<void> _verifyOtpAndComplete(String otp, OtpMethod otpMethod) async {
// Close the bottom sheet
Navigator.pop(context);
// Show loading
setState(() {
_isSubmitting = true;
});
try {
final provider = Provider.of<ApplicationDemoRequestProvider>(context, listen: false);
// Verify OTP - pass email or phoneNumber based on selected method
final otpVerified = await provider.verifyOtp(
email: otpMethod == OtpMethod.email ? _emailController.text.trim() : null,
phoneNumber: otpMethod == OtpMethod.sms ? _mobileController.text.trim() : null,
otpCode: otp,
);
if (otpVerified) {
// OTP verified, now submit the demo request
final request = ApplicationDemoRequestModel(
fullName: _fullNameController.text.trim(),
emailAddress: _emailController.text.trim(),
mobileNumber: _mobileController.text.trim(),
companyId: 0,
roleIds: _selectedRoles.map((role) => role.identifier ?? '').toList(),
roleNames: _selectedRoles.map((role) => role.name ?? '').toList(),
status: 'Pending',
requestDate: DateTime.now(),
);
final success = await provider.submitApplicationDemoRequest(request);
if (mounted) {
setState(() {
_isSubmitting = false;
});
if (success) {
context.showConfirmDialog(
provider.successMessage ?? 'Your demo request has been verified and submitted successfully. We will contact you soon!',
onTap: () {
Navigator.of(context).pop(); // Close dialog
Navigator.of(context).pop(); // Go back to login
},
);
} else {
context.showConfirmDialog(
provider.errorMessage ?? 'Failed to submit demo request. Please try again.',
);
}
}
} else {
// OTP verification failed
if (mounted) {
setState(() {
_isSubmitting = false;
});
context.showConfirmDialog(
provider.errorMessage ?? 'Invalid OTP code. Please try again.',
);
}
}
} catch (e) {
if (mounted) {
setState(() {
_isSubmitting = false;
});
context.showConfirmDialog('OTP verification failed: $e');
}
}
}
}

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/applicationDemo/enums/otp_method.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
class OtpMethodSelectionDialog extends StatelessWidget {
final Function(OtpMethod method) onMethodSelected;
const OtpMethodSelectionDialog({
Key? key,
required this.onMethodSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(),
insetPadding: const EdgeInsets.only(left: 21, right: 21),
child: Padding(
padding: const EdgeInsets.only(left: 20, right: 20, top: 18, bottom: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: InfoHeader16Widget("Information").paddingOnly(top: 16),
),
IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close),
color: Colors.black87,
constraints: const BoxConstraints(),
onPressed: () => Navigator.pop(context),
)
],
),
"Choose a method to receive the OTP".bodyText(context),
28.height,
Row(
children: [
Expanded(
child: AppFilledButton(
label: OtpMethod.sms.value,
onPressed: () {
Navigator.pop(context);
onMethodSelected(OtpMethod.sms);
},
textColor: Colors.white,
),
),
16.width,
Expanded(
child: AppFilledButton(
label: OtpMethod.email.value,
onPressed: () {
Navigator.pop(context);
onMethodSelected(OtpMethod.email);
},
textColor: Colors.white,
),
),
],
),
],
),
),
);
}
}

@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import 'package:pinput/pinput.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
class OtpVerificationBottomSheet extends StatefulWidget {
final String otpMethod;
final Function(String otp) onOtpVerified;
final VoidCallback? onResend;
final VoidCallback? onChangeMethod;
const OtpVerificationBottomSheet({
Key? key,
required this.otpMethod,
required this.onOtpVerified,
this.onResend,
this.onChangeMethod,
}) : super(key: key);
@override
State<OtpVerificationBottomSheet> createState() => _OtpVerificationBottomSheetState();
}
class _OtpVerificationBottomSheetState extends State<OtpVerificationBottomSheet> {
final ValueNotifier<String> _timerNotifier = ValueNotifier("2:00");
bool _canResend = false;
@override
void initState() {
super.initState();
_startTimer();
}
void _startTimer() {
int seconds = 120; // 2 minutes
_canResend = false;
Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return false;
seconds--;
if (seconds <= 0) {
_timerNotifier.value = "0:00";
_canResend = true;
return false;
}
final minutes = seconds ~/ 60;
final secs = seconds % 60;
_timerNotifier.value = "$minutes:${secs.toString().padLeft(2, '0')}";
return true;
});
}
@override
void dispose() {
_timerNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final defaultPinTheme = PinTheme(
width: 51.toScreenWidth,
height: 63.toScreenHeight,
textStyle: TextStyle(fontSize: 24, color: AppColor.headingTextColor(context)),
decoration: BoxDecoration(
color: AppColor.background(context),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColor.border2Color(context)),
),
);
return WillPopScope(
onWillPop: () async => false, // Prevent dismiss by back button
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoTextWidget(
label: 'OTP has been sent to your ${widget.otpMethod.toLowerCase()}',
isMsg: true,
showEmptyValue: true,
),
Center(
child: Pinput(
length: 4,
defaultPinTheme: defaultPinTheme,
focusedPinTheme: defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration?.copyWith(
border: Border.all(color: AppColor.primary10),
),
),
onCompleted: (pin) async {
// Call the verification callback
widget.onOtpVerified(pin);
},
),
).paddingOnly(top: 24, bottom: 24),
Row(
children: [
InfoTextWidget(label: 'Resend in', isMsg: true, showEmptyValue: true),
6.width,
ValueListenableBuilder<String>(
valueListenable: _timerNotifier,
builder: (context, value, _) {
return InfoTextWidget(label: value, isMsg: true, showEmptyValue: true);
},
),
6.width,
InkWell(
onTap: _canResend
? () {
if (widget.onResend != null) {
widget.onResend!();
_startTimer();
}
}
: null,
child: Text(
'Resend',
style: TextStyle(
color: _canResend ? AppColor.primary10 : AppColor.white50,
fontWeight: FontWeight.w500,
fontSize: 12.toScreenWidth,
decorationColor: _canResend ? AppColor.primary10 : AppColor.white50,
decoration: TextDecoration.underline,
),
),
),
],
),
],
).toShadowContainer(context),
16.height,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
InfoHeader16Widget('Having trouble receiving OTP?'),
InkWell(
onTap: () {
Navigator.pop(context);
if (widget.onChangeMethod != null) {
widget.onChangeMethod!();
}
},
child: Text(
'Check out other methods',
style: TextStyle(
color: AppColor.primary10,
fontSize: 16.toScreenWidth,
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
decorationColor: AppColor.primary10,
),
),
),
],
),
],
),
),
);
}
}

@ -29,6 +29,9 @@ class SignUpProvider extends ChangeNotifier {
String? _errorMessage;
String? get errorMessage => _errorMessage;
String? _successMessage;
String? get successMessage => _successMessage;
/// Get headers without authentication token for signup APIs
Map<String, String> get _unauthenticatedHeaders => {
'Content-Type': 'application/json',
@ -79,6 +82,7 @@ class SignUpProvider extends ChangeNotifier {
loading = true;
_errorMessage = null;
_successMessage = null;
notifyListeners();
try {
@ -89,6 +93,8 @@ class SignUpProvider extends ChangeNotifier {
);
if (response.statusCode >= 200 && response.statusCode < 300) {
final data = json.decode(response.body);
_successMessage = data['message'] ?? 'Your signup request has been submitted successfully.';
loading = false;
notifyListeners();
return true;
@ -192,6 +198,7 @@ class SignUpProvider extends ChangeNotifier {
_sites = [];
_departments = [];
_errorMessage = null;
_successMessage = null;
notifyListeners();
}
}

@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:test_sa/controllers/validator/validator.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/signup/models/company_model.dart';
import 'package:test_sa/modules/signup/models/group_model.dart';
import 'package:test_sa/modules/signup/models/signup_request_model.dart';
@ -203,10 +204,10 @@ class _SignUpScreenState extends State<SignUpScreen> {
_buildReadOnlyField("Mobile Number", _mobileController),
16.height,
_buildExtensionField(),
16.height,
_buildSiteDropdown(),
16.height,
_buildDepartmentDropdown(),
// 16.height,
// _buildSiteDropdown(),
// 16.height,
// _buildDepartmentDropdown(),
16.height,
_buildRoleField(),
],
@ -343,9 +344,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
context: context,
title: "Site",
initialValue: _selectedSite,
showAsFullScreenDialog: false,
showAsBottomSheet: true,
requestById: _selectedAssetGroup?.id,
showAsFullScreenDialog: true,
requestById: _selectedAssetGroup?.id,
onSelect: (value) {
setState(() {
_selectedSite = value;
@ -364,8 +364,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
context: context,
title: "Department",
initialValue: _selectedDepartment,
showAsFullScreenDialog: false,
showAsBottomSheet: true,
showAsFullScreenDialog: true,
// showAsBottomSheet: true,
requestById: _selectedAssetGroup?.id,
onSelect: (value) {
setState(() {
@ -406,8 +406,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
if (employee != null) {
// Check if the error message indicates duplicate account
if (_signUpProvider.errorMessage != null &&
_signUpProvider.errorMessage!.toLowerCase().contains('already')) {
if (_signUpProvider.errorMessage != null && _signUpProvider.errorMessage!.toLowerCase().contains('already')) {
// Show duplicate account popup
_showDuplicateAccountDialog();
setState(() {
@ -429,8 +428,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
// Check if it's a duplicate account error
if (_signUpProvider.errorMessage != null &&
(_signUpProvider.errorMessage!.toLowerCase().contains('already') ||
_signUpProvider.errorMessage!.toLowerCase().contains('exists') ||
_signUpProvider.errorMessage!.toLowerCase().contains('account'))) {
_signUpProvider.errorMessage!.toLowerCase().contains('exists') ||
_signUpProvider.errorMessage!.toLowerCase().contains('account'))) {
_showDuplicateAccountDialog();
setState(() {
_employeeFetched = false;
@ -509,16 +508,16 @@ class _SignUpScreenState extends State<SignUpScreen> {
}
// Site is mandatory for HMG
if (_selectedSite == null) {
Fluttertoast.showToast(msg: "Site is required");
return;
}
// Department is mandatory for HMG
if (_selectedDepartment == null) {
Fluttertoast.showToast(msg: "Department is required");
return;
}
// if (_selectedSite == null) {
// Fluttertoast.showToast(msg: "Site is required");
// return;
// }
//
// // Department is mandatory for HMG
// if (_selectedDepartment == null) {
// Fluttertoast.showToast(msg: "Department is required");
// return;
// }
} else {
// Non-HMG specific validations
@ -639,22 +638,60 @@ class _SignUpScreenState extends State<SignUpScreen> {
}
void _showSuccessDialog() {
final successMessage = _signUpProvider.successMessage ?? 'Your signup request has been submitted successfully';
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColor.background(context),
title: "Success".bodyText(context),
content: "Your signup request has been submitted successfully. It will be reviewed by the Human Resources Admin."
.bodyText(context),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context); // Close dialog
Navigator.pop(context); // Go back to login
},
child: "OK".bodyText(context).custom(color: AppColor.primary10),
builder: (context) => Dialog(
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(),
insetPadding: const EdgeInsets.only(left: 21, right: 21),
child: Padding(
padding: const EdgeInsets.only(left: 20, right: 20, top: 18, bottom: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
"Success",
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.black87,
height: 35 / 24,
letterSpacing: -0.96,
),
).paddingOnly(top: 16),
),
IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close),
color: Colors.black87,
constraints: const BoxConstraints(),
onPressed: () {
Navigator.pop(context); // Close dialog
Navigator.pop(context); // Go back to login
},
)
],
),
successMessage.bodyText(context),
28.height,
AppFilledButton(
label: "OK",
onPressed: () {
Navigator.pop(context); // Close dialog
Navigator.pop(context); // Go back to login
},
textColor: Colors.white,
),
],
),
],
),
),
);
}

@ -16,6 +16,7 @@ import 'package:test_sa/models/new_models/general_response_model.dart';
import 'package:test_sa/modules/cx_module/chat/chat_page.dart';
import 'package:test_sa/modules/cx_module/survey/survey_page.dart';
import 'package:test_sa/modules/signup/screens/signup_screen.dart';
import 'package:test_sa/modules/applicationDemo/screens/demo_request_form_screen.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/forget_password_module/forget_passwod_verify_otp.dart';
import 'package:test_sa/new_views/pages/land_page/land_page.dart';
@ -236,8 +237,15 @@ class _LoginPageState extends State<LoginPage> {
"Sign Up".bodyText(context).custom(color: AppColor.primary10, fontWeight: FontWeight.w600).onPress(() {
Navigator.pushNamed(context, SignUpScreen.routeName);
}),
" or ".bodyText(context).custom(color: context.isDark ? AppColor.primary50 : AppColor.neutral50),
"Request a Demo".bodyText(context).custom(color: AppColor.primary10, fontWeight: FontWeight.w600).onPress(() {
Navigator.pushNamed(context, DemoRequestFormScreen.routeName);
}),
],
),
// Demo Request Button
24.height,
],
),
),

Loading…
Cancel
Save