site implementation completed

export_module_ui_ux_enhancement_signup
WaseemAbbasi22 2 weeks ago
parent c76b7253b0
commit 527ba1eb19

@ -65,6 +65,7 @@ class URLs {
static get submitSiteRequest => "$_baseUrl/SiteRequest/CreateSiteRequest"; // post submit site request static get submitSiteRequest => "$_baseUrl/SiteRequest/CreateSiteRequest"; // post submit site request
static get getSiteRequests => "$_baseUrl/SiteRequest/GetSiteRequests"; // get site request history static get getSiteRequests => "$_baseUrl/SiteRequest/GetSiteRequests"; // get site request history
static get getDepartmentsBySites => "$_baseUrl/Customer/GetDepartmentsBySites"; // post get departments by sites 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 login => "$_baseUrl/MobileAuth/LoginIntegration"; // mobile login
static get register => "$_baseUrl/handle/create/user"; // post static get register => "$_baseUrl/handle/create/user"; // post

@ -1,5 +1,6 @@
import 'package:test_sa/models/base.dart'; import 'package:test_sa/models/base.dart';
/// Model representing a site for site request operations
class SiteRequestSiteModel extends Base { class SiteRequestSiteModel extends Base {
SiteRequestSiteModel({ SiteRequestSiteModel({
required String id, required String id,
@ -7,10 +8,10 @@ class SiteRequestSiteModel extends Base {
}) : super(identifier: id, name: name); }) : super(identifier: id, name: name);
} }
/// Model representing a department for site request operations
class SiteRequestDepartmentModel extends Base { class SiteRequestDepartmentModel extends Base {
SiteRequestDepartmentModel({ SiteRequestDepartmentModel({
required String id, required String id,
required String name, required String name,
}) : super(identifier: id, name: name); }) : super(identifier: id, name: name);
} }

@ -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<String> assignedSites;
MockUserModel({
required this.employeeId,
required this.fullName,
required this.emailAddress,
required this.mobileNumber,
required this.extensionNumber,
required this.role,
required this.assignedSites,
});
}

@ -1,5 +1,6 @@
import 'package:test_sa/models/base.dart'; import 'package:test_sa/models/base.dart';
/// Model representing a site request type (Add Site, Change Site, Remove Site)
class RequestTypeModel extends Base { class RequestTypeModel extends Base {
final int? value; final int? value;

@ -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 { class SiteRequestModel {
final String? id; final String? id;
final String requestType; // Add Site, Change Site, Remove Site final String requestType;
final String employeeId; final String employeeId;
final String fullName; final String fullName;
final String emailAddress; final String emailAddress;
@ -8,6 +10,8 @@ class SiteRequestModel {
final String? extensionNumber; final String? extensionNumber;
final List<String> siteIds; final List<String> siteIds;
final List<String> siteNames; final List<String> siteNames;
final List<String>? oldSiteIds; // For Change Site request
final List<String>? oldSiteNames; // For Change Site request
final List<String>? departmentIds; final List<String>? departmentIds;
final List<String>? departmentNames; final List<String>? departmentNames;
final String role; final String role;
@ -24,6 +28,8 @@ class SiteRequestModel {
this.extensionNumber, this.extensionNumber,
required this.siteIds, required this.siteIds,
required this.siteNames, required this.siteNames,
this.oldSiteIds,
this.oldSiteNames,
this.departmentIds, this.departmentIds,
this.departmentNames, this.departmentNames,
required this.role, required this.role,
@ -31,6 +37,7 @@ class SiteRequestModel {
required this.requestDate, required this.requestDate,
}); });
/// Converts model to JSON for API submission
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
@ -42,6 +49,8 @@ class SiteRequestModel {
'extensionNumber': extensionNumber, 'extensionNumber': extensionNumber,
'siteIds': siteIds, 'siteIds': siteIds,
'siteNames': siteNames, 'siteNames': siteNames,
'oldSiteIds': oldSiteIds,
'oldSiteNames': oldSiteNames,
'departmentIds': departmentIds, 'departmentIds': departmentIds,
'departmentNames': departmentNames, 'departmentNames': departmentNames,
'role': role, 'role': role,
@ -50,25 +59,32 @@ class SiteRequestModel {
}; };
} }
/// Creates model from API response JSON
factory SiteRequestModel.fromJson(Map<String, dynamic> json) { factory SiteRequestModel.fromJson(Map<String, dynamic> json) {
final sitesData = _parseSitesFromJson(json);
final departmentsData = _parseDepartmentsFromJson(json);
return SiteRequestModel( return SiteRequestModel(
id: json['id'], id: json['id']?.toString() ?? json['requestNumber']?.toString() ?? '',
requestType: json['requestType'] ?? '', requestType: _parseRequestType(json),
employeeId: json['employeeId'] ?? '', employeeId: json['employeeId'] ?? '',
fullName: json['fullName'] ?? '', fullName: json['userName'] ?? json['fullName'] ?? '',
emailAddress: json['emailAddress'] ?? '', emailAddress: json['email'] ?? json['emailAddress'] ?? '',
mobileNumber: json['mobileNumber'] ?? '', mobileNumber: json['mobileNumber'] ?? '',
extensionNumber: json['extensionNumber'], extensionNumber: json['extensionNo'] ?? json['extensionNumber'],
siteIds: List<String>.from(json['siteIds'] ?? []), siteIds: sitesData['siteIds'],
siteNames: List<String>.from(json['siteNames'] ?? []), siteNames: sitesData['siteNames'],
departmentIds: json['departmentIds'] != null ? List<String>.from(json['departmentIds']) : null, oldSiteIds: sitesData['oldSiteIds'],
departmentNames: json['departmentNames'] != null ? List<String>.from(json['departmentNames']) : null, oldSiteNames: sitesData['oldSiteNames'],
departmentIds: departmentsData['departmentIds'],
departmentNames: departmentsData['departmentNames'],
role: json['role'] ?? '', role: json['role'] ?? '',
status: json['status'] ?? 'Pending', status: _parseStatus(json),
requestDate: json['requestDate'] != null ? DateTime.parse(json['requestDate']) : DateTime.now(), requestDate: _parseDate(json),
); );
} }
/// Creates a copy with optional field updates
SiteRequestModel copyWith({ SiteRequestModel copyWith({
String? id, String? id,
String? requestType, String? requestType,
@ -79,6 +95,8 @@ class SiteRequestModel {
String? extensionNumber, String? extensionNumber,
List<String>? siteIds, List<String>? siteIds,
List<String>? siteNames, List<String>? siteNames,
List<String>? oldSiteIds,
List<String>? oldSiteNames,
List<String>? departmentIds, List<String>? departmentIds,
List<String>? departmentNames, List<String>? departmentNames,
String? role, String? role,
@ -95,6 +113,8 @@ class SiteRequestModel {
extensionNumber: extensionNumber ?? this.extensionNumber, extensionNumber: extensionNumber ?? this.extensionNumber,
siteIds: siteIds ?? this.siteIds, siteIds: siteIds ?? this.siteIds,
siteNames: siteNames ?? this.siteNames, siteNames: siteNames ?? this.siteNames,
oldSiteIds: oldSiteIds ?? this.oldSiteIds,
oldSiteNames: oldSiteNames ?? this.oldSiteNames,
departmentIds: departmentIds ?? this.departmentIds, departmentIds: departmentIds ?? this.departmentIds,
departmentNames: departmentNames ?? this.departmentNames, departmentNames: departmentNames ?? this.departmentNames,
role: role ?? this.role, role: role ?? this.role,
@ -102,5 +122,76 @@ class SiteRequestModel {
requestDate: requestDate ?? this.requestDate, requestDate: requestDate ?? this.requestDate,
); );
} }
}
// Private helper methods for parsing
static Map<String, dynamic> _parseSitesFromJson(Map<String, dynamic> json) {
List<String> siteIds = [];
List<String> siteNames = [];
List<String>? oldSiteIds;
List<String>? 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<String, List<String>?> _parseDepartmentsFromJson(Map<String, dynamic> json) {
List<String>? departmentIds;
List<String>? 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<String, dynamic> json) {
return json['requestType'] is Map
? (json['requestType']['name'] ?? '')
: (json['requestType'] ?? '');
}
static String _parseStatus(Map<String, dynamic> json) {
return json['status'] is Map
? (json['status']['name'] ?? 'Pending')
: (json['status'] ?? 'Pending');
}
static DateTime _parseDate(Map<String, dynamic> json) {
if (json['createdDate'] != null) {
return DateTime.parse(json['createdDate']);
} else if (json['requestDate'] != null) {
return DateTime.parse(json['requestDate']);
}
return DateTime.now();
}
}

@ -9,80 +9,75 @@ import 'package:test_sa/models/lookup.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart'; 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 { 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 _loading = false;
bool get loading => _loading; bool _isLoadingMore = false;
set loading(bool value) { // Data
_loading = value; List<RequestTypeModel> _requestTypes = [];
notifyListeners(); List<SiteRequestSiteModel> _userCurrentSites = [];
} List<SiteRequestModel> _requestHistory = [];
String? _errorMessage; // Pagination
String? get errorMessage => _errorMessage; int _currentPage = 1;
int _pageSize = _defaultPageSize;
bool _hasMoreData = true;
// Error handling
String? _errorMessage;
int? stateCode; int? stateCode;
// Get current user from UserProvider // Getters
String getCurrentUserEmployeeId(UserProvider userProvider) { bool get loading => _loading;
return userProvider.user?.userID ?? ''; bool get isLoadingMore => _isLoadingMore;
} String? get errorMessage => _errorMessage;
int get currentPage => _currentPage;
String getCurrentUserFullName(UserProvider userProvider) { bool get hasMoreData => _hasMoreData;
return userProvider.user?.username ?? ''; List<RequestTypeModel> get requestTypes => _requestTypes;
} List<SiteRequestSiteModel> get userCurrentSites => _userCurrentSites;
List<SiteRequestModel> get requestHistory => _requestHistory;
String getCurrentUserEmail(UserProvider userProvider) {
return userProvider.user?.email ?? '';
}
String getCurrentUserMobile(UserProvider userProvider) {
return userProvider.user?.phoneNumber ?? '';
}
String getCurrentUserExtension(UserProvider userProvider) { // User helper methods
return userProvider.user?.extensionNo ?? ''; 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) { String getCurrentUserRole(UserProvider userProvider) {
// Get the first role if available
if (userProvider.user?.userRoles != null && userProvider.user!.userRoles!.isNotEmpty) { if (userProvider.user?.userRoles != null && userProvider.user!.userRoles!.isNotEmpty) {
return userProvider.user!.userRoles!.first.name ?? ''; return userProvider.user!.userRoles!.first.name ?? '';
} }
return ''; return '';
} }
List<String> getCurrentUserAssignedSites(UserProvider userProvider) { List<String> getCurrentUserAssignedSites(UserProvider userProvider) => [];
return []; bool hasAssignedSites(List<String> assignedSites) => assignedSites.isNotEmpty;
// Setter
set loading(bool value) {
_loading = value;
notifyListeners();
} }
List<RequestTypeModel> _requestTypes = []; /// Fetches available request types from the server
List<RequestTypeModel> get requestTypes => _requestTypes;
Future<int> fetchRequestTypes() async { Future<int> fetchRequestTypes() async {
if (_loading) return -2; if (_loading) return -2;
_loading = true; return _performApiCall(
_errorMessage = null; apiCall: () => ApiManager.instance.get(URLs.getSiteRequestTypes, enableToastMessage: false),
notifyListeners(); onSuccess: (responseData) {
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; List listJson = responseData['data'] ?? responseData;
_requestTypes = listJson _requestTypes = listJson
.map((item) => Lookup.fromJson(item)) .map((item) => Lookup.fromJson(item))
@ -92,240 +87,66 @@ class SiteRequestProvider extends ChangeNotifier {
value: lookup.value, value: lookup.value,
)) ))
.toList(); .toList();
} catch (e) { },
_errorMessage = 'Failed to parse request types: $e'; errorMessage: 'Failed to fetch request types',
_loading = false; );
notifyListeners();
return -1;
}
} else {
_errorMessage = 'Failed to fetch request types';
}
_loading = false;
notifyListeners();
return response.statusCode;
}
List<SiteRequestSiteModel> _allSites = [];
List<SiteRequestSiteModel> get allSites => _allSites;
Future<int> 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;
} }
// Mock departments as Base models /// Fetches user's current sites for Change/Remove Site operations
List<SiteRequestDepartmentModel> _allDepartments = []; Future<int> fetchUserCurrentSites(String userId) async {
List<SiteRequestDepartmentModel> get allDepartments => _allDepartments;
// Fetch departments from API (Step 3)
Future<int> fetchDepartments() async {
if (_loading) return -2; if (_loading) return -2;
_loading = true; return _performApiCall(
_errorMessage = null; apiCall: () => ApiManager.instance.post(URLs.getUserSites(userId), showToast: false, body: {}),
notifyListeners(); onSuccess: (responseData) {
List listJson = responseData['data'] ?? [];
late Response response; _userCurrentSites = listJson
try { .map((item) => SiteRequestSiteModel(
response = await ApiManager.instance.get( id: item['siteId']?.toString() ?? '',
URLs.signUpGetDepartmentAutoComplete, name: item['siteName'] ?? '',
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(); .toList();
} catch (e) { },
_errorMessage = 'Failed to parse departments'; errorMessage: 'Failed to fetch user sites',
_loading = false; );
notifyListeners();
return -1;
}
} else {
_errorMessage = 'Failed to fetch departments';
}
_loading = false;
notifyListeners();
return response.statusCode;
}
// History of submitted requests
final List<SiteRequestModel> _requestHistory = [];
List<SiteRequestModel> 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<SiteRequestSiteModel> getAvailableSites(String requestType, List<String> 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;
} }
// Check if user has assigned sites for removal /// Submits a site request to the server
bool hasAssignedSites(List<String> assignedSites) {
return assignedSites.isNotEmpty;
}
// Submit site request (local only, no API)
Future<bool> submitSiteRequest(SiteRequestModel request, UserProvider userProvider) async { Future<bool> submitSiteRequest(SiteRequestModel request, UserProvider userProvider) async {
loading = true; loading = true;
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
late Response response;
try { try {
// Prepare site IDs array with correct structure final isChangeSiteRequest = _requestTypes.any((type) =>
final siteIdsArray = request.siteIds.asMap().entries.map((entry) { type.identifier == request.requestType && (type.name?.toLowerCase().contains('change') ?? false));
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,
};
response = await ApiManager.instance.post( final requestBody = _buildRequestBody(request, userProvider, isChangeSiteRequest);
URLs.submitSiteRequest,
body: requestBody,
);
} catch (error) {
_errorMessage = 'Failed to submit request: $error';
loading = false;
notifyListeners();
return false;
}
stateCode = response.statusCode; final response = await ApiManager.instance.post(URLs.submitSiteRequest, body: requestBody);
if (response.statusCode >= 200 && response.statusCode < 300) {
try { if (response.statusCode >= 200 && response.statusCode < 300) {
var responseData = json.decode(response.body); _handleSubmitSuccess(response, request);
// 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; return true;
} catch (e) { } else {
_errorMessage = 'Request submitted but failed to parse response'; _handleSubmitError(response);
loading = false; return 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';
} }
} catch (error) {
_errorMessage = '$_errorSubmitFailed: $error';
loading = false; loading = false;
notifyListeners(); notifyListeners();
return false; return false;
} }
} }
// Fetch paginated site requests /// Fetches paginated site requests
Future<void> fetchSiteRequests({int? pageSize, int? pageNumber, bool loadMore = false}) async { Future<void> fetchSiteRequests({
int? pageSize,
int? pageNumber,
bool loadMore = false,
required UserProvider userProvider,
}) async {
final size = pageSize ?? _pageSize; final size = pageSize ?? _pageSize;
final page = pageNumber ?? (loadMore ? _currentPage + 1 : 1); final page = pageNumber ?? (loadMore ? _currentPage + 1 : 1);
@ -335,18 +156,7 @@ class SiteRequestProvider extends ChangeNotifier {
return; return;
} }
// Set loading state _setLoadingState(loadMore);
if (loadMore) {
_isLoadingMore = true;
} else {
loading = true;
_currentPage = 1;
_hasMoreData = true;
_requestHistory.clear();
}
_errorMessage = null;
notifyListeners();
try { try {
final response = await ApiManager.instance.post( final response = await ApiManager.instance.post(
@ -354,54 +164,34 @@ class SiteRequestProvider extends ChangeNotifier {
body: { body: {
"pageSize": size, "pageSize": size,
"pageNumber": page, "pageNumber": page,
"userId": getCurrentUserEmployeeId(userProvider),
}, },
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = json.decode(response.body); _handleFetchSuccess(response, page, size, loadMore);
if (data['isSuccess'] == true) {
List<SiteRequestModel> 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';
}
} else { } else {
_errorMessage = 'Failed to fetch site requests'; _errorMessage = _errorFetchFailed;
} }
} catch (e) { } catch (e) {
_errorMessage = 'Error: ${e.toString()}'; _errorMessage = 'Error: ${e.toString()}';
} finally { } finally {
loading = false; _resetLoadingState();
_isLoadingMore = false;
notifyListeners();
} }
} }
// Load more site requests (for pagination) /// Loads more site requests (pagination)
Future<void> loadMoreSiteRequests() async { Future<void> loadMoreSiteRequests({required UserProvider userProvider}) async {
if (!_hasMoreData || _isLoadingMore) return; if (!_hasMoreData || _isLoadingMore) return;
await fetchSiteRequests(loadMore: true); await fetchSiteRequests(loadMore: true, userProvider: userProvider);
} }
// Refresh site requests (reset to first page) /// Refreshes site requests (resets to first page)
Future<void> refreshSiteRequests() async { Future<void> refreshSiteRequests({required UserProvider userProvider}) async {
await fetchSiteRequests(pageNumber: 1, pageSize: _pageSize); await fetchSiteRequests(pageNumber: 1, pageSize: _pageSize, userProvider: userProvider);
} }
// Reset provider state /// Resets provider state
void reset() { void reset() {
_loading = false; _loading = false;
_errorMessage = null; _errorMessage = null;
@ -409,58 +199,150 @@ class SiteRequestProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// Add mock history data for testing // Private helper methods
void addMockHistoryData(UserProvider userProvider) {
final mockRequests = [ /// Generic API call handler to reduce duplication
SiteRequestModel( Future<int> _performApiCall({
id: 'REQ_001', required Future<Response> Function() apiCall,
requestType: 'Add Site', required void Function(dynamic) onSuccess,
employeeId: getCurrentUserEmployeeId(userProvider), required String errorMessage,
fullName: getCurrentUserFullName(userProvider), }) async {
emailAddress: getCurrentUserEmail(userProvider), _loading = true;
mobileNumber: getCurrentUserMobile(userProvider), _errorMessage = null;
extensionNumber: getCurrentUserExtension(userProvider), notifyListeners();
siteIds: ['site_3', 'site_4'],
siteNames: ['Site C', 'Site D'], try {
departmentIds: ['dept_2'], final response = await apiCall();
departmentNames: ['IT'], stateCode = response.statusCode;
role: getCurrentUserRole(userProvider),
status: 'Approved', if (response.statusCode >= 200 && response.statusCode < 300) {
requestDate: DateTime.now().subtract(const Duration(days: 5)), final responseData = json.decode(response.body);
), onSuccess(responseData);
SiteRequestModel( } else {
id: 'REQ_002', _errorMessage = errorMessage;
requestType: 'Change Site', }
employeeId: getCurrentUserEmployeeId(userProvider),
fullName: getCurrentUserFullName(userProvider), _loading = false;
emailAddress: getCurrentUserEmail(userProvider), notifyListeners();
mobileNumber: getCurrentUserMobile(userProvider), return response.statusCode;
extensionNumber: getCurrentUserExtension(userProvider), } catch (error) {
siteIds: ['site_1'], _loading = false;
siteNames: ['Site A'], stateCode = -1;
departmentIds: ['dept_1', 'dept_3'], _errorMessage = errorMessage;
departmentNames: ['HR', 'Finance'], notifyListeners();
role: getCurrentUserRole(userProvider), return -1;
status: 'Pending', }
requestDate: DateTime.now().subtract(const Duration(days: 2)), }
),
SiteRequestModel( Map<String, dynamic> _buildRequestBody(
id: 'REQ_003', SiteRequestModel request,
requestType: 'Remove Site', UserProvider userProvider,
employeeId: getCurrentUserEmployeeId(userProvider), bool isChangeSiteRequest,
fullName: getCurrentUserFullName(userProvider), ) {
emailAddress: getCurrentUserEmail(userProvider), final siteIdsArray = request.siteIds.asMap().entries.map((entry) {
mobileNumber: getCurrentUserMobile(userProvider), final index = entry.key;
extensionNumber: getCurrentUserExtension(userProvider), return {
siteIds: ['site_2'], "id": 0,
siteNames: ['Site B'], "siteId": int.tryParse(request.siteIds[index]) ?? 0,
role: getCurrentUserRole(userProvider), "siteName": request.siteNames[index],
status: 'Rejected', "oldSiteId": (isChangeSiteRequest &&
requestDate: DateTime.now().subtract(const Duration(days: 10)), request.oldSiteIds != null &&
), index < request.oldSiteIds!.length)
]; ? int.tryParse(request.oldSiteIds![index]) ?? 0
: 0,
_requestHistory.addAll(mockRequests); "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(); notifyListeners();
} }
} }

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:provider/provider.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/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/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/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/providers/site_request_provider.dart';
import 'package:test_sa/modules/site_request_module/screens/site_request_history_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/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_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.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/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/single_item_drop_down_menu.dart';
import 'package:test_sa/new_views/common_widgets/multiple_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/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_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_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 { class SiteRequestFormScreen extends StatefulWidget {
static const String routeName = "/site_request_form"; static const String routeName = "/site_request_form";
@ -43,8 +42,15 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
RequestTypeModel? _selectedRequestType; RequestTypeModel? _selectedRequestType;
List<Site> _selectedSites = []; List<Site> _selectedSites = [];
List<Site> _selectedOldSites = []; // For Change Site - old sites
List<Department> _selectedDepartments = []; List<Department> _selectedDepartments = [];
// Map to track replacements: oldSiteId -> {newSite, departments}
Map<String, SiteReplacement> _siteReplacements = {};
// Track which site IDs have already loaded departments to prevent infinite API calls
Set<int> _loadedDepartmentSiteIds = {};
bool _isSubmitting = false; bool _isSubmitting = false;
bool _isLoadingData = true; bool _isLoadingData = true;
@ -58,6 +64,7 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
// Load data after first frame to access context // Load data after first frame to access context
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
_provider = Provider.of<SiteRequestProvider>(context, listen: false); _provider = Provider.of<SiteRequestProvider>(context, listen: false);
_userProvider = Provider.of<UserProvider>(context, listen: false);
// Initialize department provider with empty state // Initialize department provider with empty state
final departmentProvider = Provider.of<DepartmentBySitesProvider>(context, listen: false); final departmentProvider = Provider.of<DepartmentBySitesProvider>(context, listen: false);
@ -76,26 +83,23 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
}); });
} }
bool _isChangeSiteRequest() {
return _selectedRequestType?.name?.toLowerCase().contains('change') ?? false;
}
bool _isRemoveSiteRequest() {
return _selectedRequestType?.name?.toLowerCase().contains('remove') ?? false;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_provider = Provider.of<SiteRequestProvider>(context); _provider = Provider.of<SiteRequestProvider>(context);
_userProvider = Provider.of<UserProvider>(context); _userProvider = Provider.of<UserProvider>(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( return Scaffold(
backgroundColor: AppColor.scaffoldBackground(context), backgroundColor: AppColor.scaffoldBackground(context),
appBar: const DefaultAppBar(title: "Site Request"), appBar: const DefaultAppBar(title: "Site Request"),
body: Column( body: _isLoadingData ?const ALoading().center:Column(
children: [ children: [
Expanded( Expanded(
child: Form( child: Form(
@ -110,9 +114,29 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
_buildRequestTypeDropdown(), _buildRequestTypeDropdown(),
16.height, 16.height,
if (_selectedRequestType != null) ...[ if (_selectedRequestType != null) ...[
_buildSiteMultiSelect(), if (_isChangeSiteRequest()) ...[
16.height, _buildOldSiteDropdown(),
_buildDepartmentMultiSelect(), 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<SiteRequestFormScreen> {
staticData: _provider.requestTypes, staticData: _provider.requestTypes,
showAsFullScreenDialog: false, showAsFullScreenDialog: false,
showAsBottomSheet: true, showAsBottomSheet: true,
onSelect: (value) { onSelect: (value) async {
setState(() { setState(() {
_selectedRequestType = value; _selectedRequestType = value;
_selectedSites.clear(); _selectedSites.clear();
_selectedOldSites.clear();
_selectedDepartments.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<SiteProvider>(context, listen: false);
if (siteProvider.items.isEmpty) {
await siteProvider.getData();
}
}
}, },
validator: (value) { validator: (value) {
if (value == null) return "Please select request type"; if (value == null) return "Please select request type";
@ -198,34 +239,78 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
).toShadowContainer(context); ).toShadowContainer(context);
} }
Widget _buildInfoRow(String label, String value, {bool isLast = false}) { Widget _buildOldSiteDropdown() {
return Padding( return MultipleItemDropDownMenu<SiteRequestSiteModel, NullableLoadingProvider>(
padding: EdgeInsets.only(bottom: isLast ? 0 : 12.toScreenHeight), context: context,
child: Row( title: "Old Site (Current Site)",
crossAxisAlignment: CrossAxisAlignment.start, initialValue: _selectedOldSites.map((s) => SiteRequestSiteModel(
children: [ id: s.identifier ?? '',
SizedBox( name: s.name ?? '',
width: 130.toScreenWidth, )).toList(),
child: Text( staticData: _provider.userCurrentSites,
"$label:", showAsBottomSheet: true,
style: TextStyle( backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white,
fontSize: 14, showShadow: true,
color: AppColor.labelTextStyleColor(context), height: 56.toScreenHeight,
fontWeight: FontWeight.w500, showCancel: true,
), onSelect: (value) {
), if (value != null) {
), setState(() {
Expanded( _selectedOldSites = value.map((s) {
child: Text( final site = Site(
value.isEmpty ? "-" : value, id: int.tryParse(s.identifier ?? '0'),
style: TextStyle( custName: s.name,
fontSize: 14, );
color: AppColor.textStyleColor(context), 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<SiteRequestSiteModel, NullableLoadingProvider>(
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<SiteRequestFormScreen> {
} }
Widget _buildDepartmentMultiSelect() { Widget _buildDepartmentMultiSelect() {
// Enable department field only when sites are selected
final isDepartmentEnabled = _selectedSites.isNotEmpty;
log('Selected Sites: ${_selectedSites.length}, isDepartmentEnabled: $isDepartmentEnabled');
return MultipleItemDropDownMenu<Department, DepartmentBySitesProvider>( return MultipleItemDropDownMenu<Department, DepartmentBySitesProvider>(
context: context, context: context,
title: "Departments", title: "Departments",
@ -270,8 +351,8 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
showShadow: true, showShadow: true,
height: 56.toScreenHeight, height: 56.toScreenHeight,
showCancel: true, showCancel: true,
enabled: isDepartmentEnabled, // Enable only when sites are selected enabled: _selectedSites.isNotEmpty,
loading: !isDepartmentEnabled ? false : null, // Don't show loading when disabled loading: _selectedSites.isEmpty ? false : null,
onSelect: (value) { onSelect: (value) {
if (value != null) { if (value != null) {
setState(() { setState(() {
@ -296,70 +377,304 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
); );
} }
List<Widget> _buildSiteReplacementContainers() {
return _selectedOldSites.map((oldSite) {
final oldSiteId = oldSite.identifier ?? '';
final replacement = _siteReplacements[oldSiteId];
return Consumer<SiteProvider>(
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<Site, NullableLoadingProvider>(
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<DepartmentBySitesProvider>(
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<Department, NullableLoadingProvider>(
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<void> _submitForm() async { Future<void> _submitForm() async {
// Client-side validation only (no toast messages)
if (!_formKey.currentState!.validate()) { if (!_formKey.currentState!.validate()) {
Fluttertoast.showToast(msg: "Please fill all required fields");
return; return;
} }
if (_selectedRequestType == null) { if (_selectedRequestType == null) {
Fluttertoast.showToast(msg: "Please select request type");
return; return;
} }
if (_selectedSites.isEmpty) { // Validation for Change Site request
Fluttertoast.showToast(msg: "Please select at least one site"); if (_isChangeSiteRequest()) {
return; if (_selectedOldSites.isEmpty) {
} return;
}
setState(() { // Validate that each old site has a new site selected
_isSubmitting = true; for (var oldSite in _selectedOldSites) {
}); final oldSiteId = oldSite.identifier ?? '';
final replacement = _siteReplacements[oldSiteId];
if (replacement == null || replacement.newSite == null) {
return;
}
}
if (mounted) { // Build the payload from replacements
showDialog( final newSiteIds = <String>[];
context: context, final newSiteNames = <String>[];
barrierDismissible: false, final oldSiteIds = <String>[];
builder: (context) => const AppLazyLoading(), final oldSiteNames = <String>[];
); final departmentIds = <String>[];
} final departmentNames = <String>[];
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( setState(() {
requestType: _selectedRequestType!.identifier?.toString() ?? '', _isSubmitting = true;
// 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(),
);
final success = await _provider.submitSiteRequest(request, _userProvider); if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AppLazyLoading(),
);
}
if (mounted) { final request = SiteRequestModel(
Navigator.pop(context); // Close loading dialog 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(() { final success = await _provider.submitSiteRequest(request, _userProvider);
_isSubmitting = false;
});
if (success) {
Fluttertoast.showToast(msg: "Site request submitted successfully");
if (mounted) { 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); Navigator.pushReplacementNamed(context, SiteRequestHistoryScreen.routeName);
} }
} else { } else {
Fluttertoast.showToast( // For Add Site and Remove Site request types
msg: _provider.errorMessage ?? "Failed to submit site request", 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<Department> departments;
SiteReplacement({
required this.newSite,
required this.departments,
});
}

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/controllers/providers/api/user_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/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/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/requests/request_status.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 { class SiteRequestHistoryScreen extends StatefulWidget {
static const String routeName = "/site_request_history"; static const String routeName = "/site_request_history";
@ -29,8 +29,8 @@ class SiteRequestHistoryScreen extends StatefulWidget {
class _SiteRequestHistoryScreenState extends State<SiteRequestHistoryScreen> { class _SiteRequestHistoryScreenState extends State<SiteRequestHistoryScreen> {
late SiteRequestProvider _provider; late SiteRequestProvider _provider;
late UserProvider _userProvider; late UserProvider _userProvider;
bool _isLoadingData = true;
late ScrollController _scrollController; late ScrollController _scrollController;
bool _isLoadingData = true;
@override @override
void initState() { void initState() {
@ -49,7 +49,7 @@ class _SiteRequestHistoryScreenState extends State<SiteRequestHistoryScreen> {
void _onScroll() { void _onScroll() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.9) { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.9) {
_provider.loadMoreSiteRequests(); _provider.loadMoreSiteRequests(userProvider: _userProvider);
} }
} }
@ -57,18 +57,17 @@ class _SiteRequestHistoryScreenState extends State<SiteRequestHistoryScreen> {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
_provider = Provider.of<SiteRequestProvider>(context, listen: false); _provider = Provider.of<SiteRequestProvider>(context, listen: false);
_userProvider = Provider.of<UserProvider>(context, listen: false); _userProvider = Provider.of<UserProvider>(context, listen: false);
await _provider.fetchSiteRequests(pageNumber: 1, pageSize: 10);
await _provider.fetchSiteRequests(pageNumber: 1, pageSize: 10, userProvider: _userProvider);
if (mounted) { if (mounted) {
setState(() { setState(() => _isLoadingData = false);
_isLoadingData = false;
});
} }
}); });
} }
Future<void> _refreshData() async { Future<void> _refreshData() async {
await _provider.refreshSiteRequests(); await _provider.refreshSiteRequests(userProvider: _userProvider);
} }
@override @override
@ -80,82 +79,65 @@ class _SiteRequestHistoryScreenState extends State<SiteRequestHistoryScreen> {
backgroundColor: AppColor.scaffoldBackground(context), backgroundColor: AppColor.scaffoldBackground(context),
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: "Site Request History", title: "Site Request History",
actions: [ actions: [_buildNewRequestButton()],
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 body: _buildBody(),
? 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);
},
),
),
); );
} }
Widget _buildEmptyState() { Widget _buildNewRequestButton() {
return Center( return Container(
child: Column( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
mainAxisAlignment: MainAxisAlignment.center, 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: [ children: [
Icon( Icon(Icons.add, size: 20, color: AppColor.textStyleColor(context)),
Icons.inbox_outlined, Text(
size: 80, "New Request",
color: AppColor.icon2Color(context).withValues(alpha: 0.5), style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColor.textStyleColor(context),
),
), ),
16.height, 4.width,
"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),
),
], ],
), ),
).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<SiteRequestHistoryScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Status labels and date - matching ServiceRequestItemView layout _buildRequestHeader(request),
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
InfoHeaderWidget("Site Request #${request.id ?? 'N/A'}"), 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: "Full Name", value: request.fullName),
InfoTextWidget(label: "Employee ID", value: request.employeeId), InfoTextWidget(label: "Employee ID", value: request.employeeId),
InfoTextWidget(label: "Email", value: request.emailAddress), InfoTextWidget(label: "Email", value: request.emailAddress),
@ -195,59 +187,66 @@ class _SiteRequestHistoryScreenState extends State<SiteRequestHistoryScreen> {
if (request.extensionNumber != null && request.extensionNumber!.isNotEmpty) if (request.extensionNumber != null && request.extensionNumber!.isNotEmpty)
InfoTextWidget(label: "Extension", value: request.extensionNumber!, showEmptyValue: true), InfoTextWidget(label: "Extension", value: request.extensionNumber!, showEmptyValue: true),
InfoTextWidget(label: "Role", value: request.role), InfoTextWidget(label: "Role", value: request.role),
],
);
}
// Sites section with chips Widget _buildSitesSection(SiteRequestModel request) {
Row( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Text(
width: 100.toScreenWidth, "Sites:",
child: Text( style: TextStyle(
"Sites:", fontSize: 14,
style: TextStyle( color: AppColor.labelTextStyleColor(context),
fontSize: 14, fontWeight: FontWeight.w500,
color: AppColor.labelTextStyleColor(context), ),
fontWeight: FontWeight.w500, ),
), 8.height,
), Wrap(
), runSpacing: 4.toScreenHeight,
Expanded( spacing: 4.toScreenWidth,
child: Wrap( children: request.siteNames.map((site) => _buildChip(site)).toList(),
runSpacing: 4.toScreenHeight, ),
spacing: 4.toScreenWidth, ],
children: request.siteNames.map((site) => _buildChip(site)).toList(), ).paddingOnly(bottom: 8);
), }
),
],
).paddingOnly(bottom: 8),
// Departments section with chips (if available) Widget _buildDepartmentsSection(SiteRequestModel request) {
if (request.departmentNames != null && request.departmentNames!.isNotEmpty) return Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
SizedBox( "Departments:",
width: 100.toScreenWidth, style: TextStyle(
child: Text( fontSize: 14,
"Departments:", color: AppColor.labelTextStyleColor(context),
style: TextStyle( fontWeight: FontWeight.w500,
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(),
),
),
],
), ),
),
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) { Color _getStatusColor(String status) {
@ -273,18 +272,4 @@ class _SiteRequestHistoryScreenState extends State<SiteRequestHistoryScreen> {
return AppColor.yellowStatusTextColor; 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),
);
}
} }

Loading…
Cancel
Save