From d9eb1ae835a0f48b0244bf976cfc51697c7fee1a Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:16:09 +0300 Subject: [PATCH] site implementation inprogress --- lib/main.dart | 8 + lib/modules/signup/screens/signup_screen.dart | 237 +++++++++++---- .../models/dropdown_models.dart | 16 + .../models/mock_user_model.dart | 20 ++ .../models/request_type_model.dart | 9 + .../models/site_request_model.dart | 106 +++++++ .../providers/site_request_provider.dart | 193 ++++++++++++ .../screens/site_request_form_screen.dart | 278 ++++++++++++++++++ .../screens/site_request_history_screen.dart | 199 +++++++++++++ lib/new_views/common_widgets/app_drawer.dart | 3 + lib/providers/loading_list_notifier.dart | 1 + 11 files changed, 1015 insertions(+), 55 deletions(-) create mode 100644 lib/modules/site_request_module/models/dropdown_models.dart create mode 100644 lib/modules/site_request_module/models/mock_user_model.dart create mode 100644 lib/modules/site_request_module/models/request_type_model.dart create mode 100644 lib/modules/site_request_module/models/site_request_model.dart create mode 100644 lib/modules/site_request_module/providers/site_request_provider.dart create mode 100644 lib/modules/site_request_module/screens/site_request_form_screen.dart create mode 100644 lib/modules/site_request_module/screens/site_request_history_screen.dart diff --git a/lib/main.dart b/lib/main.dart index 9dadbd42..d1501ed9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -61,6 +61,9 @@ import 'package:test_sa/modules/signup/providers/signup_provider.dart'; import 'package:test_sa/modules/signup/providers/signup_site_provider.dart'; import 'package:test_sa/modules/signup/providers/signup_department_provider.dart'; 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/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'; @@ -350,6 +353,9 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => SignupSiteProvider(), lazy: true), ChangeNotifierProvider(create: (_) => SignupDepartmentProvider(), lazy: true), + // SITE REQUEST MODULE PROVIDERS (1) + ChangeNotifierProvider(create: (_) => SiteRequestProvider(), lazy: true), + // EMPLOYEE/USER PROVIDERS (2) ChangeNotifierProvider(create: (_) => NurseProvider(), lazy: true), ChangeNotifierProvider(create: (_) => UserSearchProvider(), lazy: true), @@ -426,6 +432,8 @@ class MyApp extends StatelessWidget { UpdateEquipmentInternalAuditPage.id: (_) => UpdateEquipmentInternalAuditPage(), UpdateSystemInternalAuditPage.id: (_) => UpdateSystemInternalAuditPage(), SignUpScreen.routeName: (_) => const SignUpScreen(), + SiteRequestFormScreen.routeName: (_) => const SiteRequestFormScreen(), + SiteRequestHistoryScreen.routeName: (_) => const SiteRequestHistoryScreen(), CreateLoanRequestPage.id: (_) => CreateLoanRequestPage(), CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(), CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(), diff --git a/lib/modules/signup/screens/signup_screen.dart b/lib/modules/signup/screens/signup_screen.dart index d1feba31..7c910d25 100644 --- a/lib/modules/signup/screens/signup_screen.dart +++ b/lib/modules/signup/screens/signup_screen.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; @@ -90,25 +88,46 @@ class _SignUpScreenState extends State { return Scaffold( backgroundColor: AppColor.scaffoldBackground(context), appBar: const DefaultAppBar(title: "Sign Up"), - body: Form( - key: _formKey, - child: SingleChildScrollView( - padding: EdgeInsets.all(16.toScreenWidth), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildAssetGroupDropdown(), - 16.height, - if (_selectedAssetGroup != null) ...[ - _buildCompanyDropdown(), - 16.height, - ], - if (_selectedCompany != null) ..._buildDynamicFields(), - 24.height, - if (_selectedCompany != null) _buildSubmitButton(), - ], + body: Column( + children: [ + Expanded( + child: Form( + key: _formKey, + child: SingleChildScrollView( + padding: EdgeInsets.all(16.toScreenWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAssetGroupDropdown(), + 16.height, + if (_selectedAssetGroup != null) ...[ + _buildCompanyDropdown(), + 16.height, + ], + if (_selectedCompany != null) ..._buildDynamicFields(), + ], + ), + ), + ), ), - ), + if (_selectedCompany != null) + Container( + padding: EdgeInsets.all(16.toScreenWidth), + decoration: BoxDecoration( + color: AppColor.background(context), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: _buildSubmitButton(), + ), + ), + ], ), ); } @@ -184,18 +203,17 @@ class _SignUpScreenState extends State { _buildReadOnlyField("Mobile Number", _mobileController), 16.height, _buildExtensionField(), - // 16.height, - // _buildSiteDropdown(), - // 16.height, - // _buildDepartmentDropdown(), + 16.height, + _buildSiteDropdown(), + 16.height, + _buildDepartmentDropdown(), 16.height, _buildRoleField(), ], ]; } else { return [ - // _buildReadOnlyEmployeeIdField(), - // 16.height, + // Employee ID is auto-generated for Non-HMG, so not shown during signup _buildFullNameField(), 16.height, _buildEmailField(), @@ -256,15 +274,6 @@ class _SignUpScreenState extends State { ); } - Widget _buildReadOnlyEmployeeIdField() { - return AppTextFormField( - initialValue: "Will be generated after admin approval", - labelText: "Employee ID", - enable: false, - backgroundColor: AppColor.disableFieldBackground(context), - ); - } - Widget _buildReadOnlyField(String label, TextEditingController controller) { return AppTextFormField( controller: controller, @@ -396,6 +405,19 @@ class _SignUpScreenState extends State { final employee = await _signUpProvider.getEmployeeFromERP(employeeId); if (employee != null) { + // Check if the error message indicates duplicate account + if (_signUpProvider.errorMessage != null && + _signUpProvider.errorMessage!.toLowerCase().contains('already')) { + // Show duplicate account popup + _showDuplicateAccountDialog(); + setState(() { + _employeeFetched = false; + _employeeERPData = null; + _employeeIdController.clear(); + }); + return; + } + setState(() { _employeeFetched = true; _employeeERPData = employee; // Store the ERP data for later use @@ -404,17 +426,30 @@ class _SignUpScreenState extends State { _mobileController.text = employee.employeeMobileNumber ?? ''; }); } else { - setState(() { - _employeeFetched = false; - _employeeERPData = null; - }); - Fluttertoast.showToast( - msg: _signUpProvider.errorMessage ?? "Failed to fetch employee details", - ); + // 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'))) { + _showDuplicateAccountDialog(); + setState(() { + _employeeFetched = false; + _employeeERPData = null; + _employeeIdController.clear(); + }); + } else { + setState(() { + _employeeFetched = false; + _employeeERPData = null; + }); + Fluttertoast.showToast( + msg: _signUpProvider.errorMessage ?? "Failed to fetch employee details", + ); + } } } - void _showAlreadyExistsDialog() { + void _showDuplicateAccountDialog() { showDialog( context: context, builder: (context) => AlertDialog( @@ -423,7 +458,9 @@ class _SignUpScreenState extends State { content: "Already you have an account on Atoms".bodyText(context), actions: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () { + Navigator.pop(context); + }, child: "OK".bodyText(context).custom(color: AppColor.primary10), ), ], @@ -432,6 +469,7 @@ class _SignUpScreenState extends State { } void _clearForm() { + _employeeIdController.clear(); _fullNameController.clear(); _emailController.clear(); _mobileController.clear(); @@ -439,24 +477,113 @@ class _SignUpScreenState extends State { _roleController.clear(); _selectedSite = null; _selectedDepartment = null; + _employeeFetched = false; + _employeeERPData = null; } Future _submitForm() async { - if (!_formKey.currentState!.validate()) return; + // Validate form fields + if (!_formKey.currentState!.validate()) { + Fluttertoast.showToast(msg: "Please fill all required fields"); + return; + } + + // HMG Group specific validations + if (_isHMGGroup) { + // Employee ID is mandatory for HMG + if (_employeeIdController.text.isEmpty) { + Fluttertoast.showToast(msg: "Employee ID is required"); + return; + } + + // Employee must be fetched from ERP + if (!_employeeFetched) { + Fluttertoast.showToast(msg: "Please search and fetch employee details from ERP"); + return; + } + + // Validate Employee ID is numeric + if (!Validator.isNumeric(_employeeIdController.text)) { + Fluttertoast.showToast(msg: "Employee ID must be numeric"); + return; + } + + // 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; + } + } else { + // Non-HMG specific validations + + // Full Name is mandatory + if (_fullNameController.text.isEmpty) { + Fluttertoast.showToast(msg: "Full Name is required"); + return; + } + + // Email is mandatory and must be valid format + if (_emailController.text.isEmpty) { + Fluttertoast.showToast(msg: "Email Address is required"); + return; + } + if (!Validator.isEmail(_emailController.text)) { + Fluttertoast.showToast(msg: "Please enter a valid email address"); + return; + } + + // Mobile Number is mandatory and must be numeric + if (_mobileController.text.isEmpty) { + Fluttertoast.showToast(msg: "Mobile Number is required"); + return; + } + if (!Validator.isPhoneNumber(_mobileController.text)) { + Fluttertoast.showToast(msg: "Please enter a valid mobile number"); + return; + } + + // Site is mandatory for Non-HMG + if (_selectedSite == null) { + Fluttertoast.showToast(msg: "Site is required"); + return; + } + + // Department is mandatory for Non-HMG + if (_selectedDepartment == null) { + Fluttertoast.showToast(msg: "Department is required"); + return; + } + } + + // Common validations for both HMG and Non-HMG + + // Extension Number validation (optional but must be numeric if provided) + if (_extensionController.text.isNotEmpty && !Validator.isNumeric(_extensionController.text)) { + Fluttertoast.showToast(msg: "Extension Number must be numeric"); + return; + } - if (_isHMGGroup && !_employeeFetched) { - Fluttertoast.showToast(msg: "Please enter a valid Employee ID"); + // Role is mandatory + if (_roleController.text.isEmpty) { + Fluttertoast.showToast(msg: "Role is required"); return; } - log('hmg ${_isHMGGroup}'); - if (!_isHMGGroup &&_selectedSite == null) { - Fluttertoast.showToast(msg: "Please select a site"); + // Validate Asset Group is selected + if (_selectedAssetGroup == null) { + Fluttertoast.showToast(msg: "Asset Group is required"); return; } - if (!_isHMGGroup&&_selectedDepartment == null) { - Fluttertoast.showToast(msg: "Please select a department"); + // Validate Company is selected + if (_selectedCompany == null) { + Fluttertoast.showToast(msg: "Company is required"); return; } @@ -481,10 +608,10 @@ class _SignUpScreenState extends State { emailAddress: _emailController.text, mobileNumber: _mobileController.text, extensionNumber: _extensionController.text.isEmpty ? null : _extensionController.text, - // For HMG: use ERP site/department, For non-HMG: use selected IDs - siteId: _isHMGGroup ? null : _selectedSite?.id, + // For HMG: send both selected IDs and ERP data + siteId: _selectedSite?.id, erpSite: _isHMGGroup ? _employeeERPData?.branchCode : null, - departmentId: _isHMGGroup ? null : _selectedDepartment?.id, + departmentId: _selectedDepartment?.id, erpDepartment: _isHMGGroup ? _employeeERPData?.branchName : null, role: _roleController.text, isFromERP: _isHMGGroup, diff --git a/lib/modules/site_request_module/models/dropdown_models.dart b/lib/modules/site_request_module/models/dropdown_models.dart new file mode 100644 index 00000000..e193f4aa --- /dev/null +++ b/lib/modules/site_request_module/models/dropdown_models.dart @@ -0,0 +1,16 @@ +import 'package:test_sa/models/base.dart'; + +class SiteRequestSiteModel extends Base { + SiteRequestSiteModel({ + required String id, + required String name, + }) : super(identifier: id, name: name); +} + +class SiteRequestDepartmentModel extends Base { + SiteRequestDepartmentModel({ + required String id, + required String name, + }) : super(identifier: id, name: name); +} + diff --git a/lib/modules/site_request_module/models/mock_user_model.dart b/lib/modules/site_request_module/models/mock_user_model.dart new file mode 100644 index 00000000..801cc483 --- /dev/null +++ b/lib/modules/site_request_module/models/mock_user_model.dart @@ -0,0 +1,20 @@ +class MockUserModel { + final String employeeId; + final String fullName; + final String emailAddress; + final String mobileNumber; + final String extensionNumber; + final String role; + final List assignedSites; + + MockUserModel({ + required this.employeeId, + required this.fullName, + required this.emailAddress, + required this.mobileNumber, + required this.extensionNumber, + required this.role, + required this.assignedSites, + }); +} + diff --git a/lib/modules/site_request_module/models/request_type_model.dart b/lib/modules/site_request_module/models/request_type_model.dart new file mode 100644 index 00000000..9c1513dd --- /dev/null +++ b/lib/modules/site_request_module/models/request_type_model.dart @@ -0,0 +1,9 @@ +import 'package:test_sa/models/base.dart'; + +class RequestTypeModel extends Base { + RequestTypeModel({ + required String id, + required String name, + }) : super(identifier: id, name: name); +} + diff --git a/lib/modules/site_request_module/models/site_request_model.dart b/lib/modules/site_request_module/models/site_request_model.dart new file mode 100644 index 00000000..779b49e8 --- /dev/null +++ b/lib/modules/site_request_module/models/site_request_model.dart @@ -0,0 +1,106 @@ +class SiteRequestModel { + final String? id; + final String requestType; // Add Site, Change Site, Remove Site + final String employeeId; + final String fullName; + final String emailAddress; + final String mobileNumber; + final String? extensionNumber; + final List siteIds; + final List siteNames; + final List? departmentIds; + final List? departmentNames; + final String role; + final String status; // Pending, Approved, Rejected + final DateTime requestDate; + + SiteRequestModel({ + this.id, + required this.requestType, + required this.employeeId, + required this.fullName, + required this.emailAddress, + required this.mobileNumber, + this.extensionNumber, + required this.siteIds, + required this.siteNames, + this.departmentIds, + this.departmentNames, + required this.role, + required this.status, + required this.requestDate, + }); + + Map toJson() { + return { + 'id': id, + 'requestType': requestType, + 'employeeId': employeeId, + 'fullName': fullName, + 'emailAddress': emailAddress, + 'mobileNumber': mobileNumber, + 'extensionNumber': extensionNumber, + 'siteIds': siteIds, + 'siteNames': siteNames, + 'departmentIds': departmentIds, + 'departmentNames': departmentNames, + 'role': role, + 'status': status, + 'requestDate': requestDate.toIso8601String(), + }; + } + + factory SiteRequestModel.fromJson(Map json) { + return SiteRequestModel( + id: json['id'], + requestType: json['requestType'] ?? '', + employeeId: json['employeeId'] ?? '', + fullName: json['fullName'] ?? '', + emailAddress: json['emailAddress'] ?? '', + mobileNumber: json['mobileNumber'] ?? '', + extensionNumber: json['extensionNumber'], + siteIds: List.from(json['siteIds'] ?? []), + siteNames: List.from(json['siteNames'] ?? []), + departmentIds: json['departmentIds'] != null ? List.from(json['departmentIds']) : null, + departmentNames: json['departmentNames'] != null ? List.from(json['departmentNames']) : null, + role: json['role'] ?? '', + status: json['status'] ?? 'Pending', + requestDate: json['requestDate'] != null ? DateTime.parse(json['requestDate']) : DateTime.now(), + ); + } + + SiteRequestModel copyWith({ + String? id, + String? requestType, + String? employeeId, + String? fullName, + String? emailAddress, + String? mobileNumber, + String? extensionNumber, + List? siteIds, + List? siteNames, + List? departmentIds, + List? departmentNames, + String? role, + String? status, + DateTime? requestDate, + }) { + return SiteRequestModel( + id: id ?? this.id, + requestType: requestType ?? this.requestType, + employeeId: employeeId ?? this.employeeId, + fullName: fullName ?? this.fullName, + emailAddress: emailAddress ?? this.emailAddress, + mobileNumber: mobileNumber ?? this.mobileNumber, + extensionNumber: extensionNumber ?? this.extensionNumber, + siteIds: siteIds ?? this.siteIds, + siteNames: siteNames ?? this.siteNames, + departmentIds: departmentIds ?? this.departmentIds, + departmentNames: departmentNames ?? this.departmentNames, + role: role ?? this.role, + status: status ?? this.status, + requestDate: requestDate ?? this.requestDate, + ); + } +} + diff --git a/lib/modules/site_request_module/providers/site_request_provider.dart b/lib/modules/site_request_module/providers/site_request_provider.dart new file mode 100644 index 00000000..87329237 --- /dev/null +++ b/lib/modules/site_request_module/providers/site_request_provider.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:test_sa/modules/site_request_module/models/dropdown_models.dart'; +import 'package:test_sa/modules/site_request_module/models/mock_user_model.dart'; +import 'package:test_sa/modules/site_request_module/models/request_type_model.dart'; +import 'package:test_sa/modules/site_request_module/models/site_request_model.dart'; + +class SiteRequestProvider extends ChangeNotifier { + bool _loading = false; + bool get loading => _loading; + + set loading(bool value) { + _loading = value; + notifyListeners(); + } + + String? _errorMessage; + String? get errorMessage => _errorMessage; + + // Mock user data + late MockUserModel _currentUser; + MockUserModel get currentUser => _currentUser; + + // Request types as Base models + final List _requestTypes = [ + RequestTypeModel(id: 'add_site', name: 'Add Site'), + RequestTypeModel(id: 'change_site', name: 'Change Site'), + RequestTypeModel(id: 'remove_site', name: 'Remove Site'), + ]; + + List get requestTypes => _requestTypes; + + // Mock sites as Base models + final List _allSites = [ + SiteRequestSiteModel(id: 'site_1', name: 'Site A'), + SiteRequestSiteModel(id: 'site_2', name: 'Site B'), + SiteRequestSiteModel(id: 'site_3', name: 'Site C'), + SiteRequestSiteModel(id: 'site_4', name: 'Site D'), + SiteRequestSiteModel(id: 'site_5', name: 'Site E'), + ]; + + List get allSites => _allSites; + + // Mock departments as Base models + final List _allDepartments = [ + SiteRequestDepartmentModel(id: 'dept_1', name: 'HR'), + SiteRequestDepartmentModel(id: 'dept_2', name: 'IT'), + SiteRequestDepartmentModel(id: 'dept_3', name: 'Finance'), + SiteRequestDepartmentModel(id: 'dept_4', name: 'Operations'), + SiteRequestDepartmentModel(id: 'dept_5', name: 'Administration'), + ]; + + List get allDepartments => _allDepartments; + + // History of submitted requests + final List _requestHistory = []; + List get requestHistory => _requestHistory; + + SiteRequestProvider() { + _initializeMockUser(); + } + + void _initializeMockUser() { + _currentUser = MockUserModel( + employeeId: 'EMP001', + fullName: 'John Smith', + emailAddress: 'john.smith@test.com', + mobileNumber: '+966500000000', + extensionNumber: '1234', + role: 'Engineer', + assignedSites: ['Site A', 'Site B'], + ); + } + + // Get available sites based on request type + List getAvailableSites(String requestType) { + if (requestType == 'Remove Site') { + // Only return currently assigned sites + return _allSites.where((site) { + return _currentUser.assignedSites.contains(site.name ?? ''); + }).toList(); + } + // For Add Site and Change Site, return all sites + return _allSites; + } + + // Check if user has assigned sites for removal + bool hasAssignedSites() { + return _currentUser.assignedSites.isNotEmpty; + } + + // Submit site request (local only, no API) + Future submitSiteRequest(SiteRequestModel request) async { + loading = true; + _errorMessage = null; + notifyListeners(); + + // Simulate network delay + await Future.delayed(const Duration(seconds: 1)); + + try { + // Add request to history with a generated ID + final requestWithId = request.copyWith( + id: 'REQ_${DateTime.now().millisecondsSinceEpoch}', + status: 'Pending', + requestDate: DateTime.now(), + ); + + _requestHistory.insert(0, requestWithId); + + loading = false; + notifyListeners(); + return true; + } catch (error) { + _errorMessage = 'Failed to submit request'; + loading = false; + notifyListeners(); + return false; + } + } + + // Get request by ID + SiteRequestModel? getRequestById(String id) { + try { + return _requestHistory.firstWhere((request) => request.id == id); + } catch (e) { + return null; + } + } + + // Reset provider state + void reset() { + _loading = false; + _errorMessage = null; + _requestHistory.clear(); + _initializeMockUser(); + notifyListeners(); + } + + // Add mock history data for testing + void addMockHistoryData() { + final mockRequests = [ + SiteRequestModel( + id: 'REQ_001', + requestType: 'Add Site', + employeeId: _currentUser.employeeId, + fullName: _currentUser.fullName, + emailAddress: _currentUser.emailAddress, + mobileNumber: _currentUser.mobileNumber, + extensionNumber: _currentUser.extensionNumber, + siteIds: ['site_3', 'site_4'], + siteNames: ['Site C', 'Site D'], + departmentIds: ['dept_2'], + departmentNames: ['IT'], + role: _currentUser.role, + status: 'Approved', + requestDate: DateTime.now().subtract(const Duration(days: 5)), + ), + SiteRequestModel( + id: 'REQ_002', + requestType: 'Change Site', + employeeId: _currentUser.employeeId, + fullName: _currentUser.fullName, + emailAddress: _currentUser.emailAddress, + mobileNumber: _currentUser.mobileNumber, + extensionNumber: _currentUser.extensionNumber, + siteIds: ['site_1'], + siteNames: ['Site A'], + departmentIds: ['dept_1', 'dept_3'], + departmentNames: ['HR', 'Finance'], + role: _currentUser.role, + status: 'Pending', + requestDate: DateTime.now().subtract(const Duration(days: 2)), + ), + SiteRequestModel( + id: 'REQ_003', + requestType: 'Remove Site', + employeeId: _currentUser.employeeId, + fullName: _currentUser.fullName, + emailAddress: _currentUser.emailAddress, + mobileNumber: _currentUser.mobileNumber, + extensionNumber: _currentUser.extensionNumber, + siteIds: ['site_2'], + siteNames: ['Site B'], + role: _currentUser.role, + status: 'Rejected', + requestDate: DateTime.now().subtract(const Duration(days: 10)), + ), + ]; + + _requestHistory.addAll(mockRequests); + notifyListeners(); + } +} diff --git a/lib/modules/site_request_module/screens/site_request_form_screen.dart b/lib/modules/site_request_module/screens/site_request_form_screen.dart new file mode 100644 index 00000000..269afafa --- /dev/null +++ b/lib/modules/site_request_module/screens/site_request_form_screen.dart @@ -0,0 +1,278 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/modules/site_request_module/models/dropdown_models.dart'; +import 'package:test_sa/modules/site_request_module/models/request_type_model.dart'; +import 'package:test_sa/modules/site_request_module/models/site_request_model.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_history_screen.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_lazy_loading.dart'; +import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; +import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; +import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.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 SiteRequestFormScreen extends StatefulWidget { + static const String routeName = "/site_request_form"; + + const SiteRequestFormScreen({Key? key}) : super(key: key); + + @override + State createState() => _SiteRequestFormScreenState(); +} + +class _SiteRequestFormScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + + late SiteRequestProvider _provider; + + RequestTypeModel? _selectedRequestType; + List _selectedSites = []; + List _selectedDepartments = []; + + bool _isSubmitting = false; + + @override + Widget build(BuildContext context) { + _provider = Provider.of(context); + + return Scaffold( + backgroundColor: AppColor.scaffoldBackground(context), + appBar: const DefaultAppBar(title: "Site Request"), + body: Column( + children: [ + Expanded( + child: Form( + key: _formKey, + child: SingleChildScrollView( + padding: EdgeInsets.all(16.toScreenWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRequestTypeDropdown(), + 16.height, + _buildReadOnlyField("Full Name", _provider.currentUser.fullName), + 16.height, + _buildReadOnlyField("Email Address", _provider.currentUser.emailAddress), + 16.height, + _buildReadOnlyField("Mobile Number", _provider.currentUser.mobileNumber), + 16.height, + _buildReadOnlyField("Extension Number", _provider.currentUser.extensionNumber), + 16.height, + if (_selectedRequestType != null) ...[ + _buildSiteMultiSelect(), + 16.height, + _buildDepartmentMultiSelect(), + 16.height, + _buildReadOnlyField("Role", _provider.currentUser.role), + ], + ], + ), + ), + ), + ), + if (_selectedRequestType != null) + Container( + padding: EdgeInsets.all(16.toScreenWidth), + decoration: BoxDecoration( + color: AppColor.background(context), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: _buildSubmitButton(), + ), + ), + ], + ), + ); + } + + Widget _buildRequestTypeDropdown() { + return SingleItemDropDownMenu( + context: context, + title: "Type of Site Request", + initialValue: _selectedRequestType, + staticData: _provider.requestTypes, + showAsFullScreenDialog: false, + showAsBottomSheet: true, + onSelect: (value) { + setState(() { + _selectedRequestType = value; + _selectedSites.clear(); + _selectedDepartments.clear(); + }); + }, + validator: (value) { + if (value == null) return "Please select request type"; + return null; + }, + ); + } + + Widget _buildReadOnlyField(String label, String value) { + return AppTextFormField( + initialValue: value, + labelText: label, + enable: false, + backgroundColor: AppColor.disableFieldBackground(context), + ); + } + + Widget _buildSiteMultiSelect() { + final availableSites = _provider.getAvailableSites(_selectedRequestType!.name!); + + // Check if user has assigned sites for Remove Site request + if (_selectedRequestType!.name == 'Remove Site' && !_provider.hasAssignedSites()) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(16.toScreenWidth), + decoration: BoxDecoration( + color: AppColor.disableFieldBackground(context), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColor.border2Color(context), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Site *".bodyText(context).custom( + color: AppColor.floatingTextStyleColor(context), + fontWeight: FontWeight.w500, + fontSize: 12, + ), + 4.height, + "No assigned sites available for removal".bodyText(context).custom( + color: AppColor.labelTextStyleColor(context), + ), + ], + ), + ), + ], + ); + } + + return MultipleItemDropDownMenu( + context: context, + title: "Site", + initialValue: _selectedSites, + staticData: availableSites, + showAsFullScreenDialog: true, + showAsBottomSheet: false, + onSelect: (value) { + setState(() { + _selectedSites = value ?? []; + }); + }, + ); + } + + Widget _buildDepartmentMultiSelect() { + return MultipleItemDropDownMenu( + context: context, + title: "Department (Optional)", + initialValue: _selectedDepartments, + staticData: _provider.allDepartments, + showAsFullScreenDialog: true, + showAsBottomSheet: false, + onSelect: (value) { + setState(() { + _selectedDepartments = value ?? []; + }); + }, + ); + } + + Widget _buildSubmitButton() { + // Disable button if Remove Site is selected and no assigned sites + final isDisabled = _selectedRequestType?.name == 'Remove Site' && !_provider.hasAssignedSites(); + + return AppFilledButton( + label: "Submit", + maxWidth: true, + loading: _isSubmitting, + disableButton: _isSubmitting || isDisabled, + onPressed: isDisabled ? null : _submitForm, + ); + } + + Future _submitForm() async { + if (!_formKey.currentState!.validate()) { + Fluttertoast.showToast(msg: "Please fill all required fields"); + return; + } + + if (_selectedRequestType == null) { + Fluttertoast.showToast(msg: "Please select request type"); + return; + } + + if (_selectedSites.isEmpty) { + Fluttertoast.showToast(msg: "Please select at least one site"); + return; + } + + setState(() { + _isSubmitting = true; + }); + + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AppLazyLoading(), + ); + } + + final request = SiteRequestModel( + requestType: _selectedRequestType!.name ?? '', + employeeId: _provider.currentUser.employeeId, + fullName: _provider.currentUser.fullName, + emailAddress: _provider.currentUser.emailAddress, + mobileNumber: _provider.currentUser.mobileNumber, + extensionNumber: _provider.currentUser.extensionNumber, + siteIds: _selectedSites.map((s) => s.identifier ?? '').toList(), + siteNames: _selectedSites.map((s) => s.name ?? '').toList(), + departmentIds: _selectedDepartments.isNotEmpty ? _selectedDepartments.map((d) => d.identifier ?? '').toList() : null, + departmentNames: _selectedDepartments.isNotEmpty ? _selectedDepartments.map((d) => d.name ?? '').toList() : null, + role: _provider.currentUser.role, + status: 'Pending', + requestDate: DateTime.now(), + ); + + final success = await _provider.submitSiteRequest(request); + + if (mounted) { + Navigator.pop(context); // Close loading dialog + } + + setState(() { + _isSubmitting = false; + }); + + if (success) { + Fluttertoast.showToast(msg: "Site request submitted successfully"); + if (mounted) { + Navigator.pushReplacementNamed(context, SiteRequestHistoryScreen.routeName); + } + } else { + Fluttertoast.showToast( + msg: _provider.errorMessage ?? "Failed to submit site request", + ); + } + } +} diff --git a/lib/modules/site_request_module/screens/site_request_history_screen.dart b/lib/modules/site_request_module/screens/site_request_history_screen.dart new file mode 100644 index 00000000..11601f68 --- /dev/null +++ b/lib/modules/site_request_module/screens/site_request_history_screen.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/modules/site_request_module/models/site_request_model.dart'; +import 'package:test_sa/modules/site_request_module/providers/site_request_provider.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; + +class SiteRequestHistoryScreen extends StatefulWidget { + static const String routeName = "/site_request_history"; + + const SiteRequestHistoryScreen({Key? key}) : super(key: key); + + @override + State createState() => _SiteRequestHistoryScreenState(); +} + +class _SiteRequestHistoryScreenState extends State { + late SiteRequestProvider _provider; + + @override + void initState() { + super.initState(); + // Add mock history data for demonstration purposes + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_provider.requestHistory.isEmpty) { + _provider.addMockHistoryData(); + } + }); + } + + @override + Widget build(BuildContext context) { + _provider = Provider.of(context); + + return Scaffold( + backgroundColor: AppColor.scaffoldBackground(context), + appBar: const DefaultAppBar(title: "Site Request History"), + body: _provider.requestHistory.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: EdgeInsets.all(16.toScreenWidth), + itemCount: _provider.requestHistory.length, + itemBuilder: (context, index) { + final request = _provider.requestHistory[index]; + return _buildRequestCard(request); + }, + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox_outlined, + size: 80, + color: AppColor.icon2Color(context).withOpacity(0.5), + ), + 16.height, + "No site requests found".heading5(context).custom( + color: AppColor.labelTextStyleColor(context), + ), + 8.height, + "Your submitted requests will appear here".bodyText(context).custom( + color: AppColor.lightTextColor(context), + ), + ], + ), + ); + } + + Widget _buildRequestCard(SiteRequestModel request) { + return Card( + margin: EdgeInsets.only(bottom: 16.toScreenHeight), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + color: AppColor.background(context), + child: Padding( + padding: EdgeInsets.all(16.toScreenWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: request.requestType.heading6(context).custom( + color: AppColor.labelTextStyleColor(context), + fontWeight: FontWeight.bold, + ), + ), + _buildStatusBadge(request.status), + ], + ), + 12.height, + _buildInfoRow("Request ID", request.id ?? "N/A"), + 8.height, + _buildInfoRow("Employee ID", request.employeeId), + 8.height, + _buildInfoRow("Full Name", request.fullName), + 8.height, + _buildInfoRow("Email", request.emailAddress), + 8.height, + _buildInfoRow("Mobile", request.mobileNumber), + 8.height, + if (request.extensionNumber != null && request.extensionNumber!.isNotEmpty) + _buildInfoRow("Extension", request.extensionNumber!), + 8.height, + _buildInfoRow("Role", request.role), + 8.height, + _buildInfoRow( + "Site(s)", + request.siteNames.join(", "), + ), + if (request.departmentNames != null && request.departmentNames!.isNotEmpty) ...[ + 8.height, + _buildInfoRow( + "Department(s)", + request.departmentNames!.join(", "), + ), + ], + 8.height, + _buildInfoRow( + "Request Date", + DateFormat('dd MMM yyyy, hh:mm a').format(request.requestDate), + ), + ], + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120.toScreenWidth, + child: "$label:".bodyText(context).custom( + color: AppColor.lightTextColor(context), + fontSize: 13, + ), + ), + Expanded( + child: value.bodyText(context).custom( + color: AppColor.labelTextStyleColor(context), + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } + + Widget _buildStatusBadge(String status) { + Color backgroundColor; + Color textColor; + + switch (status.toLowerCase()) { + case 'approved': + backgroundColor = Colors.green.withOpacity(0.1); + textColor = Colors.green; + break; + case 'rejected': + backgroundColor = Colors.red.withOpacity(0.1); + textColor = Colors.red; + break; + case 'pending': + default: + backgroundColor = Colors.orange.withOpacity(0.1); + textColor = Colors.orange; + break; + } + + return Container( + padding: EdgeInsets.symmetric( + horizontal: 12.toScreenWidth, + vertical: 6.toScreenHeight, + ), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(20), + ), + child: status.bodyText(context).custom( + color: textColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ); + } +} + diff --git a/lib/new_views/common_widgets/app_drawer.dart b/lib/new_views/common_widgets/app_drawer.dart index 93cabde5..89c258c4 100644 --- a/lib/new_views/common_widgets/app_drawer.dart +++ b/lib/new_views/common_widgets/app_drawer.dart @@ -4,6 +4,7 @@ import 'package:test_sa/extensions/context_extension.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/site_request_module/screens/site_request_form_screen.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/pages/login_page.dart'; import 'package:test_sa/new_views/swipe_module/swipe_history_view.dart'; @@ -70,6 +71,8 @@ class AppDrawer extends StatelessWidget { // drawerItem("rate_us", context.translation.rateUs, context), // 18.height, drawerItem("setting", context.translation.settings, context).onPress(() => Navigator.of(context).pushNamed(SettingsPage.id)), + drawerItem("setting", "Site Request", context).onPress(() => Navigator.of(context).pushNamed(SiteRequestFormScreen.routeName)), + 18.height, 18.height, if (userProvider.user != null && userProvider.user?.employeeIsHMG == false) drawerItem("swipe", "Swipe History", context).onPress(() => Navigator.of(context).pushNamed(SwipeHistoryView.routeName)), diff --git a/lib/providers/loading_list_notifier.dart b/lib/providers/loading_list_notifier.dart index 134f2a29..87288e91 100644 --- a/lib/providers/loading_list_notifier.dart +++ b/lib/providers/loading_list_notifier.dart @@ -26,6 +26,7 @@ abstract class LoadingListNotifier extends ChangeNotifier { items = []; _loading = false; stateCode = null; + notifyListeners(); } Future getData({int? id});