diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index ae56cc78..f42ccf0a 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -60,6 +60,12 @@ class URLs { static get signUpGetSiteList => "$_baseUrl/Customer/GetSiteList"; static get signUpGetDepartmentAutoComplete => "$_baseUrl/Department/GetDepartmentAutoCompleteByAssetGroup"; + // Site Request APIs + static get getSiteRequestTypes => "$_baseUrl/Lookups/GetLookup?lookupEnum=4016"; // get site request types + static get submitSiteRequest => "$_baseUrl/SiteRequest/CreateSiteRequest"; // post submit site request + static get getSiteRequestHistory => "$_baseUrl/SiteRequest/GetSiteRequestHistory"; // get site request history + static get getDepartmentsBySites => "$_baseUrl/Customer/GetDepartmentsBySites"; // post get departments by sites + // static get login => "$_baseUrl/MobileAuth/LoginIntegration"; // mobile login static get register => "$_baseUrl/handle/create/user"; // post static get updateProfile => "$_baseUrl/update/user/profile"; // post diff --git a/lib/main.dart b/lib/main.dart index d1501ed9..09a10f5a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -82,6 +82,7 @@ import 'package:test_sa/new_views/swipe_module/swipe_success_view.dart'; import 'package:test_sa/new_views/swipe_module/swipe_view.dart'; import 'package:test_sa/providers/asset_transfer/asset_transfer_status_provider.dart'; import 'package:test_sa/providers/department_provider.dart'; +import 'package:test_sa/providers/department_by_sites_provider.dart'; import 'package:test_sa/providers/gas_request_providers/cylinder_size_provider.dart'; import 'package:test_sa/providers/gas_request_providers/cylinder_type_provider.dart'; import 'package:test_sa/providers/gas_request_providers/gas_status_provider.dart'; @@ -341,6 +342,7 @@ class MyApp extends StatelessWidget { // DEPARTMENT/LOCATION PROVIDERS (5) ChangeNotifierProvider(create: (_) => DepartmentProvider(), lazy: true), + ChangeNotifierProvider(create: (_) => DepartmentBySitesProvider(), lazy: true), ChangeNotifierProvider(create: (_) => SiteProvider(), lazy: true), ChangeNotifierProvider(create: (_) => MappedSiteProvider(), lazy: true), ChangeNotifierProvider(create: (_) => MedicalDepartmentProvider(), lazy: true), diff --git a/lib/modules/site_request_module/models/request_type_model.dart b/lib/modules/site_request_module/models/request_type_model.dart index 9c1513dd..74364eee 100644 --- a/lib/modules/site_request_module/models/request_type_model.dart +++ b/lib/modules/site_request_module/models/request_type_model.dart @@ -1,9 +1,11 @@ import 'package:test_sa/models/base.dart'; class RequestTypeModel extends Base { + final int? value; + RequestTypeModel({ required String id, required String name, + this.value, }) : super(identifier: id, name: name); } - diff --git a/lib/modules/site_request_module/providers/site_request_provider.dart b/lib/modules/site_request_module/providers/site_request_provider.dart index 87329237..dd2a880a 100644 --- a/lib/modules/site_request_module/providers/site_request_provider.dart +++ b/lib/modules/site_request_module/providers/site_request_provider.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:test_sa/controllers/api_routes/api_manager.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/controllers/providers/api/user_provider.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'; +import 'package:test_sa/models/lookup.dart'; +import 'dart:convert'; +import 'package:http/http.dart'; class SiteRequestProvider extends ChangeNotifier { bool _loading = false; @@ -16,67 +21,212 @@ class SiteRequestProvider extends ChangeNotifier { String? _errorMessage; String? get errorMessage => _errorMessage; - // Mock user data - late MockUserModel _currentUser; - MockUserModel get currentUser => _currentUser; + int? stateCode; - // 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'), - ]; + // Get current user from UserProvider + String getCurrentUserEmployeeId(UserProvider userProvider) { + return userProvider.user?.employeeId ?? ''; + } + + String getCurrentUserFullName(UserProvider userProvider) { + return userProvider.user?.username ?? ''; + } + + String getCurrentUserEmail(UserProvider userProvider) { + return userProvider.user?.email ?? ''; + } + + String getCurrentUserMobile(UserProvider userProvider) { + return userProvider.user?.phoneNumber ?? ''; + } + + String getCurrentUserExtension(UserProvider userProvider) { + return userProvider.user?.extensionNo ?? ''; + } + + String getCurrentUserRole(UserProvider userProvider) { + // Get the first role if available + if (userProvider.user?.userRoles != null && userProvider.user!.userRoles!.isNotEmpty) { + return userProvider.user!.userRoles!.first.name ?? ''; + } + return ''; + } + List getCurrentUserAssignedSites(UserProvider userProvider) { + // You can modify this based on how assigned sites are stored in your User model + // For now, returning empty list as it's not clear from the User model + // You may need to fetch this from another source or API + return []; + } + + // Request types from API + List _requestTypes = []; 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'), - ]; + // Fetch request types from API (Step 1) + Future fetchRequestTypes() async { + if (_loading) return -2; + + _loading = true; + _errorMessage = null; + notifyListeners(); + + late Response response; + try { + response = await ApiManager.instance.get( + URLs.getSiteRequestTypes, + enableToastMessage: false, + ); + } catch (error) { + _loading = false; + stateCode = -1; + _errorMessage = 'Failed to fetch request types'; + notifyListeners(); + return -1; + } + + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + try { + var responseData = json.decode(response.body); + List listJson = responseData['data'] ?? responseData; + _requestTypes = listJson + .map((item) => Lookup.fromJson(item)) + .map((lookup) => RequestTypeModel( + id: lookup.id?.toString() ?? lookup.value?.toString() ?? '', + name: lookup.name ?? '', + value: lookup.value, + )) + .toList(); + } catch (e) { + _errorMessage = 'Failed to parse request types: $e'; + _loading = false; + notifyListeners(); + return -1; + } + } else { + _errorMessage = 'Failed to fetch request types'; + } + + _loading = false; + notifyListeners(); + return response.statusCode; + } + // Mock sites as Base models + List _allSites = []; 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'), - ]; + // Fetch sites from API (Step 2) + Future fetchSites() async { + if (_loading) return -2; - List get allDepartments => _allDepartments; + _loading = true; + _errorMessage = null; + notifyListeners(); - // History of submitted requests - final List _requestHistory = []; - List get requestHistory => _requestHistory; + late Response response; + try { + response = await ApiManager.instance.get( + URLs.signUpGetSiteList, + enableToastMessage: false, + ); + } catch (error) { + _loading = false; + stateCode = -1; + _errorMessage = 'Failed to fetch sites'; + notifyListeners(); + return -1; + } - SiteRequestProvider() { - _initializeMockUser(); + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + try { + var responseData = json.decode(response.body); + List listJson = responseData is List ? responseData : responseData['data'] ?? []; + _allSites = listJson + .map((item) => SiteRequestSiteModel( + id: item['id']?.toString() ?? '', + name: item['name']?.toString() ?? '', + )) + .toList(); + } catch (e) { + _errorMessage = 'Failed to parse sites'; + _loading = false; + notifyListeners(); + return -1; + } + } else { + _errorMessage = 'Failed to fetch sites'; + } + + _loading = false; + notifyListeners(); + return response.statusCode; } - 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'], - ); + // Mock departments as Base models + List _allDepartments = []; + List get allDepartments => _allDepartments; + + // Fetch departments from API (Step 3) + Future fetchDepartments() async { + if (_loading) return -2; + + _loading = true; + _errorMessage = null; + notifyListeners(); + + late Response response; + try { + response = await ApiManager.instance.get( + URLs.signUpGetDepartmentAutoComplete, + enableToastMessage: false, + ); + } catch (error) { + _loading = false; + stateCode = -1; + _errorMessage = 'Failed to fetch departments'; + notifyListeners(); + return -1; + } + + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + try { + var responseData = json.decode(response.body); + List listJson = responseData is List ? responseData : responseData['data'] ?? []; + _allDepartments = listJson + .map((item) => SiteRequestDepartmentModel( + id: item['id']?.toString() ?? '', + name: item['name']?.toString() ?? '', + )) + .toList(); + } catch (e) { + _errorMessage = 'Failed to parse departments'; + _loading = false; + notifyListeners(); + return -1; + } + } else { + _errorMessage = 'Failed to fetch departments'; + } + + _loading = false; + notifyListeners(); + return response.statusCode; } + // History of submitted requests + final List _requestHistory = []; + List get requestHistory => _requestHistory; + // Get available sites based on request type - List getAvailableSites(String requestType) { + List getAvailableSites(String requestType, List assignedSites) { if (requestType == 'Remove Site') { // Only return currently assigned sites return _allSites.where((site) { - return _currentUser.assignedSites.contains(site.name ?? ''); + return assignedSites.contains(site.name ?? ''); }).toList(); } // For Add Site and Change Site, return all sites @@ -84,47 +234,140 @@ class SiteRequestProvider extends ChangeNotifier { } // Check if user has assigned sites for removal - bool hasAssignedSites() { - return _currentUser.assignedSites.isNotEmpty; + bool hasAssignedSites(List assignedSites) { + return assignedSites.isNotEmpty; } // Submit site request (local only, no API) - Future submitSiteRequest(SiteRequestModel request) async { + Future submitSiteRequest(SiteRequestModel request, UserProvider userProvider) async { loading = true; _errorMessage = null; notifyListeners(); - // Simulate network delay - await Future.delayed(const Duration(seconds: 1)); - + late Response response; try { - // Add request to history with a generated ID - final requestWithId = request.copyWith( - id: 'REQ_${DateTime.now().millisecondsSinceEpoch}', - status: 'Pending', - requestDate: DateTime.now(), - ); + // Prepare site IDs array with correct structure + final siteIdsArray = request.siteIds.asMap().entries.map((entry) { + return { + "id": 0, + "siteId": int.tryParse(request.siteIds[entry.key]) ?? 0, + "siteName": request.siteNames[entry.key], + "oldSiteId": 0, + "oldSitName": "" + }; + }).toList(); - _requestHistory.insert(0, requestWithId); + // Prepare department IDs array with correct structure + final departmentIdsArray = (request.departmentIds != null && request.departmentIds!.isNotEmpty) + ? request.departmentIds!.asMap().entries.map((entry) { + return { + "id": 0, + "departmentId": int.tryParse(request.departmentIds![entry.key]) ?? 0, + "departmentName": request.departmentNames![entry.key], + "oldDepartmentId": 0, + "oldDepartmentName": "" + }; + }).toList() + : []; + // Prepare the request body matching the API specification + final requestBody = { + "id": 0, + "userId": getCurrentUserEmployeeId(userProvider), + "requestTypeId": int.tryParse(request.requestType) ?? 0, + "siteIds": siteIdsArray, + "departmentIds": departmentIdsArray, + }; + + response = await ApiManager.instance.post( + URLs.submitSiteRequest, + body: requestBody, + ); + } catch (error) { + _errorMessage = 'Failed to submit request: $error'; loading = false; notifyListeners(); - return true; - } catch (error) { - _errorMessage = 'Failed to submit request'; + return false; + } + + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + try { + var responseData = json.decode(response.body); + // Add request to history with server response + final requestWithId = request.copyWith( + id: responseData['id']?.toString() ?? 'REQ_${DateTime.now().millisecondsSinceEpoch}', + status: responseData['status']?.toString() ?? 'Pending', + requestDate: DateTime.now(), + ); + + _requestHistory.insert(0, requestWithId); + loading = false; + notifyListeners(); + return true; + } catch (e) { + _errorMessage = 'Request submitted but failed to parse response'; + loading = false; + notifyListeners(); + return true; // Still return true as request was submitted + } + } else { + try { + var errorData = json.decode(response.body); + _errorMessage = errorData['message'] ?? 'Failed to submit request'; + } catch (e) { + _errorMessage = 'Failed to submit request'; + } loading = false; notifyListeners(); return false; } } - // Get request by ID - SiteRequestModel? getRequestById(String id) { + // Fetch request history from API (Step 5) + Future fetchRequestHistory() async { + if (_loading) return -2; + + _loading = true; + _errorMessage = null; + notifyListeners(); + + late Response response; try { - return _requestHistory.firstWhere((request) => request.id == id); - } catch (e) { - return null; + response = await ApiManager.instance.get( + URLs.getSiteRequestHistory, + enableToastMessage: false, + ); + } catch (error) { + _loading = false; + stateCode = -1; + _errorMessage = 'Failed to fetch request history'; + notifyListeners(); + return -1; } + + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + try { + var responseData = json.decode(response.body); + List listJson = responseData is List ? responseData : responseData['data'] ?? []; + _requestHistory.clear(); + _requestHistory.addAll( + listJson.map((item) => SiteRequestModel.fromJson(item)).toList() + ); + } catch (e) { + _errorMessage = 'Failed to parse request history'; + _loading = false; + notifyListeners(); + return -1; + } + } else { + _errorMessage = 'Failed to fetch request history'; + } + + _loading = false; + notifyListeners(); + return response.statusCode; } // Reset provider state @@ -132,56 +375,55 @@ class SiteRequestProvider extends ChangeNotifier { _loading = false; _errorMessage = null; _requestHistory.clear(); - _initializeMockUser(); notifyListeners(); } // Add mock history data for testing - void addMockHistoryData() { + void addMockHistoryData(UserProvider userProvider) { final mockRequests = [ SiteRequestModel( id: 'REQ_001', requestType: 'Add Site', - employeeId: _currentUser.employeeId, - fullName: _currentUser.fullName, - emailAddress: _currentUser.emailAddress, - mobileNumber: _currentUser.mobileNumber, - extensionNumber: _currentUser.extensionNumber, + employeeId: getCurrentUserEmployeeId(userProvider), + fullName: getCurrentUserFullName(userProvider), + emailAddress: getCurrentUserEmail(userProvider), + mobileNumber: getCurrentUserMobile(userProvider), + extensionNumber: getCurrentUserExtension(userProvider), siteIds: ['site_3', 'site_4'], siteNames: ['Site C', 'Site D'], departmentIds: ['dept_2'], departmentNames: ['IT'], - role: _currentUser.role, + role: getCurrentUserRole(userProvider), 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, + employeeId: getCurrentUserEmployeeId(userProvider), + fullName: getCurrentUserFullName(userProvider), + emailAddress: getCurrentUserEmail(userProvider), + mobileNumber: getCurrentUserMobile(userProvider), + extensionNumber: getCurrentUserExtension(userProvider), siteIds: ['site_1'], siteNames: ['Site A'], departmentIds: ['dept_1', 'dept_3'], departmentNames: ['HR', 'Finance'], - role: _currentUser.role, + role: getCurrentUserRole(userProvider), 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, + employeeId: getCurrentUserEmployeeId(userProvider), + fullName: getCurrentUserFullName(userProvider), + emailAddress: getCurrentUserEmail(userProvider), + mobileNumber: getCurrentUserMobile(userProvider), + extensionNumber: getCurrentUserExtension(userProvider), siteIds: ['site_2'], siteNames: ['Site B'], - role: _currentUser.role, + role: getCurrentUserRole(userProvider), status: 'Rejected', requestDate: DateTime.now().subtract(const Duration(days: 10)), ), 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 index 269afafa..638ea95a 100644 --- a/lib/modules/site_request_module/screens/site_request_form_screen.dart +++ b/lib/modules/site_request_module/screens/site_request_form_screen.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; +import 'package:test_sa/controllers/providers/api/user_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/extensions/context_extension.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/models/new_models/site.dart'; +import 'package:test_sa/models/new_models/department.dart'; +import 'package:test_sa/providers/gas_request_providers/site_provider.dart'; +import 'package:test_sa/providers/department_by_sites_provider.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'; @@ -16,6 +21,8 @@ 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'; +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 SiteRequestFormScreen extends StatefulWidget { static const String routeName = "/site_request_form"; @@ -30,16 +37,52 @@ class _SiteRequestFormScreenState extends State { final GlobalKey _formKey = GlobalKey(); late SiteRequestProvider _provider; + late UserProvider _userProvider; RequestTypeModel? _selectedRequestType; - List _selectedSites = []; - List _selectedDepartments = []; + List _selectedSites = []; + List _selectedDepartments = []; bool _isSubmitting = false; + bool _isLoadingData = true; + + @override + void initState() { + super.initState(); + _loadInitialData(); + } + + Future _loadInitialData() async { + // Load data after first frame to access context + WidgetsBinding.instance.addPostFrameCallback((_) async { + _provider = Provider.of(context, listen: false); + + // Fetch only request types (sites and departments are loaded by their providers) + await _provider.fetchRequestTypes(); + + if (mounted) { + setState(() { + _isLoadingData = false; + }); + } + }); + } @override Widget build(BuildContext context) { _provider = Provider.of(context); + _userProvider = Provider.of(context); + + // Show loading indicator while fetching initial data + if (_isLoadingData) { + return Scaffold( + backgroundColor: AppColor.scaffoldBackground(context), + appBar: const DefaultAppBar(title: "Site Request"), + body: const Center( + child: AppLazyLoading(), + ), + ); + } return Scaffold( backgroundColor: AppColor.scaffoldBackground(context), @@ -54,22 +97,14 @@ class _SiteRequestFormScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildRequestTypeDropdown(), + _buildUserInfoCard(), 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), + _buildRequestTypeDropdown(), 16.height, if (_selectedRequestType != null) ...[ _buildSiteMultiSelect(), 16.height, _buildDepartmentMultiSelect(), - 16.height, - _buildReadOnlyField("Role", _provider.currentUser.role), ], ], ), @@ -83,7 +118,7 @@ class _SiteRequestFormScreenState extends State { color: AppColor.background(context), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, -2), ), @@ -120,86 +155,103 @@ class _SiteRequestFormScreenState extends State { ); } - Widget _buildReadOnlyField(String label, String value) { - return AppTextFormField( - initialValue: value, - labelText: label, - enable: false, - backgroundColor: AppColor.disableFieldBackground(context), - ); + Widget _buildUserInfoCard() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InfoHeader16Widget("Requester Information"), + 8.height, + InfoTextWidget(label: "Full Name",value: _provider.getCurrentUserFullName(_userProvider),showEmptyValue: true,), + InfoTextWidget(label: "Email Address",value: _provider.getCurrentUserEmail(_userProvider),showEmptyValue: true,), + InfoTextWidget(label: "Mobile Number",value: _provider.getCurrentUserMobile(_userProvider),showEmptyValue: true,), + InfoTextWidget(label: "Extension Number", value: _provider.getCurrentUserExtension(_userProvider),showEmptyValue: true,), + InfoTextWidget(label: "Role",value: _provider.getCurrentUserRole(_userProvider),showEmptyValue: true,), + ], + ).toShadowContainer(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( + Widget _buildInfoRow(String label, String value, {bool isLast = false}) { + return Padding( + padding: EdgeInsets.only(bottom: isLast ? 0 : 12.toScreenHeight), + child: Row( 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, + SizedBox( + width: 130.toScreenWidth, + child: Text( + "$label:", + style: TextStyle( + fontSize: 14, + color: AppColor.labelTextStyleColor(context), + fontWeight: FontWeight.w500, ), ), - 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), - ), - ], + ), + Expanded( + child: Text( + value.isEmpty ? "-" : value, + style: TextStyle( + fontSize: 14, + color: AppColor.textStyleColor(context), + ), ), ), ], - ); - } + ), + ); + } - return MultipleItemDropDownMenu( + Widget _buildSiteMultiSelect() { + return MultipleItemDropDownMenu( context: context, - title: "Site", + title: context.translation.bySite, initialValue: _selectedSites, - staticData: availableSites, - showAsFullScreenDialog: true, - showAsBottomSheet: false, + showAsBottomSheet: true, + backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white, + showShadow: true, + height: 56.toScreenHeight, + showCancel: true, onSelect: (value) { - setState(() { - _selectedSites = value ?? []; - }); + if (value != null) { + setState(() { + _selectedSites = value; + _selectedDepartments.clear(); // Clear departments when sites change + + // Update department provider with new site IDs + final departmentProvider = Provider.of(context, listen: false); + final siteIds = value.map((site) => int.tryParse(site.identifier ?? '0') ?? 0).toList(); + departmentProvider.setSiteIds(siteIds); + departmentProvider.getData(); + }); + } }, ); } Widget _buildDepartmentMultiSelect() { - return MultipleItemDropDownMenu( + return MultipleItemDropDownMenu( context: context, - title: "Department (Optional)", + title: "Departments", initialValue: _selectedDepartments, - staticData: _provider.allDepartments, - showAsFullScreenDialog: true, - showAsBottomSheet: false, + showAsBottomSheet: true, + backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white, + showShadow: true, + height: 56.toScreenHeight, + showCancel: true, onSelect: (value) { - setState(() { - _selectedDepartments = value ?? []; - }); + if (value != null) { + setState(() { + _selectedDepartments = value; + }); + } }, ); } Widget _buildSubmitButton() { // Disable button if Remove Site is selected and no assigned sites - final isDisabled = _selectedRequestType?.name == 'Remove Site' && !_provider.hasAssignedSites(); + final assignedSites = _provider.getCurrentUserAssignedSites(_userProvider); + final isDisabled = _selectedRequestType?.name == 'Remove Site' && !_provider.hasAssignedSites(assignedSites); return AppFilledButton( label: "Submit", @@ -239,22 +291,22 @@ class _SiteRequestFormScreenState extends State { } 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, + requestType: _selectedRequestType!.value?.toString() ?? '', // Use value instead of id + employeeId: _provider.getCurrentUserEmployeeId(_userProvider), + fullName: _provider.getCurrentUserFullName(_userProvider), + emailAddress: _provider.getCurrentUserEmail(_userProvider), + mobileNumber: _provider.getCurrentUserMobile(_userProvider), + extensionNumber: _provider.getCurrentUserExtension(_userProvider), 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, + role: _provider.getCurrentUserRole(_userProvider), status: 'Pending', requestDate: DateTime.now(), ); - final success = await _provider.submitSiteRequest(request); + final success = await _provider.submitSiteRequest(request, _userProvider); if (mounted) { Navigator.pop(context); // Close loading dialog 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 index 11601f68..0aee6559 100644 --- a/lib/modules/site_request_module/screens/site_request_history_screen.dart +++ b/lib/modules/site_request_module/screens/site_request_history_screen.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:test_sa/controllers/providers/api/user_provider.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/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_form_screen.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; @@ -19,14 +22,27 @@ class SiteRequestHistoryScreen extends StatefulWidget { class _SiteRequestHistoryScreenState extends State { late SiteRequestProvider _provider; + late UserProvider _userProvider; + bool _isLoadingData = true; @override void initState() { super.initState(); - // Add mock history data for demonstration purposes - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_provider.requestHistory.isEmpty) { - _provider.addMockHistoryData(); + _loadHistoryData(); + } + + Future _loadHistoryData() async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + _provider = Provider.of(context, listen: false); + _userProvider = Provider.of(context, listen: false); + + // Fetch request history from API + await _provider.fetchRequestHistory(); + + if (mounted) { + setState(() { + _isLoadingData = false; + }); } }); } @@ -34,20 +50,53 @@ class _SiteRequestHistoryScreenState extends State { @override Widget build(BuildContext context) { _provider = Provider.of(context); + _userProvider = 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); - }, + appBar: DefaultAppBar( + title: "Site Request History", + actions: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: AppColor.background(context), + border: Border.all(color: AppColor.border2Color(context)), + borderRadius: BorderRadius.circular(16), ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add, size: 20, color: AppColor.textStyleColor(context)), + Text( + "New Request", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColor.textStyleColor(context), + ), + ), + 4.width, + ], + ), + ).onPress(() { + Navigator.pushNamed(context, SiteRequestFormScreen.routeName); + }), + ], + ), + body: _isLoadingData + ? const Center(child: CircularProgressIndicator()) + : _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); + }, + ), ); } @@ -59,7 +108,7 @@ class _SiteRequestHistoryScreenState extends State { Icon( Icons.inbox_outlined, size: 80, - color: AppColor.icon2Color(context).withOpacity(0.5), + color: AppColor.icon2Color(context).withValues(alpha: 0.5), ), 16.height, "No site requests found".heading5(context).custom( @@ -75,125 +124,254 @@ class _SiteRequestHistoryScreenState extends State { } Widget _buildRequestCard(SiteRequestModel request) { - return Card( + return Container( margin: EdgeInsets.only(bottom: 16.toScreenHeight), - elevation: 2, - shape: RoundedRectangleBorder( + decoration: BoxDecoration( + color: AppColor.background(context), borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], ), - color: AppColor.background(context), child: Padding( padding: EdgeInsets.all(16.toScreenWidth), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Status and Date Row Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: request.requestType.heading6(context).custom( - color: AppColor.labelTextStyleColor(context), - fontWeight: FontWeight.bold, + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _buildStatusLabel(request.status), + _buildRequestTypeLabel(request.requestType), + ], ), ), - _buildStatusBadge(request.status), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + DateFormat('dd MMM yyyy').format(request.requestDate), + style: TextStyle( + fontSize: 12, + color: AppColor.labelTextStyleColor(context), + ), + ), + Text( + DateFormat('hh:mm a').format(request.requestDate), + style: TextStyle( + fontSize: 10, + color: AppColor.lightTextColor(context), + ), + ), + ], + ), ], ), + 16.height, + + // Request ID + if (request.id != null) ...[ + _buildInfoRow("Request ID", request.id!), + 12.height, + ], + + Divider(color: AppColor.border2Color(context)), + 12.height, + + // Requester Information Section + "Requester Information".heading6(context).custom( + color: AppColor.textStyleColor(context), + fontWeight: FontWeight.w600, + ), 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("Employee ID", request.employeeId), _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(", "), + + 16.height, + Divider(color: AppColor.border2Color(context)), + 12.height, + + // Site Information Section + "Site Information".heading6(context).custom( + color: AppColor.textStyleColor(context), + fontWeight: FontWeight.w600, + ), + 12.height, + _buildInfoRow("Request Type", request.requestType), + 12.height, + + // Sites List + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sites:", + style: TextStyle( + fontSize: 14, + color: AppColor.labelTextStyleColor(context), + fontWeight: FontWeight.w500, + ), + ), + 8.width, + Expanded( + child: Wrap( + spacing: 6, + runSpacing: 6, + children: request.siteNames.map((site) => _buildChip(site)).toList(), + ), + ), + ], ), + + // Departments List (if available) if (request.departmentNames != null && request.departmentNames!.isNotEmpty) ...[ - 8.height, - _buildInfoRow( - "Department(s)", - request.departmentNames!.join(", "), + 12.height, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Departments:", + style: TextStyle( + fontSize: 14, + color: AppColor.labelTextStyleColor(context), + fontWeight: FontWeight.w500, + ), + ), + 8.width, + Expanded( + child: Wrap( + spacing: 6, + runSpacing: 6, + children: request.departmentNames!.map((dept) => _buildChip(dept)).toList(), + ), + ), + ], ), ], - 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) { + Widget _buildStatusLabel(String status) { Color backgroundColor; Color textColor; switch (status.toLowerCase()) { case 'approved': - backgroundColor = Colors.green.withOpacity(0.1); - textColor = Colors.green; + backgroundColor = AppColor.greenStatusColor; + textColor = AppColor.greenStatusTextColor; break; case 'rejected': - backgroundColor = Colors.red.withOpacity(0.1); - textColor = Colors.red; + backgroundColor = AppColor.redStatusColor; + textColor = AppColor.redStatusTextColor; break; case 'pending': default: - backgroundColor = Colors.orange.withOpacity(0.1); - textColor = Colors.orange; + backgroundColor = AppColor.yellowStatusColor; + textColor = AppColor.yellowStatusTextColor; break; } return Container( - padding: EdgeInsets.symmetric( - horizontal: 12.toScreenWidth, - vertical: 6.toScreenHeight, - ), + padding: EdgeInsets.symmetric(horizontal: 12.toScreenWidth, vertical: 6.toScreenHeight), decoration: BoxDecoration( color: backgroundColor, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(4), ), - child: status.bodyText(context).custom( - color: textColor, - fontSize: 12, - fontWeight: FontWeight.w600, + child: Text( + status, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ); + } + + Widget _buildRequestTypeLabel(String requestType) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.toScreenWidth, vertical: 6.toScreenHeight), + decoration: BoxDecoration( + color: AppColor.primary10.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: AppColor.primary10.withValues(alpha: 0.3)), + ), + child: Text( + requestType, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: AppColor.primary10, + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: EdgeInsets.only(bottom: 8.toScreenHeight), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120.toScreenWidth, + child: Text( + "$label:", + style: TextStyle( + fontSize: 14, + color: AppColor.labelTextStyleColor(context), + fontWeight: FontWeight.w500, + ), + ), + ), + Expanded( + child: Text( + value, + style: TextStyle( + fontSize: 14, + color: AppColor.textStyleColor(context), + ), + ), + ), + ], ), ); } -} + Widget _buildChip(String label) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10.toScreenWidth, vertical: 4.toScreenHeight), + decoration: BoxDecoration( + color: AppColor.primary10.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColor.primary10.withValues(alpha: 0.2)), + ), + child: Text( + label, + style: TextStyle( + fontSize: 12, + color: AppColor.primary10, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/new_views/common_widgets/app_drawer.dart b/lib/new_views/common_widgets/app_drawer.dart index 89c258c4..30c3a85d 100644 --- a/lib/new_views/common_widgets/app_drawer.dart +++ b/lib/new_views/common_widgets/app_drawer.dart @@ -4,7 +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/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/pages/login_page.dart'; import 'package:test_sa/new_views/swipe_module/swipe_history_view.dart'; @@ -71,8 +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, + drawerItem("setting", "Site Request", context).onPress(() => Navigator.of(context).pushNamed(SiteRequestHistoryScreen.routeName)), 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/department_by_sites_provider.dart b/lib/providers/department_by_sites_provider.dart new file mode 100644 index 00000000..d2f51a9a --- /dev/null +++ b/lib/providers/department_by_sites_provider.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:test_sa/models/new_models/department.dart'; + +import '../controllers/api_routes/api_manager.dart'; +import '../controllers/api_routes/urls.dart'; +import 'loading_list_notifier.dart'; + +class DepartmentBySitesProvider extends LoadingListNotifier { + List siteIds = []; + + void setSiteIds(List ids) { + siteIds = ids; + } + + @override + Future getData({int? id}) async { + if (loading ?? false) return -2; + if (siteIds.isEmpty) { + items = []; + notifyListeners(); + return 200; + } + + loading = true; + notifyListeners(); + try { + Response response = await ApiManager.instance.post( + URLs.getDepartmentsBySites, + body: {"siteIds": siteIds}, + ); + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + var responseData = json.decode(response.body); + List departmentListJson = responseData is List ? responseData : responseData['data'] ?? []; + items = departmentListJson.map((item) => Department.fromJson(item)).toList(); + } + loading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + loading = false; + stateCode = -1; + notifyListeners(); + return -1; + } + } +}