From 527ba1eb19b946e6e79f566d5558098a855fb9dc Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:56:27 +0300 Subject: [PATCH] site implementation completed --- lib/controllers/api_routes/urls.dart | 1 + .../models/dropdown_models.dart | 3 +- .../models/mock_user_model.dart | 20 - .../models/request_type_model.dart | 1 + .../models/site_request_model.dart | 117 +++- .../providers/site_request_provider.dart | 598 +++++++----------- .../screens/site_request_form_screen.dart | 507 ++++++++++++--- .../screens/site_request_history_screen.dart | 307 +++++---- 8 files changed, 905 insertions(+), 649 deletions(-) delete mode 100644 lib/modules/site_request_module/models/mock_user_model.dart diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index bacfe450..0e17718a 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -65,6 +65,7 @@ class URLs { static get submitSiteRequest => "$_baseUrl/SiteRequest/CreateSiteRequest"; // post submit site request static get getSiteRequests => "$_baseUrl/SiteRequest/GetSiteRequests"; // get site request history static get getDepartmentsBySites => "$_baseUrl/Customer/GetDepartmentsBySites"; // post get departments by sites + static getUserSites(String userId) => "$_baseUrl/Customer/GetSitesForUser?userId=$userId"; // get user's current sites // static get login => "$_baseUrl/MobileAuth/LoginIntegration"; // mobile login static get register => "$_baseUrl/handle/create/user"; // post diff --git a/lib/modules/site_request_module/models/dropdown_models.dart b/lib/modules/site_request_module/models/dropdown_models.dart index e193f4aa..4fd224e4 100644 --- a/lib/modules/site_request_module/models/dropdown_models.dart +++ b/lib/modules/site_request_module/models/dropdown_models.dart @@ -1,5 +1,6 @@ import 'package:test_sa/models/base.dart'; +/// Model representing a site for site request operations class SiteRequestSiteModel extends Base { SiteRequestSiteModel({ required String id, @@ -7,10 +8,10 @@ class SiteRequestSiteModel extends Base { }) : super(identifier: id, name: name); } +/// Model representing a department for site request operations 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 deleted file mode 100644 index 801cc483..00000000 --- a/lib/modules/site_request_module/models/mock_user_model.dart +++ /dev/null @@ -1,20 +0,0 @@ -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 index 74364eee..54486795 100644 --- a/lib/modules/site_request_module/models/request_type_model.dart +++ b/lib/modules/site_request_module/models/request_type_model.dart @@ -1,5 +1,6 @@ import 'package:test_sa/models/base.dart'; +/// Model representing a site request type (Add Site, Change Site, Remove Site) class RequestTypeModel extends Base { final int? value; diff --git a/lib/modules/site_request_module/models/site_request_model.dart b/lib/modules/site_request_module/models/site_request_model.dart index 779b49e8..814f49ed 100644 --- a/lib/modules/site_request_module/models/site_request_model.dart +++ b/lib/modules/site_request_module/models/site_request_model.dart @@ -1,6 +1,8 @@ +/// Model representing a site request with all necessary information +/// Supports three request types: Add Site, Change Site, and Remove Site class SiteRequestModel { final String? id; - final String requestType; // Add Site, Change Site, Remove Site + final String requestType; final String employeeId; final String fullName; final String emailAddress; @@ -8,6 +10,8 @@ class SiteRequestModel { final String? extensionNumber; final List siteIds; final List siteNames; + final List? oldSiteIds; // For Change Site request + final List? oldSiteNames; // For Change Site request final List? departmentIds; final List? departmentNames; final String role; @@ -24,6 +28,8 @@ class SiteRequestModel { this.extensionNumber, required this.siteIds, required this.siteNames, + this.oldSiteIds, + this.oldSiteNames, this.departmentIds, this.departmentNames, required this.role, @@ -31,6 +37,7 @@ class SiteRequestModel { required this.requestDate, }); + /// Converts model to JSON for API submission Map toJson() { return { 'id': id, @@ -42,6 +49,8 @@ class SiteRequestModel { 'extensionNumber': extensionNumber, 'siteIds': siteIds, 'siteNames': siteNames, + 'oldSiteIds': oldSiteIds, + 'oldSiteNames': oldSiteNames, 'departmentIds': departmentIds, 'departmentNames': departmentNames, 'role': role, @@ -50,25 +59,32 @@ class SiteRequestModel { }; } + /// Creates model from API response JSON factory SiteRequestModel.fromJson(Map json) { + final sitesData = _parseSitesFromJson(json); + final departmentsData = _parseDepartmentsFromJson(json); + return SiteRequestModel( - id: json['id'], - requestType: json['requestType'] ?? '', + id: json['id']?.toString() ?? json['requestNumber']?.toString() ?? '', + requestType: _parseRequestType(json), employeeId: json['employeeId'] ?? '', - fullName: json['fullName'] ?? '', - emailAddress: json['emailAddress'] ?? '', + fullName: json['userName'] ?? json['fullName'] ?? '', + emailAddress: json['email'] ?? 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, + extensionNumber: json['extensionNo'] ?? json['extensionNumber'], + siteIds: sitesData['siteIds'], + siteNames: sitesData['siteNames'], + oldSiteIds: sitesData['oldSiteIds'], + oldSiteNames: sitesData['oldSiteNames'], + departmentIds: departmentsData['departmentIds'], + departmentNames: departmentsData['departmentNames'], role: json['role'] ?? '', - status: json['status'] ?? 'Pending', - requestDate: json['requestDate'] != null ? DateTime.parse(json['requestDate']) : DateTime.now(), + status: _parseStatus(json), + requestDate: _parseDate(json), ); } + /// Creates a copy with optional field updates SiteRequestModel copyWith({ String? id, String? requestType, @@ -79,6 +95,8 @@ class SiteRequestModel { String? extensionNumber, List? siteIds, List? siteNames, + List? oldSiteIds, + List? oldSiteNames, List? departmentIds, List? departmentNames, String? role, @@ -95,6 +113,8 @@ class SiteRequestModel { extensionNumber: extensionNumber ?? this.extensionNumber, siteIds: siteIds ?? this.siteIds, siteNames: siteNames ?? this.siteNames, + oldSiteIds: oldSiteIds ?? this.oldSiteIds, + oldSiteNames: oldSiteNames ?? this.oldSiteNames, departmentIds: departmentIds ?? this.departmentIds, departmentNames: departmentNames ?? this.departmentNames, role: role ?? this.role, @@ -102,5 +122,76 @@ class SiteRequestModel { requestDate: requestDate ?? this.requestDate, ); } -} + // Private helper methods for parsing + + static Map _parseSitesFromJson(Map json) { + List siteIds = []; + List siteNames = []; + List? oldSiteIds; + List? oldSiteNames; + + if (json['sites'] != null && json['sites'] is List) { + for (var site in json['sites']) { + siteIds.add(site['siteId']?.toString() ?? ''); + siteNames.add(site['siteName'] ?? ''); + + if (site['oldSiteId'] != null && site['oldSiteId'] != 0) { + oldSiteIds ??= []; + oldSiteNames ??= []; + oldSiteIds.add(site['oldSiteId']?.toString() ?? ''); + oldSiteNames.add(site['oldSitName'] ?? ''); + } + } + } + + return { + 'siteIds': siteIds, + 'siteNames': siteNames, + 'oldSiteIds': oldSiteIds, + 'oldSiteNames': oldSiteNames, + }; + } + + static Map?> _parseDepartmentsFromJson(Map json) { + List? departmentIds; + List? departmentNames; + + if (json['departments'] != null && + json['departments'] is List && + (json['departments'] as List).isNotEmpty) { + departmentIds = []; + departmentNames = []; + for (var dept in json['departments']) { + departmentIds.add(dept['departmentId']?.toString() ?? ''); + departmentNames.add(dept['departmentName'] ?? ''); + } + } + + return { + 'departmentIds': departmentIds, + 'departmentNames': departmentNames, + }; + } + + static String _parseRequestType(Map json) { + return json['requestType'] is Map + ? (json['requestType']['name'] ?? '') + : (json['requestType'] ?? ''); + } + + static String _parseStatus(Map json) { + return json['status'] is Map + ? (json['status']['name'] ?? 'Pending') + : (json['status'] ?? 'Pending'); + } + + static DateTime _parseDate(Map json) { + if (json['createdDate'] != null) { + return DateTime.parse(json['createdDate']); + } else if (json['requestDate'] != null) { + return DateTime.parse(json['requestDate']); + } + return DateTime.now(); + } +} 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 014adceb..d9ed2b68 100644 --- a/lib/modules/site_request_module/providers/site_request_provider.dart +++ b/lib/modules/site_request_module/providers/site_request_provider.dart @@ -9,80 +9,75 @@ import 'package:test_sa/models/lookup.dart'; import 'dart:convert'; import 'package:http/http.dart'; +/// Provider for managing site request operations including: +/// - Fetching request types +/// - Managing user's current sites +/// - Submitting site requests +/// - Fetching and paginating request history class SiteRequestProvider extends ChangeNotifier { + // Constants + static const int _defaultPageSize = 10; + static const String _errorFetchFailed = 'Failed to fetch data'; + static const String _errorSubmitFailed = 'Failed to submit request'; + + // Loading states bool _loading = false; - bool get loading => _loading; + bool _isLoadingMore = false; - set loading(bool value) { - _loading = value; - notifyListeners(); - } + // Data + List _requestTypes = []; + List _userCurrentSites = []; + List _requestHistory = []; - String? _errorMessage; - String? get errorMessage => _errorMessage; + // Pagination + int _currentPage = 1; + int _pageSize = _defaultPageSize; + bool _hasMoreData = true; + // Error handling + String? _errorMessage; int? stateCode; - // Get current user from UserProvider - String getCurrentUserEmployeeId(UserProvider userProvider) { - return userProvider.user?.userID ?? ''; - } - - String getCurrentUserFullName(UserProvider userProvider) { - return userProvider.user?.username ?? ''; - } - - String getCurrentUserEmail(UserProvider userProvider) { - return userProvider.user?.email ?? ''; - } - - String getCurrentUserMobile(UserProvider userProvider) { - return userProvider.user?.phoneNumber ?? ''; - } + // Getters + bool get loading => _loading; + bool get isLoadingMore => _isLoadingMore; + String? get errorMessage => _errorMessage; + int get currentPage => _currentPage; + bool get hasMoreData => _hasMoreData; + List get requestTypes => _requestTypes; + List get userCurrentSites => _userCurrentSites; + List get requestHistory => _requestHistory; - String getCurrentUserExtension(UserProvider userProvider) { - return userProvider.user?.extensionNo ?? ''; - } + // User helper methods + String getCurrentUserEmployeeId(UserProvider userProvider) => userProvider.user?.userID ?? ''; + String getCurrentUserFullName(UserProvider userProvider) => userProvider.user?.username ?? ''; + String getCurrentUserEmail(UserProvider userProvider) => userProvider.user?.email ?? ''; + String getCurrentUserMobile(UserProvider userProvider) => userProvider.user?.phoneNumber ?? ''; + String getCurrentUserExtension(UserProvider userProvider) => 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) { - return []; + List getCurrentUserAssignedSites(UserProvider userProvider) => []; + bool hasAssignedSites(List assignedSites) => assignedSites.isNotEmpty; + + // Setter + set loading(bool value) { + _loading = value; + notifyListeners(); } - List _requestTypes = []; - List get requestTypes => _requestTypes; + /// Fetches available request types from the server 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); + return _performApiCall( + apiCall: () => ApiManager.instance.get(URLs.getSiteRequestTypes, enableToastMessage: false), + onSuccess: (responseData) { List listJson = responseData['data'] ?? responseData; _requestTypes = listJson .map((item) => Lookup.fromJson(item)) @@ -92,240 +87,66 @@ class SiteRequestProvider extends ChangeNotifier { 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; - } - List _allSites = []; - List get allSites => _allSites; - Future fetchSites() async { - if (_loading) return -2; - - _loading = true; - _errorMessage = null; - notifyListeners(); - - 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; - } - - 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; + }, + errorMessage: 'Failed to fetch request types', + ); } - // Mock departments as Base models - List _allDepartments = []; - List get allDepartments => _allDepartments; - - // Fetch departments from API (Step 3) - Future fetchDepartments() async { + /// Fetches user's current sites for Change/Remove Site operations + Future fetchUserCurrentSites(String userId) 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() ?? '', + return _performApiCall( + apiCall: () => ApiManager.instance.post(URLs.getUserSites(userId), showToast: false, body: {}), + onSuccess: (responseData) { + List listJson = responseData['data'] ?? []; + _userCurrentSites = listJson + .map((item) => SiteRequestSiteModel( + id: item['siteId']?.toString() ?? '', + name: item['siteName'] ?? '', )) .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; - - // Pagination state - int _currentPage = 1; - int _pageSize = 10; - bool _hasMoreData = true; - bool _isLoadingMore = false; - - int get currentPage => _currentPage; - bool get hasMoreData => _hasMoreData; - bool get isLoadingMore => _isLoadingMore; - - // Get available sites based on request type - List getAvailableSites(String requestType, List assignedSites) { - if (requestType == 'Remove Site') { - // Only return currently assigned sites - return _allSites.where((site) { - return assignedSites.contains(site.name ?? ''); - }).toList(); - } - // For Add Site and Change Site, return all sites - return _allSites; + }, + errorMessage: 'Failed to fetch user sites', + ); } - // Check if user has assigned sites for removal - bool hasAssignedSites(List assignedSites) { - return assignedSites.isNotEmpty; - } - - // Submit site request (local only, no API) + /// Submits a site request to the server Future submitSiteRequest(SiteRequestModel request, UserProvider userProvider) async { loading = true; _errorMessage = null; notifyListeners(); - late Response response; try { - // 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(); - - // 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, - }; + final isChangeSiteRequest = _requestTypes.any((type) => + type.identifier == request.requestType && (type.name?.toLowerCase().contains('change') ?? false)); - response = await ApiManager.instance.post( - URLs.submitSiteRequest, - body: requestBody, - ); - } catch (error) { - _errorMessage = 'Failed to submit request: $error'; - loading = false; - notifyListeners(); - return false; - } + final requestBody = _buildRequestBody(request, userProvider, isChangeSiteRequest); - 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(); + final response = await ApiManager.instance.post(URLs.submitSiteRequest, body: requestBody); + + if (response.statusCode >= 200 && response.statusCode < 300) { + _handleSubmitSuccess(response, request); 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'; + } else { + _handleSubmitError(response); + return false; } + } catch (error) { + _errorMessage = '$_errorSubmitFailed: $error'; loading = false; notifyListeners(); return false; } } - // Fetch paginated site requests - Future fetchSiteRequests({int? pageSize, int? pageNumber, bool loadMore = false}) async { + /// Fetches paginated site requests + Future fetchSiteRequests({ + int? pageSize, + int? pageNumber, + bool loadMore = false, + required UserProvider userProvider, + }) async { final size = pageSize ?? _pageSize; final page = pageNumber ?? (loadMore ? _currentPage + 1 : 1); @@ -335,18 +156,7 @@ class SiteRequestProvider extends ChangeNotifier { return; } - // Set loading state - if (loadMore) { - _isLoadingMore = true; - } else { - loading = true; - _currentPage = 1; - _hasMoreData = true; - _requestHistory.clear(); - } - - _errorMessage = null; - notifyListeners(); + _setLoadingState(loadMore); try { final response = await ApiManager.instance.post( @@ -354,54 +164,34 @@ class SiteRequestProvider extends ChangeNotifier { body: { "pageSize": size, "pageNumber": page, + "userId": getCurrentUserEmployeeId(userProvider), }, ); if (response.statusCode == 200) { - final data = json.decode(response.body); - - if (data['isSuccess'] == true) { - List siteRequests = (data['data'] as List?) - ?.map((item) => SiteRequestModel.fromJson(item)) - .toList() ?? []; - - if (loadMore) { - _requestHistory.addAll(siteRequests); - } else { - _requestHistory.clear(); - _requestHistory.addAll(siteRequests); - } - - // Update pagination state - _currentPage = page; - _hasMoreData = siteRequests.length >= size; - } else { - _errorMessage = data['message'] ?? 'Failed to fetch site requests'; - } + _handleFetchSuccess(response, page, size, loadMore); } else { - _errorMessage = 'Failed to fetch site requests'; + _errorMessage = _errorFetchFailed; } } catch (e) { _errorMessage = 'Error: ${e.toString()}'; } finally { - loading = false; - _isLoadingMore = false; - notifyListeners(); + _resetLoadingState(); } } - // Load more site requests (for pagination) - Future loadMoreSiteRequests() async { + /// Loads more site requests (pagination) + Future loadMoreSiteRequests({required UserProvider userProvider}) async { if (!_hasMoreData || _isLoadingMore) return; - await fetchSiteRequests(loadMore: true); + await fetchSiteRequests(loadMore: true, userProvider: userProvider); } - // Refresh site requests (reset to first page) - Future refreshSiteRequests() async { - await fetchSiteRequests(pageNumber: 1, pageSize: _pageSize); + /// Refreshes site requests (resets to first page) + Future refreshSiteRequests({required UserProvider userProvider}) async { + await fetchSiteRequests(pageNumber: 1, pageSize: _pageSize, userProvider: userProvider); } - // Reset provider state + /// Resets provider state void reset() { _loading = false; _errorMessage = null; @@ -409,58 +199,150 @@ class SiteRequestProvider extends ChangeNotifier { notifyListeners(); } - // Add mock history data for testing - void addMockHistoryData(UserProvider userProvider) { - final mockRequests = [ - SiteRequestModel( - id: 'REQ_001', - requestType: 'Add Site', - 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: getCurrentUserRole(userProvider), - status: 'Approved', - requestDate: DateTime.now().subtract(const Duration(days: 5)), - ), - SiteRequestModel( - id: 'REQ_002', - requestType: 'Change Site', - 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: getCurrentUserRole(userProvider), - status: 'Pending', - requestDate: DateTime.now().subtract(const Duration(days: 2)), - ), - SiteRequestModel( - id: 'REQ_003', - requestType: 'Remove Site', - employeeId: getCurrentUserEmployeeId(userProvider), - fullName: getCurrentUserFullName(userProvider), - emailAddress: getCurrentUserEmail(userProvider), - mobileNumber: getCurrentUserMobile(userProvider), - extensionNumber: getCurrentUserExtension(userProvider), - siteIds: ['site_2'], - siteNames: ['Site B'], - role: getCurrentUserRole(userProvider), - status: 'Rejected', - requestDate: DateTime.now().subtract(const Duration(days: 10)), - ), - ]; - - _requestHistory.addAll(mockRequests); + // Private helper methods + + /// Generic API call handler to reduce duplication + Future _performApiCall({ + required Future Function() apiCall, + required void Function(dynamic) onSuccess, + required String errorMessage, + }) async { + _loading = true; + _errorMessage = null; + notifyListeners(); + + try { + final response = await apiCall(); + stateCode = response.statusCode; + + if (response.statusCode >= 200 && response.statusCode < 300) { + final responseData = json.decode(response.body); + onSuccess(responseData); + } else { + _errorMessage = errorMessage; + } + + _loading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + _loading = false; + stateCode = -1; + _errorMessage = errorMessage; + notifyListeners(); + return -1; + } + } + + Map _buildRequestBody( + SiteRequestModel request, + UserProvider userProvider, + bool isChangeSiteRequest, + ) { + final siteIdsArray = request.siteIds.asMap().entries.map((entry) { + final index = entry.key; + return { + "id": 0, + "siteId": int.tryParse(request.siteIds[index]) ?? 0, + "siteName": request.siteNames[index], + "oldSiteId": (isChangeSiteRequest && + request.oldSiteIds != null && + index < request.oldSiteIds!.length) + ? int.tryParse(request.oldSiteIds![index]) ?? 0 + : 0, + "oldSitName": (isChangeSiteRequest && + request.oldSiteNames != null && + index < request.oldSiteNames!.length) + ? request.oldSiteNames![index] + : "" + }; + }).toList(); + + 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], + }; + }).toList() + : []; + + return { + "id": 0, + "userId": getCurrentUserEmployeeId(userProvider), + "requestTypeId": int.tryParse(request.requestType) ?? 0, + "siteIds": siteIdsArray, + "departmentIds": departmentIdsArray, + }; + } + + void _handleSubmitSuccess(Response response, SiteRequestModel request) { + try { + final responseData = json.decode(response.body); + final requestWithId = request.copyWith( + id: responseData['id']?.toString() ?? 'REQ_${DateTime.now().millisecondsSinceEpoch}', + status: responseData['status']?.toString() ?? 'Pending', + requestDate: DateTime.now(), + ); + _requestHistory.insert(0, requestWithId); + } catch (e) { + _errorMessage = 'Request submitted but failed to parse response'; + } finally { + loading = false; + notifyListeners(); + } + } + + void _handleSubmitError(Response response) { + try { + final errorData = json.decode(response.body); + _errorMessage = errorData['message'] ?? _errorSubmitFailed; + } catch (e) { + _errorMessage = _errorSubmitFailed; + } + loading = false; + notifyListeners(); + } + + void _setLoadingState(bool loadMore) { + if (loadMore) { + _isLoadingMore = true; + } else { + loading = true; + _currentPage = 1; + _hasMoreData = true; + _requestHistory.clear(); + } + _errorMessage = null; + notifyListeners(); + } + + void _handleFetchSuccess(Response response, int page, int size, bool loadMore) { + final data = json.decode(response.body); + + if (data['isSuccess'] == true) { + final siteRequests = (data['data'] as List?) + ?.map((item) => SiteRequestModel.fromJson(item)) + .toList() ?? []; + + if (loadMore) { + _requestHistory.addAll(siteRequests); + } else { + _requestHistory.clear(); + _requestHistory.addAll(siteRequests); + } + + _currentPage = page; + _hasMoreData = siteRequests.length >= size; + } else { + _errorMessage = data['message'] ?? _errorFetchFailed; + } + } + + void _resetLoadingState() { + loading = false; + _isLoadingMore = false; 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 index 77b8a2cb..8bc635a5 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,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; @@ -13,18 +11,19 @@ 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/models/dropdown_models.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'; import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; +import 'package:test_sa/views/widgets/loaders/app_loading.dart'; class SiteRequestFormScreen extends StatefulWidget { static const String routeName = "/site_request_form"; @@ -43,8 +42,15 @@ class _SiteRequestFormScreenState extends State { RequestTypeModel? _selectedRequestType; List _selectedSites = []; + List _selectedOldSites = []; // For Change Site - old sites List _selectedDepartments = []; + // Map to track replacements: oldSiteId -> {newSite, departments} + Map _siteReplacements = {}; + + // Track which site IDs have already loaded departments to prevent infinite API calls + Set _loadedDepartmentSiteIds = {}; + bool _isSubmitting = false; bool _isLoadingData = true; @@ -58,6 +64,7 @@ class _SiteRequestFormScreenState extends State { // Load data after first frame to access context WidgetsBinding.instance.addPostFrameCallback((_) async { _provider = Provider.of(context, listen: false); + _userProvider = Provider.of(context, listen: false); // Initialize department provider with empty state final departmentProvider = Provider.of(context, listen: false); @@ -76,26 +83,23 @@ class _SiteRequestFormScreenState extends State { }); } + bool _isChangeSiteRequest() { + return _selectedRequestType?.name?.toLowerCase().contains('change') ?? false; + } + + bool _isRemoveSiteRequest() { + return _selectedRequestType?.name?.toLowerCase().contains('remove') ?? 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), appBar: const DefaultAppBar(title: "Site Request"), - body: Column( + body: _isLoadingData ?const ALoading().center:Column( children: [ Expanded( child: Form( @@ -110,9 +114,29 @@ class _SiteRequestFormScreenState extends State { _buildRequestTypeDropdown(), 16.height, if (_selectedRequestType != null) ...[ - _buildSiteMultiSelect(), - 16.height, - _buildDepartmentMultiSelect(), + if (_isChangeSiteRequest()) ...[ + _buildOldSiteDropdown(), + 16.height, + // Show replacement containers for each old site + if (_selectedOldSites.isNotEmpty) ...[ + Text( + "Site Replacements", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColor.textStyleColor(context), + ), + ), + 8.height, + ..._buildSiteReplacementContainers(), + ], + ] else if (_isRemoveSiteRequest()) ...[ + _buildRemoveSiteDropdown(), + ] else ...[ + _buildSiteMultiSelect(), + 16.height, + _buildDepartmentMultiSelect(), + ], ], ], ), @@ -149,12 +173,29 @@ class _SiteRequestFormScreenState extends State { staticData: _provider.requestTypes, showAsFullScreenDialog: false, showAsBottomSheet: true, - onSelect: (value) { + onSelect: (value) async { setState(() { _selectedRequestType = value; _selectedSites.clear(); + _selectedOldSites.clear(); _selectedDepartments.clear(); + _siteReplacements.clear(); + _loadedDepartmentSiteIds.clear(); // Reset loaded department site IDs }); + + // Fetch user's current sites if Change Site or Remove Site is selected + if ((_isChangeSiteRequest() || _isRemoveSiteRequest()) && value != null) { + final userId = _provider.getCurrentUserEmployeeId(_userProvider); + await _provider.fetchUserCurrentSites(userId); + } + + // Fetch all sites if Change Site is selected (needed for new site dropdown) + if (_isChangeSiteRequest() && value != null) { + final siteProvider = Provider.of(context, listen: false); + if (siteProvider.items.isEmpty) { + await siteProvider.getData(); + } + } }, validator: (value) { if (value == null) return "Please select request type"; @@ -198,34 +239,78 @@ class _SiteRequestFormScreenState extends State { ).toShadowContainer(context); } - 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: [ - SizedBox( - width: 130.toScreenWidth, - child: Text( - "$label:", - style: TextStyle( - fontSize: 14, - color: AppColor.labelTextStyleColor(context), - fontWeight: FontWeight.w500, - ), - ), - ), - Expanded( - child: Text( - value.isEmpty ? "-" : value, - style: TextStyle( - fontSize: 14, - color: AppColor.textStyleColor(context), - ), - ), - ), - ], - ), + Widget _buildOldSiteDropdown() { + return MultipleItemDropDownMenu( + context: context, + title: "Old Site (Current Site)", + initialValue: _selectedOldSites.map((s) => SiteRequestSiteModel( + id: s.identifier ?? '', + name: s.name ?? '', + )).toList(), + staticData: _provider.userCurrentSites, + showAsBottomSheet: true, + backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white, + showShadow: true, + height: 56.toScreenHeight, + showCancel: true, + onSelect: (value) { + if (value != null) { + setState(() { + _selectedOldSites = value.map((s) { + final site = Site( + id: int.tryParse(s.identifier ?? '0'), + custName: s.name, + ); + return site; + }).toList(); + + // Remove replacements for sites that are no longer selected + final newSiteIds = _selectedOldSites.map((s) => s.identifier ?? '').toSet(); + _siteReplacements.removeWhere((key, value) => !newSiteIds.contains(key)); + + // Initialize replacements for newly added sites + for (var site in _selectedOldSites) { + final siteId = site.identifier ?? ''; + if (!_siteReplacements.containsKey(siteId)) { + _siteReplacements[siteId] = SiteReplacement( + newSite: null, + departments: [], + ); + } + } + }); + } + }, + ); + } + + Widget _buildRemoveSiteDropdown() { + return MultipleItemDropDownMenu( + context: context, + title: "Site to Remove", + initialValue: _selectedSites.map((s) => SiteRequestSiteModel( + id: s.identifier ?? '', + name: s.name ?? '', + )).toList(), + staticData: _provider.userCurrentSites, + showAsBottomSheet: true, + backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white, + showShadow: true, + height: 56.toScreenHeight, + showCancel: true, + onSelect: (value) { + if (value != null) { + setState(() { + _selectedSites = value.map((s) { + final site = Site( + id: int.tryParse(s.identifier ?? '0'), + custName: s.name, + ); + return site; + }).toList(); + }); + } + }, ); } @@ -257,10 +342,6 @@ class _SiteRequestFormScreenState extends State { } Widget _buildDepartmentMultiSelect() { - // Enable department field only when sites are selected - final isDepartmentEnabled = _selectedSites.isNotEmpty; - log('Selected Sites: ${_selectedSites.length}, isDepartmentEnabled: $isDepartmentEnabled'); - return MultipleItemDropDownMenu( context: context, title: "Departments", @@ -270,8 +351,8 @@ class _SiteRequestFormScreenState extends State { showShadow: true, height: 56.toScreenHeight, showCancel: true, - enabled: isDepartmentEnabled, // Enable only when sites are selected - loading: !isDepartmentEnabled ? false : null, // Don't show loading when disabled + enabled: _selectedSites.isNotEmpty, + loading: _selectedSites.isEmpty ? false : null, onSelect: (value) { if (value != null) { setState(() { @@ -296,70 +377,304 @@ class _SiteRequestFormScreenState extends State { ); } + List _buildSiteReplacementContainers() { + return _selectedOldSites.map((oldSite) { + final oldSiteId = oldSite.identifier ?? ''; + final replacement = _siteReplacements[oldSiteId]; + + return Consumer( + builder: (context, siteProvider, child) { + // Filter out already selected new sites and the current old site + final alreadySelectedNewSites = _siteReplacements.values + .where((r) => r.newSite != null) + .map((r) => r.newSite!.identifier) + .toSet(); + + final filteredSites = siteProvider.items.where((site) { + // Exclude the old site itself + if (site.identifier == oldSiteId) return false; + // Exclude already selected new sites (except current one) + if (replacement?.newSite?.identifier != site.identifier && + alreadySelectedNewSites.contains(site.identifier)) { + return false; + } + return true; + }).toList(); + + return Container( + padding: EdgeInsets.all(16.toScreenWidth), + margin: EdgeInsets.only(bottom: 16.toScreenHeight), + decoration: BoxDecoration( + color: AppColor.background(context), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColor.neutral40.withValues(alpha: 0.3), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Old site name display (simple text only) + Text( + oldSite.name ?? 'Unknown Site', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColor.textStyleColor(context), + ), + ), + 16.height, + + // New site single selection + SingleItemDropDownMenu( + context: context, + title: "New Site", + initialValue: replacement?.newSite, + staticData: filteredSites, + showAsBottomSheet: true, + backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white, + showShadow: true, + height: 56.toScreenHeight, + onSelect: (value) { + setState(() { + if (value != null) { + // Remove old site ID from loaded set to allow fetching new departments + if (replacement?.newSite != null) { + final oldNewSiteId = int.tryParse(replacement!.newSite!.identifier ?? '0') ?? 0; + _loadedDepartmentSiteIds.remove(oldNewSiteId); + } + + _siteReplacements[oldSiteId] = SiteReplacement( + newSite: value, + departments: [], // Clear departments when new site changes + ); + } + }); + }, + ), + + 16.height, + + // Department multi-select based on new site + if (replacement?.newSite != null) ...[ + _buildDepartmentDropdownForSite(oldSiteId, replacement!.newSite!), + ], + ], + ), + ); + }, + ); + }).toList(); + } + + Widget _buildDepartmentDropdownForSite(String oldSiteId, Site newSite) { + final replacement = _siteReplacements[oldSiteId]; + final siteId = int.tryParse(newSite.identifier ?? '0') ?? 0; + + return Consumer( + builder: (context, departmentProvider, child) { + // Only fetch departments if we haven't loaded them for this site yet + if (siteId > 0 && !_loadedDepartmentSiteIds.contains(siteId)) { + // Mark this site as loaded to prevent infinite calls + _loadedDepartmentSiteIds.add(siteId); + + // Fetch departments in post frame callback + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + departmentProvider.setSiteIds([siteId]); + departmentProvider.getData(); + } + }); + } + + return MultipleItemDropDownMenu( + context: context, + title: "Departments", + initialValue: replacement?.departments ?? [], + staticData: departmentProvider.items, + showAsBottomSheet: true, + backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white, + showShadow: true, + height: 56.toScreenHeight, + showCancel: true, + loading: departmentProvider.loading, + onSelect: (value) { + setState(() { + if (value != null && replacement != null) { + _siteReplacements[oldSiteId] = SiteReplacement( + newSite: replacement.newSite, + departments: value, + ); + } + }); + }, + ); + }, + ); + } + Future _submitForm() async { + // Client-side validation only (no toast messages) 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; - } + // Validation for Change Site request + if (_isChangeSiteRequest()) { + if (_selectedOldSites.isEmpty) { + return; + } - setState(() { - _isSubmitting = true; - }); + // Validate that each old site has a new site selected + for (var oldSite in _selectedOldSites) { + final oldSiteId = oldSite.identifier ?? ''; + final replacement = _siteReplacements[oldSiteId]; + if (replacement == null || replacement.newSite == null) { + return; + } + } - if (mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => const AppLazyLoading(), - ); - } + // Build the payload from replacements + final newSiteIds = []; + final newSiteNames = []; + final oldSiteIds = []; + final oldSiteNames = []; + final departmentIds = []; + final departmentNames = []; + + for (var oldSite in _selectedOldSites) { + final oldSiteId = oldSite.identifier ?? ''; + final replacement = _siteReplacements[oldSiteId]!; + + // Add old site info + oldSiteIds.add(oldSiteId); + oldSiteNames.add(oldSite.name ?? ''); + + // Add new site info + newSiteIds.add(replacement.newSite!.identifier ?? ''); + newSiteNames.add(replacement.newSite!.name ?? ''); + + // Add departments for this replacement + for (var dept in replacement.departments) { + departmentIds.add(dept.identifier ?? ''); + departmentNames.add(dept.name ?? ''); + } + } - final request = SiteRequestModel( - requestType: _selectedRequestType!.identifier?.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.getCurrentUserRole(_userProvider), - status: 'Pending', - requestDate: DateTime.now(), - ); + setState(() { + _isSubmitting = true; + }); - final success = await _provider.submitSiteRequest(request, _userProvider); + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AppLazyLoading(), + ); + } - if (mounted) { - Navigator.pop(context); // Close loading dialog - } + final request = SiteRequestModel( + requestType: _selectedRequestType!.identifier?.toString() ?? '', + employeeId: _provider.getCurrentUserEmployeeId(_userProvider), + fullName: _provider.getCurrentUserFullName(_userProvider), + emailAddress: _provider.getCurrentUserEmail(_userProvider), + mobileNumber: _provider.getCurrentUserMobile(_userProvider), + extensionNumber: _provider.getCurrentUserExtension(_userProvider), + siteIds: newSiteIds, + siteNames: newSiteNames, + oldSiteIds: oldSiteIds, + oldSiteNames: oldSiteNames, + departmentIds: departmentIds.isNotEmpty ? departmentIds : null, + departmentNames: departmentNames.isNotEmpty ? departmentNames : null, + role: _provider.getCurrentUserRole(_userProvider), + status: 'Pending', + requestDate: DateTime.now(), + ); - setState(() { - _isSubmitting = false; - }); + final success = await _provider.submitSiteRequest(request, _userProvider); - if (success) { - Fluttertoast.showToast(msg: "Site request submitted successfully"); if (mounted) { + Navigator.pop(context); // Close loading dialog + } + + setState(() { + _isSubmitting = false; + }); + + // API will handle success/error toast messages + if (success && mounted) { Navigator.pushReplacementNamed(context, SiteRequestHistoryScreen.routeName); } } else { - Fluttertoast.showToast( - msg: _provider.errorMessage ?? "Failed to submit site request", + // For Add Site and Remove Site request types + if (_selectedSites.isEmpty) { + return; + } + + setState(() { + _isSubmitting = true; + }); + + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AppLazyLoading(), + ); + } + + final request = SiteRequestModel( + requestType: _selectedRequestType!.identifier?.toString() ?? '', + 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.getCurrentUserRole(_userProvider), + status: 'Pending', + requestDate: DateTime.now(), ); + + final success = await _provider.submitSiteRequest(request, _userProvider); + + if (mounted) { + Navigator.pop(context); // Close loading dialog + } + + setState(() { + _isSubmitting = false; + }); + + // API will handle success/error toast messages + if (success && mounted) { + Navigator.pushReplacementNamed(context, SiteRequestHistoryScreen.routeName); + } } } } + +class SiteReplacement { + final Site? newSite; + final List departments; + + SiteReplacement({ + required this.newSite, + required this.departments, + }); +} 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 dcf82692..f8807ae2 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,5 +1,4 @@ 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'; @@ -17,6 +16,7 @@ import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; import 'package:test_sa/views/widgets/requests/request_status.dart'; +/// Screen displaying the history of site requests with pagination support class SiteRequestHistoryScreen extends StatefulWidget { static const String routeName = "/site_request_history"; @@ -29,8 +29,8 @@ class SiteRequestHistoryScreen extends StatefulWidget { class _SiteRequestHistoryScreenState extends State { late SiteRequestProvider _provider; late UserProvider _userProvider; - bool _isLoadingData = true; late ScrollController _scrollController; + bool _isLoadingData = true; @override void initState() { @@ -49,7 +49,7 @@ class _SiteRequestHistoryScreenState extends State { void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.9) { - _provider.loadMoreSiteRequests(); + _provider.loadMoreSiteRequests(userProvider: _userProvider); } } @@ -57,18 +57,17 @@ class _SiteRequestHistoryScreenState extends State { WidgetsBinding.instance.addPostFrameCallback((_) async { _provider = Provider.of(context, listen: false); _userProvider = Provider.of(context, listen: false); - await _provider.fetchSiteRequests(pageNumber: 1, pageSize: 10); + + await _provider.fetchSiteRequests(pageNumber: 1, pageSize: 10, userProvider: _userProvider); if (mounted) { - setState(() { - _isLoadingData = false; - }); + setState(() => _isLoadingData = false); } }); } Future _refreshData() async { - await _provider.refreshSiteRequests(); + await _provider.refreshSiteRequests(userProvider: _userProvider); } @override @@ -80,82 +79,65 @@ class _SiteRequestHistoryScreenState extends State { backgroundColor: AppColor.scaffoldBackground(context), 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); - }), - ], + actions: [_buildNewRequestButton()], ), - body: _isLoadingData - ? const Center(child: CircularProgressIndicator()) - : _provider.requestHistory.isEmpty - ? const NoDataFound().center - : RefreshIndicator( - onRefresh: _refreshData, - child: ListView.builder( - controller: _scrollController, - padding: EdgeInsets.all(16.toScreenWidth), - itemCount: _provider.requestHistory.length + (_provider.isLoadingMore ? 1 : 0), - itemBuilder: (context, index) { - // Show loading indicator at the bottom - if (index == _provider.requestHistory.length) { - return Padding( - padding: EdgeInsets.symmetric(vertical: 16.toScreenHeight), - child: const Center(child: CircularProgressIndicator()), - ); - } - - final request = _provider.requestHistory[index]; - return _buildRequestCard(request); - }, - ), - ), + body: _buildBody(), ); } - Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + Widget _buildNewRequestButton() { + return 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.inbox_outlined, - size: 80, - color: AppColor.icon2Color(context).withValues(alpha: 0.5), + Icon(Icons.add, size: 20, color: AppColor.textStyleColor(context)), + Text( + "New Request", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColor.textStyleColor(context), + ), ), - 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), - ), + 4.width, ], ), + ).onPress(() => Navigator.pushNamed(context, SiteRequestFormScreen.routeName)); + } + + Widget _buildBody() { + if (_isLoadingData) { + return const Center(child: CircularProgressIndicator()); + } + + if (_provider.requestHistory.isEmpty) { + return const NoDataFound().center; + } + + return RefreshIndicator( + onRefresh: _refreshData, + child: ListView.builder( + controller: _scrollController, + padding: EdgeInsets.all(16.toScreenWidth), + itemCount: _provider.requestHistory.length + (_provider.isLoadingMore ? 1 : 0), + itemBuilder: (context, index) { + if (index == _provider.requestHistory.length) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 16.toScreenHeight), + child: const Center(child: CircularProgressIndicator()), + ); + } + + return _buildRequestCard(_provider.requestHistory[index]); + }, + ), ); } @@ -164,30 +146,40 @@ class _SiteRequestHistoryScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Status labels and date - matching ServiceRequestItemView layout - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StatusLabel( - label: request.status, - textColor: _getStatusTextColor(request.status), - backgroundColor: _getStatusColor(request.status), - ), - 8.width, - StatusLabel( - label: request.requestType, - textColor: AppColor.primary10, - backgroundColor: AppColor.primary10.withValues(alpha: 0.1), - ), - 1.width.expanded, - InfoDateWidget(request.requestDate.toString()), - ], - ), - - // Request header + _buildRequestHeader(request), InfoHeaderWidget("Site Request #${request.id ?? 'N/A'}"), + _buildUserInfo(request), + _buildSitesSection(request), + if (request.departmentNames != null && request.departmentNames!.isNotEmpty) + _buildDepartmentsSection(request), + ], + ).toShadowContainer(context).paddingOnly(bottom: 16); + } - // Request details using InfoTextWidget + Widget _buildRequestHeader(SiteRequestModel request) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StatusLabel( + label: request.status, + textColor: _getStatusTextColor(request.status), + backgroundColor: _getStatusColor(request.status), + ), + 8.width, + StatusLabel( + label: request.requestType, + textColor: AppColor.primary10, + backgroundColor: AppColor.primary10.withValues(alpha: 0.1), + ), + 1.width.expanded, + InfoDateWidget(request.requestDate.toString()), + ], + ); + } + + Widget _buildUserInfo(SiteRequestModel request) { + return Column( + children: [ InfoTextWidget(label: "Full Name", value: request.fullName), InfoTextWidget(label: "Employee ID", value: request.employeeId), InfoTextWidget(label: "Email", value: request.emailAddress), @@ -195,59 +187,66 @@ class _SiteRequestHistoryScreenState extends State { if (request.extensionNumber != null && request.extensionNumber!.isNotEmpty) InfoTextWidget(label: "Extension", value: request.extensionNumber!, showEmptyValue: true), InfoTextWidget(label: "Role", value: request.role), + ], + ); + } - // Sites section with chips - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 100.toScreenWidth, - child: Text( - "Sites:", - style: TextStyle( - fontSize: 14, - color: AppColor.labelTextStyleColor(context), - fontWeight: FontWeight.w500, - ), - ), - ), - Expanded( - child: Wrap( - runSpacing: 4.toScreenHeight, - spacing: 4.toScreenWidth, - children: request.siteNames.map((site) => _buildChip(site)).toList(), - ), - ), - ], - ).paddingOnly(bottom: 8), + Widget _buildSitesSection(SiteRequestModel request) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sites:", + style: TextStyle( + fontSize: 14, + color: AppColor.labelTextStyleColor(context), + fontWeight: FontWeight.w500, + ), + ), + 8.height, + Wrap( + runSpacing: 4.toScreenHeight, + spacing: 4.toScreenWidth, + children: request.siteNames.map((site) => _buildChip(site)).toList(), + ), + ], + ).paddingOnly(bottom: 8); + } - // Departments section with chips (if available) - if (request.departmentNames != null && request.departmentNames!.isNotEmpty) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 100.toScreenWidth, - child: Text( - "Departments:", - style: TextStyle( - fontSize: 14, - color: AppColor.labelTextStyleColor(context), - fontWeight: FontWeight.w500, - ), - ), - ), - Expanded( - child: Wrap( - runSpacing: 4.toScreenHeight, - spacing: 4.toScreenWidth, - children: request.departmentNames!.map((dept) => _buildChip(dept)).toList(), - ), - ), - ], + Widget _buildDepartmentsSection(SiteRequestModel request) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Departments:", + style: TextStyle( + fontSize: 14, + color: AppColor.labelTextStyleColor(context), + fontWeight: FontWeight.w500, ), + ), + 8.height, + Wrap( + runSpacing: 4.toScreenHeight, + spacing: 4.toScreenWidth, + children: request.departmentNames!.map((dept) => _buildChip(dept)).toList(), + ), ], - ).toShadowContainer(context).paddingOnly(bottom: 16); + ); + } + + Widget _buildChip(String label) { + return Chip( + backgroundColor: AppColor.backgroundTabBarColor(context), + side: BorderSide(color: AppColor.border2Color(context)), + label: Text(label), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30.0)), + labelStyle: AppTextStyles.tinyFont2.copyWith( + color: AppColor.labelTextStyleColor(context), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: EdgeInsets.symmetric(horizontal: 8.toScreenWidth, vertical: 4.toScreenHeight), + ); } Color _getStatusColor(String status) { @@ -273,18 +272,4 @@ class _SiteRequestHistoryScreenState extends State { return AppColor.yellowStatusTextColor; } } - - Widget _buildChip(String label) { - return Chip( - backgroundColor: AppColor.backgroundTabBarColor(context), - side: BorderSide(color: AppColor.border2Color(context)), - label: Text(label), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30.0)), - labelStyle: AppTextStyles.tinyFont2.copyWith( - color: AppColor.labelTextStyleColor(context), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: EdgeInsets.symmetric(horizontal: 8.toScreenWidth, vertical: 4.toScreenHeight), - ); - } }