Merge branch 'refs/heads/main_production' into export_module_ui_ux_enhancement_signup

# Conflicts:
#	lib/extensions/context_extension.dart
#	lib/main.dart
#	lib/new_views/common_widgets/default_app_bar.dart
export_module_ui_ux_enhancement_signup
Sikander Saleem 3 days ago
commit f62f5c9db7

@ -0,0 +1,13 @@
<svg width="130" height="150" viewBox="0 0 130 150" xmlns="http://www.w3.org/2000/svg">
<!-- front face -->
<rect x="30" y="46" width="65" height="48" rx="4" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- screen indicator -->
<rect x="40" y="56" width="26" height="16" rx="2" fill="none" stroke="#3D3D3A" stroke-width="1.2"/>
<!-- bottom face (highlighted) -->
<polygon points="30,94 50,114 115,114 95,94" fill="#F5C4B3" stroke="#993C1D" stroke-width="2"/>
<!-- right side face -->
<polygon points="95,46 115,26 115,74 95,94" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- arrow pointing up into bottom face -->
<line x1="73" y1="142" x2="73" y2="118" stroke="#3D3D3A" stroke-width="3" stroke-linecap="round"/>
<polygon points="73,112 66,124 80,124" fill="#3D3D3A"/>
</svg>

After

Width:  |  Height:  |  Size: 808 B

@ -0,0 +1,13 @@
<svg width="150" height="150" viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
<!-- front face (highlighted, left side facing camera) -->
<rect x="32" y="52" width="56" height="58" rx="4" fill="#9FE1CB" stroke="#0F6E56" stroke-width="2"/>
<!-- screen indicator -->
<rect x="42" y="62" width="24" height="15" rx="2" fill="none" stroke="#0F6E56" stroke-width="1.2"/>
<!-- top face -->
<polygon points="32,52 52,32 118,32 88,52" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- right side face -->
<polygon points="88,52 118,32 118,90 88,110" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- arrow pointing right onto left face -->
<line x1="5" y1="81" x2="28" y2="81" stroke="#3D3D3A" stroke-width="3" stroke-linecap="round"/>
<polygon points="34,81 22,74 22,88" fill="#3D3D3A"/>
</svg>

After

Width:  |  Height:  |  Size: 824 B

@ -0,0 +1,13 @@
<svg width="150" height="150" viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
<!-- front-left face -->
<polygon points="28,52 58,32 58,90 28,110" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- top face -->
<polygon points="28,52 58,32 95,32 65,52" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- right face highlighted, facing camera -->
<polygon points="65,52 95,32 95,90 65,110" fill="#FAC775" stroke="#854F0B" stroke-width="2"/>
<!-- screen indicator on front-left face for context -->
<rect x="34" y="60" width="16" height="14" rx="2" fill="none" stroke="#3D3D3A" stroke-width="1"/>
<!-- arrow pointing left onto right face -->
<line x1="145" y1="81" x2="122" y2="81" stroke="#3D3D3A" stroke-width="3" stroke-linecap="round"/>
<polygon points="116,81 128,74 128,88" fill="#3D3D3A"/>
</svg>

After

Width:  |  Height:  |  Size: 837 B

@ -0,0 +1,13 @@
<svg width="130" height="150" viewBox="0 0 130 150" xmlns="http://www.w3.org/2000/svg">
<!-- front face -->
<rect x="30" y="68" width="65" height="48" rx="4" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- screen indicator -->
<rect x="40" y="78" width="26" height="16" rx="2" fill="none" stroke="#3D3D3A" stroke-width="1.2"/>
<!-- top face (highlighted) -->
<polygon points="30,68 50,48 115,48 95,68" fill="#B5D4F4" stroke="#185FA5" stroke-width="2"/>
<!-- right side face -->
<polygon points="95,68 115,48 115,96 95,116" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- arrow pointing down onto top face -->
<line x1="73" y1="8" x2="73" y2="38" stroke="#3D3D3A" stroke-width="3" stroke-linecap="round"/>
<polygon points="73,44 66,32 80,32" fill="#3D3D3A"/>
</svg>

After

Width:  |  Height:  |  Size: 797 B

@ -19,9 +19,9 @@ import 'package:test_sa/new_views/pages/login_page.dart';
class _CachedResponse {
final http.Response response;
final DateTime timestamp;
_CachedResponse(this.response, this.timestamp);
bool isValid(Duration cacheDuration) {
return DateTime.now().difference(timestamp) < cacheDuration;
}
@ -41,26 +41,26 @@ class ApiManager {
User? user;
AssetGroup? assetGroup;
// OPTIMIZATION: HTTP Connection Pooling - Reuse connections instead of creating new ones
// This improves performance by 100-200ms on subsequent API calls
static final http.Client _httpClient = http.Client();
// OPTIMIZATION: Response Caching - Cache lookup/static data to reduce network calls
// This reduces API calls by 60% for frequently accessed but rarely changing data
static final Map<String, _CachedResponse> _cache = {};
static const Duration _defaultCacheDuration = Duration(hours: 1);
/// Clear all cached responses (call on logout or when needed)
void clearCache() {
_cache.clear();
}
/// Clear specific cache entry
void clearCacheEntry(String url) {
_cache.remove(_generateCacheKey(url));
}
/// Generate cache key from URL and user context
String _generateCacheKey(String url) {
// Include user and asset group in cache key to avoid cross-user pollution
@ -68,7 +68,7 @@ class ApiManager {
}
/// GET request with optional caching support
///
///
/// [url] - API endpoint URL
/// [headers] - Optional additional headers
/// [enableToastMessage] - Show toast message from API response
@ -86,12 +86,12 @@ class ApiManager {
headers ??= {};
headers.addAll(_headers);
Uri url0 = Uri.parse(url);
// OPTIMIZATION: Check cache first if enabled
if (useCache && !forceRefresh) {
final cacheKey = _generateCacheKey(url);
final cachedResponse = _cache[cacheKey];
if (cachedResponse != null && cachedResponse.isValid(cacheDuration ?? _defaultCacheDuration)) {
if (kDebugMode) {
print('📦 Cache HIT: $url');
@ -99,10 +99,10 @@ class ApiManager {
return cachedResponse.response;
}
}
// OPTIMIZATION: Use persistent HTTP client for connection pooling
http.Response response = await _httpClient.get(url0, headers: headers);
// OPTIMIZATION: Cache the response if caching is enabled
if (useCache && response.statusCode >= 200 && response.statusCode < 300) {
final cacheKey = _generateCacheKey(url);
@ -310,7 +310,7 @@ class ApiManager {
Uri url0 = Uri.parse(url);
// print(headers);
// log(json.encode(body));
// OPTIMIZATION: Use persistent HTTP client for connection pooling
var request = http.Request('PUT', url0);
request.body = json.encode(body);
@ -337,10 +337,9 @@ class ApiManager {
}
Future<http.Response> multiPart(String url, {Map<String, String>? headers, required Map<String, String> body, required List<Future<MultipartFile>> files}) async {
Map<String, String> headers0 = const {
'Content-Type': 'multipart/form-data',
};
Map<String, String> headers0 = _headers;
headers0["Content-Type"] = 'multipart/form-data';
headers ??= {};
headers.addAll(headers0);
@ -423,7 +422,7 @@ class ApiManager {
Navigator.pop(context);
Navigator.of(context).pushNamedAndRemoveUntil(LoginPage.routeName, (routes) => true);
}
/// Dispose HTTP client (call when app is closing if needed)
/// Note: In most cases, keep client alive for app lifetime for best performance
void dispose() {

@ -1,15 +1,14 @@
class URLs {
URLs._();
static const String appReleaseBuildNumber = "40";
static const String appReleaseBuildNumber = "49";
// static const host1 = "https://atomsm.hmg.com"; // production url
static const host1 = "https://atomsm.hmg.com"; // production url
// static const host1 = "https://atomsmdev.hmg.com"; // local DEV url
static const host1 = "https://atomsmuat.hmg.com"; // local UAT url
// static const host1 = "https://atomsmuat.hmg.com"; // local UAT url
// static final String _baseUrl = "$_host/mobile"; // host local UAT
// static final String _baseUrl = "$_host/v1/mobile"; // new V1 apis
static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis
static final String _baseUrl = "$_host/mobile"; // host local UAT
// static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis
// static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM
// static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module
// static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation
@ -85,13 +84,23 @@ class URLs {
static get getFaultDescription => "$_baseUrl/Asset/GetDefectForModelDefinition"; // get
static get getAssetById => "$_baseUrl/Asset/GetAssetById?assetId="; // get
static get getAssetColumnsMetadata => "$_baseUrl/Asset/columns/metadata"; // get asset export columns metadata
static get saveAssetImages => "$_baseUrl/Asset/SaveAssetImages"; // get
static get getModels => "$_baseUrl/ModelDefinition/GetModelDefinitionAsset"; // get ?client=2
static get getAllRequestsAndCount => "$_baseUrl/CallRequest/GetAllRequestsAndCount"; // get
static get updateUserContactInformation => "$_baseUrl/Account/UpdateContactInfo"; // get
static get searchPlanRecurrentTaskListSearch => "$_baseUrl/PlanRecurrentTasks/PlanRecurrentTaskListSearchDto"; // get
static get checkQRSameRoom => "$_baseUrl/PlanRecurrentTasks/CheckQRSameRoom"; // get
//
//One Time Task Urls cleaner module new flow urls ....
static get getTaskType => "$_baseUrl/OneTimeTasks/GetTaskTypes";
static get scanQRCode => "$_baseUrl/OneTimeTasks/ScanQRCode";
static get proceedRequest => "$_baseUrl/OneTimeTasks/Proceed";
static get completeRequest => "$_baseUrl/OneTimeTasks/Complete";
static get getRequestById => "$_baseUrl/OneTimeTasks/GetById";
//service request new flow urls.
static get nurseDashboardCountUrl => '$_baseUrl/ServiceRequest/GetDashboardNurseCount';
@ -176,6 +185,8 @@ class URLs {
static get uploadWorkOrderAttachmentsUrl => '$_baseUrl/ServiceRequest/UploadAttachmentsWorkOrder';
static get workOrderByAssetIdAutoComplete => '$_baseUrl/ServiceRequest/WorkOrderByAssetIdAutoComplete';
static get getArrivalVerificationTypeUrl => '$_baseUrl/ArrivalVerificationType/GetArrivalVerificationType';
static get sendOtpUrl => '$_baseUrl/SmsNotification/SendOTP/';
@ -387,6 +398,7 @@ class URLs {
static get getSiteContactInfo => "$_baseUrl/AssetGroupSiteContactInfo"; // add
static get getDepartmentBasedOnSite => "$_baseUrl/TRAFDataSource/GetDepartmentBasedOnSite"; // add
static get getCustomersByUserId => "$_baseUrl/TRAFDataSource/GetCustomersByUserId"; // add
// asset inventory Urls.
static get getAssetInventoryById => '$_baseUrl/AssetInventory/GetAssetInventoryById';

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart';
@ -157,14 +158,20 @@ class AllRequestsProvider extends ChangeNotifier {
list.add(7); // task mod
}
// switching asset inventory to users permissions
//TODO uncommit this to enable TRAF in production.
context.userProvider.modulesPermissionsList.forEach((module) {
if (module.module!.value! == 8 || module.module!.value! == 9) {
list.add(module.module!.value!);
}
});
// if (context.settingProvider.isUserFlowMedical && context.userProvider.isNurse) {
//Todo commit this when passing to production.
// context.userProvider.modulesPermissionsList.forEach((module) {
// if (module.module!.value! == 8) {
// list.add(module.module!.value!);
// }
// });
//
// if (context.settingProvider.isUserFlowMedical && !context.userProvider.isAssessor && (context.userProvider.isNurse || context.userProvider.isEngineer)) {
// list.add(9); //
// }
list.add(12);
@ -177,6 +184,10 @@ class AllRequestsProvider extends ChangeNotifier {
// list = [16];
list.add(16);
}
if ( context.userProvider.isHouseKeeper) {
list = [5];
}
return list;
}
@ -274,23 +285,27 @@ class AllRequestsProvider extends ChangeNotifier {
}
}
Future<bool> updateRecurrentWo({required int status}) async {
isLoading = true;
Future<Map<String, dynamic>> updateRecurrentWo({required int status, bool showToast = true}) async {
Response response;
try {
response = await ApiManager.instance.post(URLs.updateRecurrentPlanUrl, body: recurrentWoData!.toJson(status: status));
response = await ApiManager.instance.post(URLs.updateRecurrentPlanUrl, body: recurrentWoData!.toJson(status: status), showToast: showToast);
stateCode = response.statusCode;
isLoading = false;
notifyListeners();
if (stateCode == 200) {
return true;
return {'success': true, 'message': ''};
} else {
// Parse error message from response
try {
final responseBody = jsonDecode(response.body);
String errorMessage = responseBody['message'] ?? responseBody['title'] ?? 'Failed to update the task';
return {'success': false, 'message': errorMessage};
} catch (e) {
return {'success': false, 'message': 'Failed to update the task. Status code: $stateCode'};
}
}
return false;
} catch (error) {
isLoading = false;
stateCode = -1;
notifyListeners();
return false;
return {'success': false, 'message': error.toString()};
}
}

@ -9,6 +9,7 @@ import 'package:test_sa/controllers/api_routes/http_status_manger.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/device/asset_by_id_model.dart';
import 'package:test_sa/models/device/asset_search.dart';
import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/user.dart';
import 'package:test_sa/modules/asset_delivery_module/models/technical_inpection_asset_model.dart';
@ -152,9 +153,7 @@ class AssetProvider extends ChangeNotifier {
print(error);
throw (HttpStatusManger.getStatusMessage(status: -1, subtitle: appLocalizations));
}
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
Map<String, dynamic> assetData = json.decode(response.body)["data"];
_assetById = AssetByIdModel.fromJson(assetData);
return _assetById!;
@ -230,4 +229,32 @@ class AssetProvider extends ChangeNotifier {
return [];
}
}
Future<bool> uploadAssetImages(int assetId, TrainingImageData trainingImageData) async {
late Response response;
try {
List<Future<MultipartFile>> files = [];
files.add(MultipartFile.fromPath("LeftImage", trainingImageData.leftImage!.name!));
files.add(MultipartFile.fromPath("RightImage", trainingImageData.rightImage!.name!));
files.add(MultipartFile.fromPath("UpImage", trainingImageData.upImage!.name!));
files.add(MultipartFile.fromPath("DownImage", trainingImageData.downImage!.name!));
for (var i = 0; i < (trainingImageData.additionalImages?.length ?? 0); i++) {
files.add(
MultipartFile.fromPath('AdditionalImage', trainingImageData.additionalImages![i].name!),
);
}
response = await ApiManager.instance.multiPart(URLs.saveAssetImages + "?AssetId=$assetId", body: {}, files: files);
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
// Map<String, dynamic> assetData = json.decode(response.body)["data"];
// _assetById = AssetByIdModel.fromJson(assetData);
// return _assetById!;
return true;
}
return false;
} catch (error) {
return false;
}
}
}

@ -51,6 +51,7 @@ class UserProvider extends ChangeNotifier {
bool get isQualityUser => user!.type == UsersTypes.qualityUser;
bool get isCommissioningEngineer => user!.type == UsersTypes.commissioningEngineer;
bool get isHouseKeeper => user!.type == UsersTypes.houseKeeper;
VerifyOtpModel _verifyOtpModel = VerifyOtpModel();
SwipeTransaction _swipeTransactionModel = SwipeTransaction();

@ -205,6 +205,7 @@ class DashBoardProvider extends ChangeNotifier {
UsersTypes.qualityUser: URLs.qaAuditorCount,
UsersTypes.commissioningEngineer: URLs.commissioningEngineerCount,
UsersTypes.engineer: URLs.engineerDashboardCountUrl,
UsersTypes.houseKeeper: URLs.engineerDashboardCountUrl,
UsersTypes.assessor: URLs.engineerDashboardCountUrl,
UsersTypes.assessorTl: URLs.engineerDashboardCountUrl,
};
@ -328,7 +329,7 @@ class DashBoardProvider extends ChangeNotifier {
if (!statusTransaction.contains(5)) statusTransaction.add(5);
break;
case RequestTypeEnum.pendingAcknowledgment:
if (!statusTransaction.contains(3)) statusTransaction.add(3);
if (!statusTransaction.contains(4)) statusTransaction.add(4);
break;
case RequestTypeEnum.unassigned:
case RequestTypeEnum.highPriority:
@ -408,6 +409,7 @@ class DashBoardProvider extends ChangeNotifier {
case UsersTypes.engineer:
case UsersTypes.assessor:
case UsersTypes.assessorTl:
case UsersTypes.houseKeeper:
if (status == 0) {
return URLs.engineerDashboardUpcoming;
} else if (status == 1 && tabId == 1) {
@ -575,6 +577,17 @@ class CategoryTabs {
return tabs;
}
if (userType == UsersTypes.houseKeeper) {
// tabs.add(CategoryTabs(label: 'Upcoming', tag: 0, id: 0));
// tabs.add(CategoryTabs('Open', 1));
// tabs.add(CategoryTabs(label: 'UnAssigned', tag: 1, id: 1));
// tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 2));
// tabs.add(CategoryTabs(label: 'Completed', tag: 4, id: 3));
tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 2, requestType: RequestTypeEnum.inProgress, icon: "dashboard/inprogress_icon", iconColor: AppColor.primary10));
tabs.add(CategoryTabs(label: 'Completed', tag: 4, id: 3, requestType: RequestTypeEnum.completed, icon: "complete_request", iconColor: AppColor.completedColor));
return tabs;
}
if (userType == UsersTypes.qualityUser) {
tabs.add(CategoryTabs(label: 'Open', tag: 1, id: 2, requestType: RequestTypeEnum.open, icon: "dashboard/open_icon", iconColor: AppColor.openColor));
@ -592,7 +605,7 @@ class CategoryTabs {
tabs.add(CategoryTabs(label: 'Open', tag: 1, id: 2, requestType: RequestTypeEnum.open, icon: "dashboard/open_icon", iconColor: AppColor.openColor));
tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 3, requestType: RequestTypeEnum.inProgress, icon: "dashboard/inprogress_icon", iconColor: AppColor.primary10));
// tabs.add(CategoryTabs(label: 'Pending Acknowledgement', tag: 3, id: 1, requestType: RequestTypeEnum.pendingAcknowledgment, icon: "complete_request", iconColor: AppColor.overdueColor));
tabs.add(CategoryTabs(label: 'Acknowledgement', tag: 3, id: 1, requestType: RequestTypeEnum.pendingAcknowledgment, icon: "complete_request", iconColor: AppColor.overdueColor));
tabs.add(CategoryTabs(label: 'Acknowledgement', tag: 4, id: 1, requestType: RequestTypeEnum.pendingAcknowledgment, icon: "complete_request", iconColor: AppColor.overdueColor));
tabs.add(CategoryTabs(label: 'Canceled', tag: 6, id: 4, requestType: RequestTypeEnum.canceled, icon: "dashboard/unassigned", iconColor: AppColor.red30));
return tabs;
}

@ -12,7 +12,9 @@ import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/controllers/providers/settings/setting_provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/dashboard_latest/widgets/app_bar_widget.dart';
import 'package:test_sa/extensions/enum_extensions.dart';
import 'package:test_sa/dashboard_latest/widgets/progress_fragment.dart';
import 'package:test_sa/dashboard_latest/widgets/request_category_fragment_housekeeper.dart';
import 'package:test_sa/dashboard_latest/widgets/requests_fragment.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
@ -58,17 +60,22 @@ class _DashboardViewState extends State<DashboardView> {
_dashBoardProvider.setTabs(userType: userProvider.user!.type!, context: context);
_dashBoardProvider.getDashBoardCount(usersType: userProvider.user!.type!);
_dashBoardProvider.resetRequestDataList();
_dashBoardProvider.currentListIndex = null;
Provider.of<AllRequestsProvider>(context, listen: false)
..reset()
..getAllRequests(context);
// _dashBoardProvider.getRequestDetail(
// usersType: userProvider.user!.type!,
// status: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].tag,
// tabId: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].id,
// isHighPriority: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].isHighPriority,
// isOverdue: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex].isOverDue);
if (userProvider.isHouseKeeper) {
_dashBoardProvider.currentListIndex = 0;
_dashBoardProvider.getRequestDetail(
usersType: userProvider.user!.type!,
showLoader: true,
status: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex!].tag,
tabId: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex!].id,
// isAscending: _dashBoardProvider.isAscending,
);
} else {
_dashBoardProvider.currentListIndex = null;
Provider.of<AllRequestsProvider>(context, listen: false)
..reset()
..getAllRequests(context);
}
}
void getInitialData() {
@ -97,15 +104,86 @@ class _DashboardViewState extends State<DashboardView> {
void handleScroll() async {
_scrollController = ScrollController();
_scrollController.addListener(() async {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && !_dashBoardProvider.isDetailLoading) {
_dashBoardProvider.pageNum = _dashBoardProvider.pageNum + 1;
if (_dashBoardProvider.currentListIndex == null) {
} else {
_dashBoardProvider.getRequestDetail(
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
final allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
// Check which scenario is active and call appropriate pagination API
if (_dashBoardProvider.listIndex.isEmpty) {
// No category selected - paginate AllRequests
if (!allRequestsProvider.isAllLoading && allRequestsProvider.nextPage) {
allRequestsProvider.getAllRequests(context);
}
} else if (_dashBoardProvider.listIndex.length == 1 && _dashBoardProvider.isUnassignedTab(_dashBoardProvider.listIndex[0])) {
// Only UnAssigned tab selected - paginate with getRequestDetail
if (!_dashBoardProvider.isDetailLoading && _dashBoardProvider.nextPage) {
_dashBoardProvider.pageNum = _dashBoardProvider.pageNum + 1;
_dashBoardProvider.getRequestDetail(
usersType: user.type!,
showLoader: false,
status: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex!].tag,
tabId: _dashBoardProvider.tabs[_dashBoardProvider.currentListIndex!].id);
status: _dashBoardProvider.tabs[_dashBoardProvider.listIndex[0]].tag,
tabId: _dashBoardProvider.tabs[_dashBoardProvider.listIndex[0]].id,
);
}
} else {
// Multi-select categories - paginate with getDashboardMultifilterRequests
if (!allRequestsProvider.isMultiFilterLoading && allRequestsProvider.nextPage) {
// Build filter arrays based on selected tabs
List<int> statusTransaction = [];
List<int> priority = [];
List<int> displayData = [];
for (int index in _dashBoardProvider.listIndex) {
if (index >= 0 && index < _dashBoardProvider.tabs.length) {
CategoryTabs tab = _dashBoardProvider.tabs[index];
// High Priority
if (tab.isHighPriority) {
if (!priority.contains(1)) priority.add(1);
}
// Overdue
if (tab.isOverDue) {
if (!displayData.contains(1)) displayData.add(1);
}
// Status categories
if (!tab.isHighPriority && !tab.isOverDue && tab.requestType != RequestTypeEnum.unassigned) {
switch (tab.requestType) {
case RequestTypeEnum.open:
if (!statusTransaction.contains(1)) statusTransaction.add(1);
break;
case RequestTypeEnum.inProgress:
if (!statusTransaction.contains(2)) statusTransaction.add(2);
break;
case RequestTypeEnum.completed:
if (!statusTransaction.contains(4)) statusTransaction.add(4);
break;
case RequestTypeEnum.closed:
if (!statusTransaction.contains(4)) statusTransaction.add(4);
break;
case RequestTypeEnum.canceled:
if (!statusTransaction.contains(5)) statusTransaction.add(5);
break;
case RequestTypeEnum.pendingAcknowledgment:
if (!statusTransaction.contains(3)) statusTransaction.add(3);
break;
case RequestTypeEnum.unassigned:
case RequestTypeEnum.highPriority:
case RequestTypeEnum.overdue:
break;
}
}
}
}
allRequestsProvider.getDashboardMultifilterRequests(
context,
pagination: false,
statusTransaction: statusTransaction,
priority: priority,
displayData: displayData,
);
}
}
}
});
@ -145,7 +223,7 @@ class _DashboardViewState extends State<DashboardView> {
children: [
16.height, ProgressFragment(), 16.height,
// const RequestsFragment(),
const RequestCategoryFragment()
context.userProvider.isHouseKeeper ? const RequestCategoryFragmentHouseKeeper() : const RequestCategoryFragment()
],
),
),

@ -211,7 +211,7 @@ class _AppBarWidgetState extends State<AppBarWidget> with TickerProviderStateMix
[],
builder: (_, MenuController controller, Widget? child) {
return Container(
padding: const EdgeInsets.all(10),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: AppColor.assetGroupBackground(context),
@ -222,10 +222,13 @@ class _AppBarWidgetState extends State<AppBarWidget> with TickerProviderStateMix
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 16,
spacing: 8,
children: [
Text(settingProvider.assetGroup?.name ?? "", style: AppTextStyles.bodyText2.copyWith(color: AppColor.headingTextColor(context))),
Icon(Icons.keyboard_arrow_down, color: AppColor.headingTextColor(context)),
Text(
settingProvider.assetGroup?.name ?? "",
style: AppTextStyles.bodyText2.copyWith(color: AppColor.headingTextColor(context)),
),
Icon(Icons.keyboard_arrow_down, color: AppColor.headingTextColor(context), size: 20),
],
),
).onPress(() {
@ -240,7 +243,7 @@ class _AppBarWidgetState extends State<AppBarWidget> with TickerProviderStateMix
}),
),
),
16.width,
8.width,
SlideTransition(
position: _rightSlideAnimation,
child: FadeTransition(
@ -248,20 +251,31 @@ class _AppBarWidgetState extends State<AppBarWidget> with TickerProviderStateMix
child: Row(
children: [
if (context.settingProvider.isUserFlowMedical) ...[
("dashboard/chat").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0).onPress(() {
Navigator.of(context).push(CupertinoPageRoute(builder: (_) => UnReadChatList()));
}),
16.width,
IconButton(
icon: ("dashboard/chat").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
tooltip: "Notifications",
onPressed: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (_) => UnReadChatList()));
},
),
],
("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0).onPress(() {
Navigator.of(context).pushNamed(NotificationsPage.id);
}),
IconButton(
icon: ("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
tooltip: "Notifications",
onPressed: () {
Navigator.of(context).pushNamed(NotificationsPage.id);
},
),
],
),
),
),
],
).paddingOnly(start: 16, end: 16);
).paddingOnly(start: 16, end: 08);
}),
);
}

@ -226,12 +226,18 @@ class _ProgressFragmentState extends State<ProgressFragment> with SingleTickerPr
ChartData('Acknowledged', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.completeChartColor, "dashboard/completed_icon"),
];
} else if (_userProvider.isQualityUser) {
total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [
ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.openChartColor, "dashboard/open_icon"),
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.inProgressChartColor, "dashboard/inprogress_icon"),
ChartData('Closed', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.completeChartColor, "dashboard/completed_icon"),
];
} else if (_userProvider.isHouseKeeper) {
total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.inProgressChartColor, "dashboard/inprogress_icon"),
ChartData('Completed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.completeChartColor, "dashboard/completed_icon"),
];
} else {
total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [
@ -312,7 +318,7 @@ class _ProgressFragmentState extends State<ProgressFragment> with SingleTickerPr
),
).toShimmer(isShow: snapshot.isAllCountLoading, radius: 250, context: context),
),
).paddingOnly(top: 16,bottom: 16),
).paddingOnly(top: 16, bottom: 16),
),
Expanded(
flex: 2,
@ -340,7 +346,7 @@ class _ProgressFragmentState extends State<ProgressFragment> with SingleTickerPr
],
)
],
).paddingOnly(top: 4,bottom: 4),
).paddingOnly(top: 4, bottom: 4),
),
],
).expanded,

@ -7,6 +7,7 @@ import 'package:test_sa/dashboard_latest/widgets/request_category_list.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/enum_extensions.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart';
@ -112,10 +113,47 @@ class _RequestCategoryFragmentState extends State<RequestCategoryFragment> with
final user = Provider.of<UserProvider>(context, listen: false).user;
final settingProvider = Provider.of<SettingProvider>(context, listen: false);
// Check if user is FMS Engineer using SettingProvider
final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS;
// Check if user is houseKeeper
final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper;
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
// !isNurse && dashboardProvider.tabs.isNotEmpty && dashboardProvider.tabs[dashboardProvider.currentListIndex].tag == 0
// ? Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// context.translation.allWorkOrder.heading4(context),
// // Row(
// // mainAxisAlignment: MainAxisAlignment.spaceBetween,
// // children: [
// // context.translation.allWorkOrder.heading4(context),
// // GestureDetector(
// // onTap: () {
// // dashboardProvider.isShowDashBoardCalendar = !dashboardProvider.isShowDashBoardCalendar;
// // },
// // child: dashboardProvider.isShowDashBoardCalendar ? AppAsset.listIcon.toSvgAsset() : AppAsset.calendarIcon.toSvgAsset())
// // ],
// // ),
// dashboardProvider.isShowDashBoardCalendar
// ? Padding(
// padding: EdgeInsets.only(top: 12.toScreenHeight),
// child: const WeeklyCalendarFragment(),
// )
// : const SizedBox(),
// ],
// ).paddingOnly(start: 14, end: 12)
// : isHouseKeeper
// ? Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// 'All Tasks'.addTranslation.heading4(context),
// 'add_icon'.toSvgAsset(width: 20, height: 20).onPress(() {
// Navigator.of(context).pushNamed('/cleaner_search');
// }),
// ],
// ).paddingOnly(start: 16, end: 16)
// : context.translation.allWorkOrder.heading4(context).paddingOnly(start: 16, end: 16),
//
getTabs(
requestsProvider: dashboardProvider,
context: context,
@ -152,35 +190,51 @@ class _RequestCategoryFragmentState extends State<RequestCategoryFragment> with
// ],
// ).paddingOnly(start: 14, end: 12)
// :
Row(
spacing: 16,
children: [
Text(context.translation.allWorkOrder, style: AppTextStyles.heading4.copyWith(color: AppColor.headingTextColor(context))).expanded,
// todo commenting this code, for FM
// if (!isFMSEngineer)
// 'search'.toSvgAsset(width: 24, height: 24, color: AppColor.headingTextColor(context)).onPress(() {
// Navigator.of(context).pushNamed('/cleaner_search');
// }),
if (dashboardProvider.listIndex.isNotEmpty)
Text(
'Clear Filters',
style: AppTextStyles.bodyText2.copyWith(color: AppColor.red30),
).onPress(() {
dashboardProvider.resetRequestDataList();
dashboardProvider.currentListIndex = null;
Provider.of<AllRequestsProvider>(context, listen: false)
..reset()
..getAllRequests(context);
}),
Transform.flip(
flipY: isAscending,
child: 'dashboard/sort_descending'.toSvgAsset(width: 24, height: 24, color: AppColor.headingTextColor(context)).onPress(() {
_toggleSort(dashboardProvider, user?.type);
}),
)
],
).paddingOnly(start: 16, end: 16),
isHouseKeeper
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
'All Tasks'.addTranslation.heading4(context),
'add_icon'
.toSvgAsset(
width: 24,
height: 24,
)
.onPress(() {
Navigator.of(context).pushNamed('/cleaner_search');
}),
],
).paddingOnly(start: 16, end: 16)
: Row(
spacing: 16,
children: [
Text(context.translation.allWorkOrder, style: AppTextStyles.heading4.copyWith(color: AppColor.headingTextColor(context))).expanded,
// todo commenting this code, for FM
// if (!isFMSEngineer)
// 'search'.toSvgAsset(width: 24, height: 24, color: AppColor.headingTextColor(context)).onPress(() {
// Navigator.of(context).pushNamed('/cleaner_search');
// }),
if (dashboardProvider.listIndex.isNotEmpty)
Text(
'Clear Filters',
style: AppTextStyles.bodyText2.copyWith(color: AppColor.red30),
).onPress(() {
dashboardProvider.resetRequestDataList();
dashboardProvider.currentListIndex = null;
Provider.of<AllRequestsProvider>(context, listen: false)
..reset()
..getAllRequests(context);
}),
Transform.flip(
flipY: isAscending,
child: 'dashboard/sort_descending'.toSvgAsset(width: 24, height: 24, color: AppColor.headingTextColor(context)).onPress(() {
_toggleSort(dashboardProvider, user?.type);
}),
)
],
).paddingOnly(start: 16, end: 16),
),
),
@ -201,7 +255,7 @@ class _RequestCategoryFragmentState extends State<RequestCategoryFragment> with
snapshot.allRequestsAndCount?.requestsDetails ?? [],
snapshot.isAllLoading,
disableScroll: true,
key: ValueKey('request_list_'), // Key changes on every tab click
key: const ValueKey('request_list_'), // Key changes on every tab click
);
})
: dashboardProvider.listIndex.length == 1 && dashboardProvider.isUnassignedTab(dashboardProvider.listIndex[0])

@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/dashboard_latest/widgets/request_category_list.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/enum_extensions.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart';
import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/tab_button.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
/// Separate component for HouseKeeper role
/// HouseKeeper has simpler requirements:
/// - Single-select only (no multi-select)
/// - Uses getRequestDetail API only
/// - Default shows first category (In Progress)
class RequestCategoryFragmentHouseKeeper extends StatefulWidget {
const RequestCategoryFragmentHouseKeeper({Key? key}) : super(key: key);
@override
State<RequestCategoryFragmentHouseKeeper> createState() => _RequestCategoryFragmentHouseKeeperState();
}
class _RequestCategoryFragmentHouseKeeperState extends State<RequestCategoryFragmentHouseKeeper> with SingleTickerProviderStateMixin {
bool isAscending = false;
late AnimationController _animationController;
late Animation<double> _headerFadeAnimation;
late Animation<Offset> _headerSlideAnimation;
final ScrollController _scrollController = ScrollController();
// int _selectedIndex = 0; // Default first tab
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
// Header animation (appears first, faster)
_headerFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
),
);
_headerSlideAnimation = Tween<Offset>(
begin: const Offset(0.0, -0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeOutCubic),
),
);
_animationController.forward();
// Load first category data on init
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadCategoryData(0);
});
}
@override
void dispose() {
_animationController.dispose();
_scrollController.dispose();
super.dispose();
}
void _loadCategoryData(int index) {
final dashboardProvider = Provider.of<DashBoardProvider>(context, listen: false);
List<CategoryTabs> tabs = CategoryTabs.getTabs(userType: UsersTypes.houseKeeper, context: context);
if (index >= 0 && index < tabs.length) {
dashboardProvider.resetRequestDataList();
dashboardProvider.getRequestDetail(
usersType: UsersTypes.houseKeeper,
showLoader: true,
status: tabs[index].tag,
tabId: tabs[index].id,
isAscending: isAscending,
);
}
}
void _toggleSort(DashBoardProvider dashboardProvider) {
setState(() {
isAscending = !isAscending;
});
// Reload current category with new sort order
_loadCategoryData(dashboardProvider.currentListIndex!);
}
@override
Widget build(BuildContext context) {
return Consumer<DashBoardProvider>(builder: (context, dashboardProvider, child) {
List<CategoryTabs> tabs = CategoryTabs.getTabs(userType: UsersTypes.houseKeeper, context: context);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Tabs
_buildTabs(tabs, dashboardProvider),
// Header with sort
FadeTransition(
opacity: _headerFadeAnimation,
child: SlideTransition(
position: _headerSlideAnimation,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
'All Tasks'.addTranslation.heading4(context),
'add_icon'.toSvgAsset(width: 24, height: 24).onPress(() {
Navigator.of(context).pushNamed('/cleaner_search');
}),
// Transform.flip(
// flipY: isAscending,
// child: 'dashboard/sort_descending'.toSvgAsset(
// width: 24,
// height: 24,
// color: AppColor.headingTextColor(context),
// ).onPress(()=>_toggleSort(dashboardProvider)),
// )
],
).paddingOnly(start: 16, end: 16),
),
),
// Data list
dashboardProvider.isDetailLoading
? Column(
mainAxisSize: MainAxisSize.max,
children: List.generate(3, (index) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 10.toScreenHeight, horizontal: 0),
child: const SizedBox().toRequestShimmer(context, dashboardProvider.isDetailLoading),
);
}),
).paddingOnly(start: 16, end: 16, top: 16)
: (dashboardProvider.requestDetailList == null || dashboardProvider.requestDetailList?.data?.isEmpty == true)
? const NoDataFound().paddingOnly(top: 80).center
: RequestCategoryList(
dashboardProvider.requestDetailList?.data ?? <Data>[],
dashboardProvider.isDetailLoading,
dashboardProvider.requestDetailList?.totalRows ?? 0,
),
],
);
});
}
Widget _buildTabs(List<CategoryTabs> tabs, DashBoardProvider dashboardProvider) {
// Calculate item width
final screenWidth = MediaQuery.of(context).size.width;
final isTablet = context.isTablet();
final visibleItemsWithPeek = isTablet ? 4.5 : 3.5;
const spacing = 4.0;
const horizontalPadding = 16.0;
final availableWidth = screenWidth - (horizontalPadding * 2);
final fullItems = visibleItemsWithPeek.floor();
final totalSpacing = (fullItems - 1) * spacing + (spacing * 0.5);
final itemWidth = (availableWidth - totalSpacing) / visibleItemsWithPeek;
return SizedBox(
height: itemWidth + 16,
child: ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal,
separatorBuilder: (cxt, index) => const SizedBox(width: 4),
padding: const EdgeInsets.only(bottom: 16, left: horizontalPadding, right: horizontalPadding),
itemBuilder: (cxt, index) {
// Animation
final delay = index * 0.08;
final animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(
delay.clamp(0.0, 0.8),
(delay + 0.3).clamp(0.0, 1.0),
curve: Curves.easeOut,
),
),
);
final slideAnimation = Tween<Offset>(
begin: const Offset(-0.3, 0.0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(
delay.clamp(0.0, 0.8),
(delay + 0.3).clamp(0.0, 1.0),
curve: Curves.easeOutCubic,
),
),
);
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: slideAnimation,
child: SizedBox(
width: itemWidth,
child: RequestTypeButton(
label: tabs[index].label,
loading: dashboardProvider.isDetailLoading && dashboardProvider.currentListIndex == index,
isSelected: dashboardProvider.currentListIndex == index,
count: dashboardProvider.getStatusCount(tabs[index].requestType),
onPressed: () {
setState(() {
isAscending = false;
// _selectedIndex = index;
});
dashboardProvider.currentListIndex = index;
_loadCategoryData(index);
},
icon: tabs[index].icon,
iconColor: tabs[index].iconColor,
),
),
),
);
},
itemCount: tabs.length,
),
);
}
}

@ -4,6 +4,7 @@ import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart';
import 'package:test_sa/modules/asset_inventory_module/pages/inventory_session_item_view.dart';
import 'package:test_sa/modules/cleaner_module/widgets/one_time_task_item_view.dart';
import 'package:test_sa/modules/demo_module/demo_request_item_view.dart';
import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart';
import 'package:test_sa/modules/loan_module/pages/loan_equipment_item_view.dart';
@ -131,6 +132,8 @@ class _RequestCategoryListState extends State<RequestCategoryList> with SingleTi
return MedicalGasInspectionItemView(requestData: request);
case 15:
return DemoRequestItemView(requestData: request);
case 17:
return OneTimeTaskItemView(requestData: request);
default:
return Container(
height: 100,

@ -9,6 +9,8 @@ import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/swipe_module/dialoge/confirm_dialog.dart';
import 'package:test_sa/new_views/swipe_module/dialoge/info_dialog.dart';
import 'package:test_sa/views/full_screen_image_viewer.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import '../controllers/providers/settings/setting_provider.dart';
import '../l10n/app_localizations.dart';
@ -30,6 +32,15 @@ extension BuildContextExtension on BuildContext {
Future<T?> pushRoute<T>(Widget child) => Navigator.push<T>(this, Platform.isIOS ? CupertinoPageRoute<T>(builder: (context) => child) : MaterialPageRoute<T>(builder: (context) => child));
void fullScreenImageViewer(Widget child) {
Navigator.of(this).push(
CupertinoPageRoute(
fullscreenDialog: true,
builder: (_) => FullScreenImageViewer(child),
),
);
}
bool isTablet() {
final mediaQueryData = MediaQuery.of(this);
final double screenWidth = mediaQueryData.size.width;

@ -120,12 +120,13 @@ extension WidgetExtensions on Widget {
double padding = 12,
EdgeInsets? paddingObject,
EdgeInsets? margin,
double? width,
}) =>
withShadow
? Container(
padding: paddingObject ?? EdgeInsets.all(padding),
margin: margin,
width: double.infinity,
width: width ?? double.infinity,
decoration: ShapeDecoration(
color: backgroundColor ?? AppColor.background(context),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius), side: BorderSide(color: borderColor ?? AppColor.borderColor(context))),

@ -13,7 +13,7 @@ import 'package:nfc_manager/nfc_manager_ios.dart' show MiFareIos, Iso15693Ios;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/views/widgets/dialogs/loading_dialog.dart';
import 'package:test_sa/views/widgets/dialogs/loading_with_text_dialog.dart';
// ignore_for_file: avoid_annotating_with_dynamic
@ -103,7 +103,7 @@ class Utils {
_isLoadingVisible = false;
}
static void showUploadingDialog(BuildContext context) {
static void showLoadingWithTextDialog(BuildContext context, {String? message}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_isLoadingVisible = true;
@ -111,7 +111,7 @@ class Utils {
context: context,
barrierColor: Colors.black.withOpacity(0.5),
useRootNavigator: false,
builder: (BuildContext context) => const UploadingDialog(),
builder: (BuildContext context) => LoadingWithTextDialog(message: message),
).then((value) {
_isLoadingVisible = false;
});

@ -146,13 +146,13 @@ import 'modules/tm_module/device_transfer/device_search_screen.dart';
import 'modules/tm_module/tasks/create_task_view.dart';
import 'modules/tm_module/gas_refill/gas_refill_request_form.dart';
import 'providers/lookups/classification_lookup_provider.dart';
import 'providers/lookups/customer_lookup_provider.dart';
import 'providers/lookups/department_lookup_provider.dart';
import 'providers/lookups/request_type_lookup_provider.dart';
import 'providers/lookups/yes_no_lookup_provider.dart';
import 'providers/service_request_providers/loan_availability_provider.dart';
import 'providers/service_request_providers/reject_reason_provider.dart';
import 'modules/cleaner_module/pages/cleaner_search_form_view.dart';
import 'modules/cleaner_module/pages/cleaner_search_list_view.dart';
import 'modules/cleaner_module/provider/cleaner_provider.dart';
// class MyHttpOverrides extends HttpOverrides {
@ -245,6 +245,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => RequestTypeLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => YesNoLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => DepartmentLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => CustomerLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => EndUserStatusLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => EndUserRejectionReasonLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => AttachmentTypeLookupProvider(), lazy: true),
@ -438,7 +439,6 @@ class MyApp extends StatelessWidget {
CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(),
CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(),
CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(),
CleanerSearchListView.routeName: (_) => const CleanerSearchListView(),
ApplicationDemoRequestFormScreen.routeName: (_) => const ApplicationDemoRequestFormScreen(),
},
),

@ -134,36 +134,47 @@ class RequestsDetails {
int? transactionType;
int? numberOfAssets;
int? numberOfSites;
String? siteName;
String? buildingName;
String? departmentName;
String? floorName;
String? roomName;
RequestsDetails(
{this.id,
this.nameOfType,
this.priority,
this.status,
this.statusValue,
this.assetName,
this.assetNo,
this.assetSN,
this.model,
this.supplier,
this.manufacturer,
this.requestType,
this.requestNo,
this.numberOfAssets,
this.numberOfSites,
this.sessionType,
this.gasType,
this.site,
this.statusReceiver,
this.assetTransferFrom,
this.assetTransferTo,
this.code,
this.siteTransferFrom,
this.siteTransferTo,
this.poInternal,
this.poExternal,
this.date,
this.transactionType});
RequestsDetails({
this.id,
this.nameOfType,
this.priority,
this.status,
this.statusValue,
this.assetName,
this.assetNo,
this.assetSN,
this.model,
this.supplier,
this.manufacturer,
this.requestType,
this.requestNo,
this.numberOfAssets,
this.numberOfSites,
this.sessionType,
this.gasType,
this.site,
this.statusReceiver,
this.assetTransferFrom,
this.assetTransferTo,
this.code,
this.siteTransferFrom,
this.siteTransferTo,
this.poInternal,
this.poExternal,
this.date,
this.transactionType,
this.siteName,
this.buildingName,
this.departmentName,
this.floorName,
this.roomName,
});
RequestsDetails.fromJson(Map<String, dynamic> json) {
id = json['id'];
@ -194,6 +205,11 @@ class RequestsDetails {
siteTransferFrom = json['siteTransferFrom'];
siteTransferTo = json['siteTransferTo'];
transactionType = json['transactionType'];
siteName = json['siteName'];
buildingName = json['buildingName'];
departmentName = json['departmentName'];
floorName = json['floorName'];
roomName = json['roomName'];
}
Map<String, dynamic> toJson() {
@ -224,6 +240,11 @@ class RequestsDetails {
data['siteTransferFrom'] = siteTransferFrom;
data['siteTransferTo'] = siteTransferTo;
data['transactionType'] = transactionType;
data['siteName'] = siteName;
data['buildingName'] = buildingName;
data['departmentName'] = departmentName;
data['floorName'] = floorName;
data['roomName'] = roomName;
return data;
}
}

@ -65,6 +65,7 @@ class Asset {
this.comment,
this.tagCode,
this.assetPhoto,
this.hasTrainingImageData,
});
Asset.fromJson(dynamic json) {
@ -122,6 +123,7 @@ class Asset {
comment = json['comment'];
tagCode = json['tagCode'];
assetPhoto = json['assetPhoto'];
hasTrainingImageData = json['hasTrainingImageData'];
}
num? id; // Now nullable
@ -177,6 +179,7 @@ class Asset {
String? comment; // Now nullable
String? tagCode; // Now nullable
String? assetPhoto;
bool? hasTrainingImageData;
Asset copyWith({
num? id,
@ -232,6 +235,7 @@ class Asset {
String? comment,
String? tagCode,
String? assetPhoto,
bool? hasTrainingImageData,
}) =>
Asset(
id: id ?? this.id,
@ -287,6 +291,7 @@ class Asset {
comment: comment ?? this.comment,
tagCode: tagCode ?? this.tagCode,
assetPhoto: assetPhoto ?? this.assetPhoto,
hasTrainingImageData: hasTrainingImageData ?? this.hasTrainingImageData,
);
Map<String, dynamic> toJson() {
@ -381,6 +386,7 @@ class Asset {
map['comment'] = comment;
map['tagCode'] = tagCode;
map['assetPhoto'] = assetPhoto;
map['hasTrainingImageData'] = hasTrainingImageData;
return map;
}
}

@ -1,4 +1,5 @@
import 'package:test_sa/models/device/technical_guidance_book.dart';
import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/lookup.dart';
class AssetByIdModel {
@ -13,12 +14,12 @@ class AssetByIdModel {
AssetInfo? oldAsset;
AssetReplace? isParent;
AssetInfo? parentAsset;
AssetReplace? assetType;
Lookup? assetType;
DemoRequest? demoRequest;
String? missionCritical;
Site? site;
AssetReplace? building;
AssetReplace? floor;
Lookup? building;
Lookup? floor;
Department? department;
Lookup? room;
num? testsDay;
@ -54,14 +55,17 @@ class AssetByIdModel {
String? comment;
bool? isEnabled;
String? tagCode;
List<String>? assetAttachments;
List<AssetAttachment>? assetAttachments;
String? retirementTypeName;
String? retirementStatusName;
String? retirementDate;
AssetGroup? assetGroup;
String? assetPhoto;
String? modelPhoto;
String? nextPMDate;
String? lastPMDate;
bool? hasTrainingImageData;
TrainingImageData? trainingImageData;
AssetByIdModel(
{this.multiAssets,
@ -122,10 +126,15 @@ class AssetByIdModel {
this.retirementDate,
this.assetGroup,
this.assetPhoto,
this.modelPhoto,
this.nextPMDate,
this.lastPMDate});
this.lastPMDate,
this.trainingImageData,
this.hasTrainingImageData});
AssetByIdModel.fromJson(Map<String, dynamic> json) {
print("assetModel!.fromJson:${json['hasTrainingImageData']}");
if (json['multiAssets'] != null) {
multiAssets = <MultiAssets>[];
json['multiAssets'].forEach((v) {
@ -142,12 +151,12 @@ class AssetByIdModel {
oldAsset = json['oldAsset'] != null ? AssetInfo.fromJson(json['oldAsset']) : null;
isParent = json['isParent'] != null ? AssetReplace.fromJson(json['isParent']) : null;
parentAsset = json['parentAsset'] != null ? AssetInfo.fromJson(json['parentAsset']) : null;
assetType = json['assetType'] != null ? AssetReplace.fromJson(json['assetType']) : null;
assetType = json['assetType'] != null ? Lookup.fromJson(json['assetType']) : null;
demoRequest = json['demoRequest'] != null ? DemoRequest.fromJson(json['demoRequest']) : null;
missionCritical = json['missionCritical'];
site = json['site'] != null ? Site.fromJson(json['site']) : null;
building = json['building'] != null ? AssetReplace.fromJson(json['building']) : null;
floor = json['floor'] != null ? AssetReplace.fromJson(json['floor']) : null;
building = json['building'] != null ? Lookup.fromJson(json['building']) : null;
floor = json['floor'] != null ? Lookup.fromJson(json['floor']) : null;
department = json['department'] != null ? Department.fromJson(json['department']) : null;
room = json['room'] != null ? Lookup.fromJson(json['room']) : null;
testsDay = json['testsDay'];
@ -189,9 +198,9 @@ class AssetByIdModel {
isEnabled = json['isEnabled'];
tagCode = json['tagCode'];
if (json['assetAttachments'] != null) {
assetAttachments = <String>[];
assetAttachments = <AssetAttachment>[];
json['assetAttachments'].forEach((v) {
assetAttachments!.add(v);
assetAttachments!.add(AssetAttachment.fromJson(v));
});
}
retirementTypeName = json['retirementTypeName'];
@ -199,8 +208,11 @@ class AssetByIdModel {
retirementDate = json['retirementDate'];
assetGroup = json['assetGroup'] != null ? AssetGroup.fromJson(json['assetGroup']) : null;
assetPhoto = json['assetPhoto'];
modelPhoto = json['modelPhoto'];
nextPMDate = json['nextPMDate'];
lastPMDate = json['lastPMDate'];
hasTrainingImageData = json['hasTrainingImageData'];
trainingImageData = json['trainingImageData'] != null ? TrainingImageData.fromJson(json['trainingImageData']) : null;
}
Map<String, dynamic> toJson() {
@ -295,8 +307,11 @@ class AssetByIdModel {
data['assetGroup'] = assetGroup?.toJson();
}
data['assetPhoto'] = assetPhoto;
data['modelPhoto'] = modelPhoto;
data['nextPMDate'] = nextPMDate;
data['lastPMDate'] = lastPMDate;
data['hasTrainingImageData'] = hasTrainingImageData;
data['trainingImageData'] = trainingImageData?.toJson();
return data;
}
}
@ -323,6 +338,31 @@ class MultiAssets {
}
}
class AssetAttachment {
int? id;
String? attachmentName;
String? attachmentURL;
String? originalName;
AssetAttachment({this.id, this.attachmentName, this.attachmentURL, this.originalName});
AssetAttachment.fromJson(Map<String, dynamic> json) {
id = json['id'];
attachmentName = json['attachmentName'];
attachmentURL = json['attachmentURL'];
originalName = json['originalName'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['attachmentName'] = attachmentName;
data['attachmentURL'] = attachmentURL;
data['originalName'] = originalName;
return data;
}
}
class ModelDefinition {
int? id;
String? assetName;
@ -546,18 +586,50 @@ class Floors {
class Departments {
int? id;
String? name;
List<Rooms>? rooms;
Departments({this.id, this.name});
Departments.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
if (json['departments'] != null) {
rooms = <Rooms>[];
json['rooms'].forEach((v) {
rooms!.add(Rooms.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['name'] = name;
if (rooms != null) {
data['rooms'] = rooms!.map((v) => v.toJson()).toList();
}
return data;
}
}
class Rooms {
int? id;
String? name;
int? value;
Rooms({this.id, this.name, this.value});
Rooms.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
value = json['value'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['name'] = name;
data['value'] = value;
return data;
}
}
@ -567,14 +639,16 @@ class Department {
String? departmentName;
String? departmentCode;
String? ntCode;
num? operationalHours;
Department({this.id, this.departmentName, this.departmentCode, this.ntCode});
Department({this.id, this.departmentName, this.departmentCode, this.ntCode, this.operationalHours});
Department.fromJson(Map<String, dynamic> json) {
id = json['id'];
departmentName = json['departmentName'];
departmentCode = json['departmentCode'];
ntCode = json['ntCode'];
operationalHours = json['operationalHours'];
}
Map<String, dynamic> toJson() {
@ -583,6 +657,7 @@ class Department {
data['departmentName'] = departmentName;
data['departmentCode'] = departmentCode;
data['ntCode'] = ntCode;
data['operationalHours'] = operationalHours;
return data;
}
}
@ -725,3 +800,37 @@ class DemoRequest {
return data;
}
}
class TrainingImageData {
GenericAttachmentModel? leftImage;
GenericAttachmentModel? rightImage;
GenericAttachmentModel? upImage;
GenericAttachmentModel? downImage;
List<GenericAttachmentModel>? additionalImages;
TrainingImageData({this.leftImage, this.rightImage, this.upImage, this.downImage, this.additionalImages = const []});
TrainingImageData.fromJson(Map<String, dynamic> json) {
leftImage = json['leftImage'] == null ? null : GenericAttachmentModel.fromJson(json['leftImage'], convertToLink: true);
rightImage = json['rightImage'] == null ? null : GenericAttachmentModel.fromJson(json['rightImage'], convertToLink: true);
upImage = json['upImage'] == null ? null : GenericAttachmentModel.fromJson(json['upImage'], convertToLink: true);
downImage = json['downImage'] == null ? null : GenericAttachmentModel.fromJson(json['downImage'], convertToLink: true);
additionalImages = [];
if (json['additionalImages'] != null) {
json['additionalImages'].forEach((v) {
additionalImages!.add(GenericAttachmentModel.fromJson(v, convertToLink: true));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['leftImage'] = leftImage?.toJson();
data['rightImage'] = rightImage?.toJson();
data['upImage'] = upImage?.toJson();
data['downImage'] = downImage?.toJson();
data['additionalImages'] = additionalImages?.map((v) => v.toJson()).toList() ?? [];
return data;
}
}

@ -5,5 +5,6 @@ enum UsersTypes {
assessor,
assessorTl,
qualityUser,
commissioningEngineer
commissioningEngineer,
houseKeeper
}

@ -1,9 +1,10 @@
import 'dart:io';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/lookup.dart';
class GenericAttachmentModel {
GenericAttachmentModel({this.id, this.name, this.originalName, this.createdBy,this.documentTypeId});
GenericAttachmentModel({this.id, this.name, this.originalName, this.createdBy, this.documentTypeId});
int? id;
String? name;
@ -11,11 +12,14 @@ class GenericAttachmentModel {
String? originalName;
Lookup? documentTypeId;
GenericAttachmentModel.fromJson(Map<String, dynamic> json) {
GenericAttachmentModel.fromJson(Map<String, dynamic> json, {bool convertToLink = false}) {
id = json['id'];
name = json['name'];
name = json['name'] ?? json['imageName'];
createdBy = json['createdBy'];
originalName = json['originalName'];
originalName = json['originalName'] ?? json['imageOriginalName'];
if (convertToLink) {
name = URLs.getFileUrl(name);
}
}
Map<String, dynamic> toJson() {

@ -63,6 +63,11 @@ class Data {
int? numberOfSites;
int? numberOfAssets;
String? nameOfType;
String? siteName;
String? buildingName;
String? floorName;
String? departmentName;
String? roomName;
Data(
{this.id,
@ -80,10 +85,15 @@ class Data {
this.assetNumber,
this.requestTypeName,
this.requestNo,
this.requestType,
this.poExternal,
this.poInternal,
this.transactionNo});
this.requestType,
this.poExternal,
this.poInternal,
this.transactionNo,
this.siteName,
this.departmentName,
this.buildingName,
this.floorName,
this.roomName});
Data.fromJson(Map<String, dynamic> json) {
id = json['id'];
@ -106,6 +116,11 @@ class Data {
poInternal = json['poInternal'];
poExternal = json['poExternal'];
nameOfType = json['nameOfType'];
siteName = json['siteName'];
departmentName = json['departmentName'];
buildingName = json['buildingName'];
floorName = json['floorName'];
roomName = json['roomName'];
}
Map<String, dynamic> toJson() {
@ -127,6 +142,11 @@ class Data {
data['numberOfAssets'] = numberOfAssets;
data['numberOfSites'] = numberOfSites;
data['nameOfType'] = nameOfType;
data['siteName'] = siteName;
data['buildingName'] = buildingName;
data['departmentName'] = departmentName;
data['floorName'] = floorName;
data['roomName'] = roomName;
return data;
}
}

@ -0,0 +1,62 @@
import '../base.dart';
class TrafCustomer extends Base {
int? customerId;
int? langId;
String? nameValue;
String? customerName;
String? languageName;
int? id;
String? createdBy;
String? createdDate;
String? modifiedBy;
String? modifiedDate;
TrafCustomer({
this.customerId,
this.langId,
this.nameValue,
this.customerName,
this.languageName,
this.id,
this.createdBy,
this.createdDate,
this.modifiedBy,
this.modifiedDate,
}) : super(identifier: id?.toString() ?? '', name: nameValue ?? customerName);
factory TrafCustomer.fromJson(Map<String, dynamic> json) {
return TrafCustomer(
customerId: json['customerId'],
langId: json['langId'],
nameValue: json['nameValue'],
customerName: json['customerName'],
languageName: json['languageName'],
id: json['id'],
createdBy: json['createdBy'],
createdDate: json['createdDate'],
modifiedBy: json['modifiedBy'],
modifiedDate: json['modifiedDate'],
);
}
Map<String, dynamic> toJson() {
return {
'customerId': customerId,
'langId': langId,
'nameValue': nameValue,
'customerName': customerName,
'languageName': languageName,
'id': id,
'createdBy': createdBy,
'createdDate': createdDate,
'modifiedBy': modifiedBy,
'modifiedDate': modifiedDate,
};
}
@override
String toString() {
return nameValue ?? customerName ?? '';
}
}

@ -3,9 +3,9 @@ import 'package:test_sa/models/new_models/room_model.dart';
import '../base.dart';
class TrafDepartment extends Base {
TrafDepartment({
this.departmentName, this.departmentCode, this.ntCode, this.costCenterNumber, this.costCenterName, this.name, this.id, this.createdBy, this.createdDate, this.modifiedBy, this.modifiedDate
}) : super(identifier: id?.toString() ?? '', name: departmentName); // Handle potential null id
TrafDepartment(
{this.departmentName, this.departmentCode, this.ntCode, this.costCenterNumber, this.costCenterName, this.name, this.id, this.createdBy, this.createdDate, this.modifiedBy, this.modifiedDate})
: super(identifier: id?.toString() ?? '', name: departmentName); // Handle potential null id
TrafDepartment.fromJson(Map<String, dynamic> json) {
departmentName = json['departmentName'] ?? json['name'];
@ -15,6 +15,7 @@ class TrafDepartment extends Base {
costCenterName = json['costCenterName'];
name = json['name'];
id = json['id'];
identifier = id?.toString() ?? '';
createdBy = json['createdBy'];
createdDate = json['createdDate'];
modifiedBy = json['modifiedBy'];
@ -33,24 +34,22 @@ class TrafDepartment extends Base {
String? modifiedBy;
String? modifiedDate;
// TrafDepartment copyWith({
// num? id, // Parameters are now nullable
// String? departmentName,
// String? departmentCode,
// String? departmentId,
// String? ntCode,
// List<Rooms>? rooms,
// }) =>
// TrafDepartment(
// id: id ?? this.id,
// departmentName: departmentName ?? this.departmentName,
// departmentCode: departmentCode ?? this.departmentCode,
// departmentId: departmentId ?? this.departmentId,
// ntCode: ntCode ?? this.ntCode,
// rooms: rooms ?? this.rooms,
// );
// TrafDepartment copyWith({
// num? id, // Parameters are now nullable
// String? departmentName,
// String? departmentCode,
// String? departmentId,
// String? ntCode,
// List<Rooms>? rooms,
// }) =>
// TrafDepartment(
// id: id ?? this.id,
// departmentName: departmentName ?? this.departmentName,
// departmentCode: departmentCode ?? this.departmentCode,
// departmentId: departmentId ?? this.departmentId,
// ntCode: ntCode ?? this.ntCode,
// rooms: rooms ?? this.rooms,
// );
}
// class TrafDepartment extends Base {

@ -304,17 +304,20 @@ class WorkOrderAsset {
required this.id,
required this.assetNumber,
required this.assetSerialNo,
required this.hasTrainingImageData,
});
int? id;
String? assetNumber;
String? assetSerialNo;
bool? hasTrainingImageData;
factory WorkOrderAsset.fromJson(Map<String, dynamic> json) {
return WorkOrderAsset(
id: json["id"],
assetNumber: json["assetNumber"],
assetSerialNo: json["assetSerialNo"],
hasTrainingImageData: json["hasTrainingImageData"],
);
}
@ -322,6 +325,7 @@ class WorkOrderAsset {
"id": id,
"assetNumber": assetNumber,
"assetSerialNo": assetSerialNo,
"hasTrainingImageData": hasTrainingImageData,
};
}

@ -119,6 +119,8 @@ class User {
return UsersTypes.qualityUser;
case "R-201": // Quality User
return UsersTypes.commissioningEngineer;
case "R-204": // Quality User
return UsersTypes.houseKeeper;
default:
return null;
}

@ -0,0 +1,11 @@
class WoByAssetIdModel {
int? id;
String? workOrderNo;
WoByAssetIdModel({this.id, this.workOrderNo});
WoByAssetIdModel.fromJson(dynamic json) {
id = json['id'];
workOrderNo = json['workOrderNo'];
}
}

@ -4,6 +4,7 @@ import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/asset_delivery_module/models/delivery_line_model.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
class AssetDeliveryLineCard extends StatelessWidget {
DeliveryLineModel deliveryLineModel;
@ -28,59 +29,27 @@ class AssetDeliveryLineCard extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Item Category: ${deliveryLineModel.itemCategory ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
InfoTextWidget(label: "Item Category", value: deliveryLineModel.itemCategory ?? '-'),
if (!isViewOnly)
'edit_icon'.toSvgAsset().onPress(() {
'edit_icon'.toSvgAsset(width: 24, height: 24, color: AppColor.icon2Color(context)).onPress(() {
if (editPress != null) {
editPress!();
}
}),
],
),
Text(
'Item No: ${deliveryLineModel.itemNumber ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
Text(
'Item Description: ${deliveryLineModel.itemDescription ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
Text(
'Quantity: ${deliveryLineModel.qty ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
if(!isTechnical)...[
Text(
'Remaining Quantity: ${deliveryLineModel.remainingQty ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
Text(
'Quantity of this Delivery: ${deliveryLineModel.usingQtyInThisDelivery ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
InfoTextWidget(label: "Item No", value: deliveryLineModel.itemNumber ?? '-'),
InfoTextWidget(label: "Item Description", value: deliveryLineModel.itemDescription ?? '-'),
InfoTextWidget(label: "Quantity", value: deliveryLineModel.qty?.toString() ?? '-'),
if (!isTechnical) ...[
InfoTextWidget(label: "Remaining Quantity", value: deliveryLineModel.remainingQty?.toString() ?? '-'),
InfoTextWidget(label: "Quantity of this Delivery", value: deliveryLineModel.usingQtyInThisDelivery?.toString() ?? '-'),
],
Text(
'Measure Unit: ${deliveryLineModel.unitOfMeasure ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
Text(
'Unit price: ${deliveryLineModel.unitPrice ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
Text(
'Line Type: ${deliveryLineModel.lineType ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
Text(
'Total: ${deliveryLineModel.total ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
InfoTextWidget(label: "Measure Unit", value: deliveryLineModel.unitOfMeasure?.toString() ?? '-'),
InfoTextWidget(label: "Unit price", value: deliveryLineModel.unitPrice?.toString() ?? '-'),
InfoTextWidget(label: "Line Type", value: deliveryLineModel.lineType?.toString() ?? '-'),
InfoTextWidget(label: "Total", value: deliveryLineModel.total?.toString() ?? '-'),
],
).toShadowContainer(context, borderRadius: 20, padding: 12);
).toShadowContainer(context);
}
}

@ -101,7 +101,7 @@ class _AssetDeliveryPageState extends State<AssetDeliveryPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
requestDetailCard(context).toShadowContainer(context, borderRadius: 20, padding: 0),
requestDetailCard(context).toShadowContainer(context),
16.height,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -160,7 +160,7 @@ class _AssetDeliveryPageState extends State<AssetDeliveryPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget('PO Details'.addTranslation),
InfoTextWidget(label: 'PO No. Internal'.addTranslation, value: widget.poInternal ?? '-'),
InfoTextWidget(label: 'PO No. Internal'.addTranslation, value: dataModel?.poNumber?.toString() ?? '-'),
InfoTextWidget(label: 'Client Name'.addTranslation, value: dataModel?.clientName ?? '-'),
// Row(
// mainAxisSize: MainAxisSize.min,
@ -193,7 +193,7 @@ class _AssetDeliveryPageState extends State<AssetDeliveryPage> {
InfoTextWidget(label: 'Total'.addTranslation, value: dataModel?.total != null ? dataModel!.total.toString() : '-'),
InfoTextWidget(label: 'Total Invoice'.addTranslation, value: dataModel?.totalInvoice != null ? dataModel!.totalInvoice.toString() : '-'),
],
).paddingAll(16);
);
}
String getPOExternal() {
@ -218,7 +218,7 @@ class _AssetDeliveryPageState extends State<AssetDeliveryPage> {
? const NoDataFound().center
: ListView.separated(
itemCount: assetDeliveryTableList.length,
separatorBuilder: (_, __) => 8.height,
separatorBuilder: (_, __) => 12.height,
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),

@ -7,6 +7,7 @@ import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/asset_delivery_module/models/asset_delivery_data_model.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class AssetDeliveryTableCard extends StatelessWidget {
@ -54,24 +55,11 @@ class AssetDeliveryTableCard extends StatelessWidget {
// ],
// ),
8.height,
Text(
'Delivery Number: ${deliveryTableModel.number ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
Text(
'Delivery Amount: ${deliveryTableModel.deliveryAmount ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
Text(
'Invoice Amount: ${deliveryTableModel.invoiceAmount ?? '-'}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
Align(
alignment: Alignment.centerRight,
child: Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14),
),
InfoTextWidget(label: "Delivery Number", value: deliveryTableModel.number ?? '-'),
InfoTextWidget(label: "Delivery Amount", value: deliveryTableModel.deliveryAmount?.toString() ?? '-'),
InfoTextWidget(label: "Invoice Amount", value: deliveryTableModel.invoiceAmount?.toString() ?? '-', showArrow: true),
],
).toShadowContainer(context, borderRadius: 20).onPress(() {
).toShadowContainer(context).onPress(() {
onCardTap();
});
}

@ -18,6 +18,7 @@ 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/views/widgets/item_views/info_text_widget.dart';
class CostCenterFormView extends StatefulWidget {
final List<CostCenterModel>? costCenterList;
@ -113,44 +114,53 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
),
body: Consumer<AssetDeliveryProvider>(builder: (context, provider, child) {
return provider.isLoading
? CircularProgressIndicator(color: AppColor.loadingColor(context)).center
? CircularProgressIndicator(color: AppColor.loadingColor(context)).center
: Column(
children: [
itemDetails(),
selectionButton(),
_selectedType == CostCenterType.costCenter ? _buildCostCenterList() : _buildSerialNumberList(),
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
buttonColor: AppColor.primary10,
label: 'Save'.addTranslation,
maxWidth: true,
onPressed: () async {
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
await assetDeliveryProvider
.saveCostCenterToLineDeliveryInspection(tableItemId: widget.tableItemId, lineId: widget.lineId, list: formModel.costCenters, serialNoList: formModel.serialNos)
.then((status) async {
Navigator.pop(context);
if (status) {
Navigator.pop(context);
assetDeliveryProvider.getDeliveryLinesListById(
itemId: widget.tableItemId,
);
}
});
}),
),
],
);
children: [
ListView(
padding: const EdgeInsets.all(16),
children: [
itemDetails(),
12.height,
selectionButton(),
12.height,
_selectedType == CostCenterType.costCenter ? _buildCostCenterList() : _buildSerialNumberList(),
],
).expanded,
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
buttonColor: AppColor.primary10,
label: 'Save'.addTranslation,
maxWidth: true,
onPressed: () async {
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
await assetDeliveryProvider
.saveCostCenterToLineDeliveryInspection(tableItemId: widget.tableItemId, lineId: widget.lineId, list: formModel.costCenters, serialNoList: formModel.serialNos)
.then((status) async {
Navigator.pop(context);
if (status) {
Navigator.pop(context);
assetDeliveryProvider.getDeliveryLinesListById(
itemId: widget.tableItemId,
);
}
});
}),
),
],
);
}),
);
}
Widget _buildCostCenterList() {
return ListView.builder(
return ListView.separated(
itemCount: formModel.costCenters!.length + 1,
shrinkWrap: true,
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16, top: 8),
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => 12.height,
itemBuilder: (context, index) {
if (index == formModel.costCenters!.length) {
return AppFilledButton(
@ -184,10 +194,12 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
}
Widget _buildSerialNumberList() {
return ListView.builder(
return ListView.separated(
itemCount: formModel.serialNos!.length + 1,
shrinkWrap: true,
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16, top: 8),
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => 12.height,
itemBuilder: (context, index) {
if (index == formModel.serialNos!.length) {
return AppFilledButton(
@ -217,36 +229,33 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
}
Widget selectionButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Expanded(
child: _radioItem(
value: CostCenterType.costCenter,
groupValue: _selectedType,
title: 'Cost Center',
onTap: () {
setState(() {
_selectedType = CostCenterType.costCenter;
});
},
),
return Row(
children: [
Expanded(
child: _radioItem(
value: CostCenterType.costCenter,
groupValue: _selectedType,
title: 'Cost Center',
onTap: () {
setState(() {
_selectedType = CostCenterType.costCenter;
});
},
),
Expanded(
child: _radioItem(
value: CostCenterType.serialNumber,
groupValue: _selectedType,
title: 'Serial Number',
onTap: () {
setState(() {
_selectedType = CostCenterType.serialNumber;
});
},
),
),
Expanded(
child: _radioItem(
value: CostCenterType.serialNumber,
groupValue: _selectedType,
title: 'Serial Number',
onTap: () {
setState(() {
_selectedType = CostCenterType.serialNumber;
});
},
),
],
),
),
],
);
}
@ -263,6 +272,13 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
children: [
Radio<CostCenterType>(
value: value,
activeColor: AppColor.primary10,
fillColor: WidgetStateColor.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColor.primary10; // Thumb color when selected
}
return AppColor.icon2Color(context); // Thumb color when unselected (grey)
}),
groupValue: groupValue,
onChanged: (_) => onTap(),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
@ -279,34 +295,12 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Text(
'Item No'.addTranslation,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10),
),
8.height,
Text(
widget.itemNo ?? '-',
style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120),
),
8.height,
Text(
'Item Description'.addTranslation,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10),
),
8.height,
Text(
widget.itemDescription ?? '-',
style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120),
),
InfoTextWidget(label: "Item No", value: "\n${widget.itemNo ?? '-'}"),
InfoTextWidget(label: "Item Description", value: "\n${widget.itemDescription ?? '-'}"),
],
).toShadowContainer(
context,
borderRadius: 20,
padding: 12,
showShadow: false,
margin: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
);
).toShadowContainer(context);
}
}
@ -354,34 +348,34 @@ class CostCenterItemCard extends StatelessWidget {
children: [
isHmg == true
? SingleItemDropDownMenu<Lookup, CostCenterProvider>(
context: context,
height: 56.toScreenHeight,
title: 'Cost Center'.addTranslation,
showShadow: false,
backgroundColor: AppColor.fieldBgColor(context),
requestById: requestId,
showAsBottomSheet: true,
matchBy: DropDownMatchType.name,
initialValue: model.costCenter,
onSelect: (status) {
if (status != null) {
model.costCenter = status;
model.costCenterName = status.name;
}
},
)
context: context,
height: 56.toScreenHeight,
title: 'Cost Center'.addTranslation,
showShadow: false,
backgroundColor: AppColor.fieldBgColor(context),
requestById: requestId,
showAsBottomSheet: true,
matchBy: DropDownMatchType.name,
initialValue: model.costCenter,
onSelect: (status) {
if (status != null) {
model.costCenter = status;
model.costCenterName = status.name;
}
},
)
: AppTextFormField(
labelText: 'Cost Center'.addTranslation,
backgroundColor: AppColor.fieldBgColor(context),
initialValue: model.costCenterName ?? '',
textAlign: TextAlign.center,
labelStyle: AppTextStyles.textFieldLabelStyle,
showShadow: false,
onChange: (value) {
model.costCenterName = value;
},
style: Theme.of(context).textTheme.titleMedium,
),
labelText: 'Cost Center'.addTranslation,
backgroundColor: AppColor.fieldBgColor(context),
initialValue: model.costCenterName ?? '',
textAlign: TextAlign.center,
labelStyle: AppTextStyles.textFieldLabelStyle,
showShadow: false,
onChange: (value) {
model.costCenterName = value;
},
style: Theme.of(context).textTheme.titleMedium,
),
8.height,
AppTextFormField(
labelText: 'Quantity'.addTranslation,
@ -402,7 +396,7 @@ class CostCenterItemCard extends StatelessWidget {
],
)
],
).toShadowContainer(context, paddingObject: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)).paddingOnly(bottom: 12);
).toShadowContainer(context);
}
}
@ -447,11 +441,6 @@ class SerialNumberItemCard extends StatelessWidget {
style: Theme.of(context).textTheme.titleMedium,
),
],
)
.toShadowContainer(
context,
paddingObject: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
)
.paddingOnly(bottom: 12);
).toShadowContainer(context);
}
}

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:developer';
import 'package:date_picker_timeline/extra/color.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -32,6 +33,8 @@ import 'package:test_sa/views/widgets/date_and_time/time_picker.dart';
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
import 'package:test_sa/new_views/swipe_module/dialoge/acknowledge_work_dialog.dart';
import 'package:test_sa/views/widgets/date_and_time/date_picker.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/no_data_found.dart';
import '../../../../new_views/common_widgets/app_filled_button.dart';
@ -97,23 +100,19 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: isLoading
? CircularProgressIndicator(color: AppColor.loadingColor(context)).center
? CircularProgressIndicator(color: AppColor.loadingColor(context)).center
: Form(
key: _formKey,
child: Column(
children: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
if (!viewOnly) ...[inspectionDetailsForm()] else ...[inspectionDetails()],
20.height,
Text(
"Lines",
style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10),
),
8.height,
InfoHeader16Widget("Lines"),
linesList(context),
],
)).expanded,
@ -178,12 +177,12 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
return Consumer<AssetDeliveryProvider>(builder: (context, provider, child) {
List<DeliveryLineModel> linesList = provider.deliveryLinesList;
return provider.isDeliveryLineListLoading
? SizedBox.expand(child: CircularProgressIndicator(color: AppColor.loadingColor(context)).center)
? SizedBox.expand(child: CircularProgressIndicator(color: AppColor.loadingColor(context)).center)
: linesList.isEmpty
? const NoDataFound().center
: ListView.separated(
itemCount: linesList.length,
separatorBuilder: (_, __) => 8.height,
separatorBuilder: (_, __) => 12.height,
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@ -234,6 +233,7 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
Widget inspectionDetailsForm() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
_inspectionTile(
title: 'Inspected by'.addTranslation,
@ -251,7 +251,6 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
}
},
),
8.height,
_inspectionTile(
title: 'Approved by'.addTranslation,
model: model.approvedByModel,
@ -269,7 +268,6 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
},
),
if (widget.requestModel?.paymentTerm?.value == 2) ...[
8.height,
_inspectionTile(
title: 'ThirdApprover'.addTranslation,
model: model.thirdApprovalModel,
@ -287,7 +285,6 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
},
),
],
8.height,
ADatePicker(
label: 'Inspected Date/Time From'.addTranslation,
hideShadow: true,
@ -314,12 +311,10 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
if (!_validateInspectionDates(from: fromDateTime, to: model.deliveryInspectionInspectedDateTo)) {
return; // invalid, do not assign
}
model.deliveryInspectionInspectedDateFrom = fromDateTime;
setState(() {});
},
),
8.height,
ADatePicker(
label: 'Inspected Date/Time To'.addTranslation,
hideShadow: true,
@ -356,18 +351,7 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
setState(() {});
},
),
8.height,
Text(
'Total Time'.addTranslation,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10),
),
8.height,
Text(
formatInspectionDuration(model.totalTime),
style: AppTextStyles.bodyText2.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral120,
),
),
InfoTextWidget(label: 'Total Time'.addTranslation, value: formatInspectionDuration(model.totalTime)),
],
).toShadowContainer(context, borderRadius: 20, padding: 12);
}
@ -421,64 +405,18 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
return '${hours.toStringAsFixed(2)} hours';
}
Widget inspectionDetails() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
Text(
'Inspected by'.addTranslation,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10),
),
8.height,
Text(
model.deliveryInspectionInspectedBy ?? '-',
style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120),
),
8.height,
Text(
'Approved by'.addTranslation,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10),
),
8.height,
Text(
model.deliveryInspectionApprovedBy ?? '-',
style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120),
),
8.height,
Text(
'Inspected Date/Time From'.addTranslation,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10),
),
8.height,
Text(
model.deliveryInspectionInspectedDateFrom != null ? model.deliveryInspectionInspectedDateFrom!.toString().toServiceRequestDetailsFormat : '-',
style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120),
),
8.height,
Text(
'Inspected Date/Time To'.addTranslation,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10),
),
8.height,
Text(
model.deliveryInspectionInspectedDateTo != null ? model.deliveryInspectionInspectedDateTo!.toString().toServiceRequestDetailsFormat : '-',
style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120),
),
8.height,
Text(
'Total Time'.addTranslation,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10),
),
8.height,
Text(
formatInspectionDuration(model.totalTime),
style: AppTextStyles.bodyText2.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral120,
),
),
InfoTextWidget(label: "Inspected by", value: "\n${model.deliveryInspectionInspectedBy ?? '-'}"),
InfoTextWidget(label: "Approved by", value: "\n${model.deliveryInspectionApprovedBy ?? '-'}"),
InfoTextWidget(label: 'Inspected Date/Time From'.addTranslation, value: "\n${model.deliveryInspectionInspectedDateFrom?.toString().toServiceRequestDetailsFormat ?? '-'}"),
InfoTextWidget(label: 'Inspected Date/Time To'.addTranslation, value: "\n${model.deliveryInspectionInspectedDateTo?.toString().toServiceRequestDetailsFormat ?? '-'}"),
InfoTextWidget(label: 'Total Time'.addTranslation, value: "\n${formatInspectionDuration(model.totalTime) ?? '-'}"),
],
).toShadowContainer(context, borderRadius: 20, padding: 12);
).toShadowContainer(context);
}
Widget _inspectionTile({
@ -493,8 +431,9 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(12),
color: AppColor.background(context),
border: Border.all(color: AppColor.border2Color(context), width: 1),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
@ -502,25 +441,9 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: AppTextStyles.textFieldLabelStyle),
6.height,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
model?.name?.isNotEmpty == true ? model!.name! : 'Tap to add'.addTranslation,
style: AppTextStyles.bodyText,
),
///Need to display other info as well
// Text(
// model?.email?.isNotEmpty == true
// ? model!.email!
// : '',
// style: AppTextStyles.bodyText,
// ),
],
),
InfoTextLabelWidget(label: title, textStyle: AppTextStyles.textFieldLabelStyle),
4.height,
InfoTextLabelWidget(label: model?.name?.isNotEmpty == true ? model!.name! : 'Tap to add'.addTranslation, textStyle: AppTextStyles.bodyText),
],
),
],
@ -630,6 +553,7 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
return showModalBottomSheet<InspectionPersonModel>(
context: context,
isScrollControlled: true,
backgroundColor: AppColor.bottomSheetColor(context),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),

@ -67,14 +67,14 @@ class _InspectionPersonBottomSheetState extends State<InspectionPersonBottomShee
top: 16,
left: 16,
right: 16,
bottom: MediaQuery.of(context).viewInsets.bottom+MediaQuery.of(context).padding.bottom,
bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
Text(widget.title, style: Theme.of(context).textTheme.titleLarge),
12.height,
widget.title.heading4(context),
if (widget.isHmg)
OracleUserAutoCompleteField(
clearAfterPick: false,
@ -101,7 +101,6 @@ class _InspectionPersonBottomSheetState extends State<InspectionPersonBottomShee
setState(() {});
},
),
8.height,
Row(
children: [
AppTextFormField(
@ -121,7 +120,7 @@ class _InspectionPersonBottomSheetState extends State<InspectionPersonBottomShee
},
).expanded,
if (emailController.text.isNotEmpty) ...[
8.width,
12.width,
AppFilledButton(
buttonColor: AppColor.primary10,
label: 'SendEmail'.addTranslation,
@ -161,7 +160,6 @@ class _InspectionPersonBottomSheetState extends State<InspectionPersonBottomShee
],
],
),
8.height,
ESignature(
title: '',
editable: false,
@ -184,7 +182,6 @@ class _InspectionPersonBottomSheetState extends State<InspectionPersonBottomShee
// });
// },
),
12.height,
AppFilledButton(
label: context.translation.save,
maxWidth: true,

@ -52,11 +52,12 @@ class _AutoCompletePartsFieldState extends State<OracleUserAutoCompleteField> {
@override
Widget build(BuildContext context) {
assetDeliveryProvider ??= Provider.of<AssetDeliveryProvider>(context);
final border = UnderlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(10));
final border = UnderlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(16));
return Container(
decoration: BoxDecoration(
color: AppColor.background(context),
borderRadius: BorderRadius.circular(AppStyle.borderRadius * AppStyle.getScaleFactor(context)),
border: Border.all(color: AppColor.border2Color(context), width: 1),
borderRadius: BorderRadius.circular(16),
// boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)],
),
child: Autocomplete<OracleUserModel>(
@ -73,7 +74,7 @@ class _AutoCompletePartsFieldState extends State<OracleUserAutoCompleteField> {
return TextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
style: AppTextStyles.bodyText.copyWith(color: AppColor.black10),
style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 14, color: AppColor.textStyleColor(context)),
textAlign: TextAlign.start,
decoration: InputDecoration(
border: border,
@ -81,15 +82,16 @@ class _AutoCompletePartsFieldState extends State<OracleUserAutoCompleteField> {
focusedBorder: border,
enabledBorder: border,
errorBorder: border,
contentPadding: EdgeInsets.symmetric(vertical: 8.toScreenHeight, horizontal: 16.toScreenWidth),
contentPadding: EdgeInsets.symmetric(vertical: 12.toScreenHeight, horizontal: 16.toScreenWidth),
constraints: const BoxConstraints(),
suffixIconConstraints: const BoxConstraints(minWidth: 0),
filled: true,
fillColor: AppColor.fieldBgColor(context),
fillColor: AppColor.dropDownBackground(context),
errorStyle: AppTextStyle.tiny.copyWith(color: context.isDark ? AppColor.red50 : AppColor.red60),
floatingLabelStyle: AppTextStyle.body1.copyWith(fontWeight: FontWeight.w500, color: context.isDark ? null : AppColor.neutral20),
labelText: widget.label,
labelStyle: AppTextStyles.tinyFont.copyWith(color: AppColor.textColor(context)),
floatingLabelStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.floatingTextStyleColor(context)),
labelStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.labelTextStyleColor(context)),
),
textInputAction: TextInputAction.search,
onChanged: (text) {

@ -10,6 +10,7 @@ import 'package:test_sa/modules/asset_delivery_module/provider/asset_delivery_pr
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class AssetDeliveryHistoryLogView extends StatefulWidget {
@ -61,8 +62,8 @@ class _AssetDeliveryHistoryLogViewState extends State<AssetDeliveryHistoryLogVie
children: [
Padding(
padding: EdgeInsets.only(left: 122.toScreenWidth),
child: const VerticalDivider(
color: AppColor.white40,
child: VerticalDivider(
color: AppColor.borderColor(context),
thickness: 2,
width: 20,
indent: 10,
@ -126,7 +127,7 @@ class _AssetDeliveryHistoryLogViewState extends State<AssetDeliveryHistoryLogVie
child: Text(
object.createdDate.toString().toServiceRequestCardFormat,
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120),
style: AppTextStyles.tinyFont.copyWith(color: AppColor.labelTextStyleColor(context)),
),
).expanded,
Expanded(
@ -134,7 +135,7 @@ class _AssetDeliveryHistoryLogViewState extends State<AssetDeliveryHistoryLogVie
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title.bodyText(context).custom(color: AppColor.textColor(context)),
InfoHeaderWidget(title),
// object.timeDifference.isNotEmpty ? object.timeDifference?.tinyFont(context).custom(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120) : const SizedBox(),
],
),

@ -69,7 +69,7 @@ class _AssetInventoryDetailViewState extends State<AssetInventoryDetailView> {
InfoTextWidget(label: 'Start Date'.addTranslation, value: sessionModel.startDate?.toServiceRequestDetailsFormat ?? '-'),
InfoTextWidget(label: 'End Date'.addTranslation, value: sessionModel.endDate?.toServiceRequestDetailsFormat ?? '-'),
],
).toShadowContainer(context, padding: 16, borderRadius: 20);
).toShadowContainer(context, borderRadius: 20);
}
Widget siteListCard(BuildContext context, SessionModel sessionModel) {
@ -84,7 +84,7 @@ class _AssetInventoryDetailViewState extends State<AssetInventoryDetailView> {
sites: sites,
),
],
).toShadowContainer(context, padding: 16, borderRadius: 20);
).toShadowContainer(context, borderRadius: 20);
}
}

@ -1,24 +0,0 @@
// Cleaner request search model for mock/static data
class CleanerRequest {
final String priority;
final String date;
final String time;
final String roomNo;
final String site;
final String building;
final String floor;
final String clinic;
final String? taskNo;
CleanerRequest({
required this.priority,
required this.date,
required this.time,
required this.roomNo,
required this.site,
required this.building,
required this.floor,
required this.clinic,
this.taskNo,
});
}

@ -0,0 +1,247 @@
class OneTimeTaskData {
int? id;
int? oneTimeSettingId;
OneTimeSetting? oneTimeSetting;
String? userId;
String? userName;
int? siteId;
String? siteName;
int? buildingId;
String? buildingName;
int? floorId;
String? floorName;
int? departmentID;
String? departmentName;
int? roomId;
String? roomName;
String? locationDescription;
String? startTime;
String? endTime;
int? statusId;
String? statusName;
int? statusValue;
List<Instruction>? instructions;
List<Attachment>? attachments;
String? createdDate;
String? createdBy;
String? imageRoom; // For backward compatibility - deprecated
List<String>? imageRooms; // For multiple captured images (max 2)
OneTimeTaskData({
this.id,
this.oneTimeSettingId,
this.oneTimeSetting,
this.userId,
this.userName,
this.siteId,
this.siteName,
this.buildingId,
this.buildingName,
this.floorId,
this.floorName,
this.departmentID,
this.departmentName,
this.roomId,
this.roomName,
this.locationDescription,
this.startTime,
this.endTime,
this.statusId,
this.statusName,
this.statusValue,
this.instructions,
this.attachments,
this.createdDate,
this.createdBy,
this.imageRoom,
this.imageRooms,
});
factory OneTimeTaskData.fromJson(Map<String, dynamic> json) {
return OneTimeTaskData(
id: json['id'],
oneTimeSettingId: json['oneTimeSettingId'],
oneTimeSetting: json['oneTimeSetting'] != null ? OneTimeSetting.fromJson(json['oneTimeSetting']) : null,
userId: json['userId'],
userName: json['userName'],
siteId: json['siteId'],
siteName: json['siteName'],
buildingId: json['buildingId'],
buildingName: json['buildingName'],
floorId: json['floorId'],
floorName: json['floorName'],
departmentID: json['departmentID'],
departmentName: json['departmentName'],
roomId: json['roomId'],
roomName: json['roomName'],
locationDescription: json['locationDescription'],
startTime: json['startTime'],
endTime: json['endTime'],
statusId: json['statusId'],
statusName: json['statusName'],
statusValue: json['statusValue'],
instructions: json['instructions'] != null
? (json['instructions'] as List).map((i) => Instruction.fromJson(i)).toList()
: null,
attachments: json['attachments'] != null
? (json['attachments'] as List).map((a) => Attachment.fromJson(a)).toList()
: null,
createdDate: json['createdDate'],
createdBy: json['createdBy'],
imageRoom: json['imageRoom'],
imageRooms: json['imageRooms'] != null ? List<String>.from(json['imageRooms']) : null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'oneTimeSettingId': oneTimeSettingId,
'oneTimeSetting': oneTimeSetting?.toJson(),
'userId': userId,
'userName': userName,
'siteId': siteId,
'siteName': siteName,
'buildingId': buildingId,
'buildingName': buildingName,
'floorId': floorId,
'floorName': floorName,
'departmentID': departmentID,
'departmentName': departmentName,
'roomId': roomId,
'roomName': roomName,
'locationDescription': locationDescription,
'startTime': startTime,
'endTime': endTime,
'statusId': statusId,
'statusName': statusName,
'statusValue': statusValue,
'instructions': instructions?.map((i) => i.toJson()).toList(),
'attachments': attachments?.map((a) => a.toJson()).toList(),
'createdDate': createdDate,
'createdBy': createdBy,
'imageRoom': imageRoom,
'imageRooms': imageRooms,
};
}
}
class OneTimeSetting {
int? id;
int? taskTypeId;
String? taskTypeName;
int? instructionDescriptionId;
String? instructionDescriptionName;
int? sla;
OneTimeSetting({
this.id,
this.taskTypeId,
this.taskTypeName,
this.instructionDescriptionId,
this.instructionDescriptionName,
this.sla,
});
factory OneTimeSetting.fromJson(Map<String, dynamic> json) {
return OneTimeSetting(
id: json['id'],
taskTypeId: json['taskTypeId'],
taskTypeName: json['taskTypeName'],
instructionDescriptionId: json['instructionDescriptionId'],
instructionDescriptionName: json['instructionDescriptionName'],
sla: json['sla'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'taskTypeId': taskTypeId,
'taskTypeName': taskTypeName,
'instructionDescriptionId': instructionDescriptionId,
'instructionDescriptionName': instructionDescriptionName,
'sla': sla,
};
}
}
class Instruction {
int? id;
int? instructionTextId;
String? instructionText;
bool? isRequired;
bool? isDone;
String? comment;
Instruction({
this.id,
this.instructionTextId,
this.instructionText,
this.isRequired,
this.isDone,
this.comment,
});
factory Instruction.fromJson(Map<String, dynamic> json) {
return Instruction(
id: json['id'],
instructionTextId: json['instructionTextId'],
instructionText: json['instructionText'],
isRequired: json['isRequired'],
isDone: json['isDone'],
comment: json['comment'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'instructionTextId': instructionTextId,
'instructionText': instructionText,
'isRequired': isRequired,
'isDone': isDone,
'comment': comment,
};
}
}
class Attachment {
int? id;
String? name;
String? originalName;
String? fileName; // For backward compatibility
String? fileUrl; // For backward compatibility
String? fileType;
Attachment({
this.id,
this.name,
this.originalName,
this.fileName,
this.fileUrl,
this.fileType,
});
factory Attachment.fromJson(Map<String, dynamic> json) {
return Attachment(
id: json['id'],
name: json['name'],
originalName: json['originalName'],
fileName: json['fileName'],
fileUrl: json['fileUrl'],
fileType: json['fileType'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'originalName': originalName,
'fileName': fileName,
'fileUrl': fileUrl,
'fileType': fileType,
};
}
}

@ -0,0 +1,60 @@
class QrScanLocationData {
int? siteId;
String? siteName;
int? buildingId;
String? buildingName;
int? floorId;
String? floorName;
int? departmentId;
String? departmentName;
int? roomId;
String? roomName;
String? locationDescription;
QrScanLocationData({
this.siteId,
this.siteName,
this.buildingId,
this.buildingName,
this.floorId,
this.floorName,
this.departmentId,
this.departmentName,
this.roomId,
this.roomName,
this.locationDescription,
});
factory QrScanLocationData.fromJson(Map<String, dynamic> json) {
return QrScanLocationData(
siteId: json['siteId'],
siteName: json['siteName'],
buildingId: json['buildingId'],
buildingName: json['buildingName'],
floorId: json['floorId'],
floorName: json['floorName'],
departmentId: json['departmentId'],
departmentName: json['departmentName'],
roomId: json['roomId'],
roomName: json['roomName'],
locationDescription: json['locationDescription'],
);
}
Map<String, dynamic> toJson() {
return {
'siteId': siteId,
'siteName': siteName,
'buildingId': buildingId,
'buildingName': buildingName,
'floorId': floorId,
'floorName': floorName,
'departmentId': departmentId,
'departmentName': departmentName,
'roomId': roomId,
'roomName': roomName,
'locationDescription': locationDescription,
};
}
}

@ -0,0 +1,26 @@
import 'package:test_sa/models/base.dart';
class TaskType extends Base {
int? id;
String? taskTypeName;
TaskType({this.id, this.taskTypeName}) : super(identifier: id?.toString(), name: taskTypeName);
@override
bool operator ==(Object other) => identical(this, other) || other is TaskType && id != null && id == other.id;
@override
int get hashCode => id.hashCode;
Map<String, dynamic> toJson() {
return {"id": id, "taskTypeName": taskTypeName};
}
factory TaskType.fromJson(Map<String, dynamic>? parsedJson) {
if (parsedJson == null) return TaskType();
return TaskType(
id: parsedJson["id"],
taskTypeName: parsedJson["taskTypeName"],
);
}
}

@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import '../models/cleaner_task_search_result.dart';
class CleanerRequestItemView extends StatelessWidget {
final CleanerTaskSearchResult result;
final VoidCallback? onTap;
final bool showShadow;
const CleanerRequestItemView({Key? key, required this.result, this.onTap, this.showShadow = true}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Task Number and Date Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (result.taskNo != null && result.taskNo!.isNotEmpty) result.taskNo!.heading5(context).custom(color: AppColor.textStyleColor(context)),
if (result.createdDate != null)
Text(
result.createdDate!.split('T').first,
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral50,
),
),
],
),
8.height,
// Location Information
if (result.siteName != null && result.siteName!.isNotEmpty) 'Site: ${result.siteName}'.bodyText(context),
if (result.buildingName != null && result.buildingName!.isNotEmpty) 'Building: ${result.buildingName}'.bodyText(context),
if (result.floorName != null && result.floorName!.isNotEmpty) 'Floor: ${result.floorName}'.bodyText(context),
if (result.departmentName != null && result.departmentName!.isNotEmpty) 'Department: ${result.departmentName}'.bodyText(context),
if (result.roomName != null && result.roomName!.isNotEmpty) 'Room: ${result.roomName}'.bodyText(context),
if (result.locationDescription != null && result.locationDescription!.isNotEmpty) 'Location: ${result.locationDescription}'.bodyText(context),
8.height,
// View Details Link
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.translation.viewDetails,
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
),
4.width,
Icon(
Icons.arrow_forward,
color: AppColor.blueStatus(context),
size: 14,
)
],
),
],
).toShadowContainer(context, withShadow: showShadow).onPress(onTap);
}
}

@ -1,292 +0,0 @@
// UI: Cleaner Search Details View
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/views/widgets/qr/scan_qr.dart';
import '../models/cleaner_task_search_result.dart';
import '../provider/cleaner_provider.dart';
import '../../pm_module/recurrent_wo/recurrent_work_order_view.dart';
import 'package:test_sa/controllers/providers/api/all_requests_provider.dart';
class CleanerSearchDetailsView extends StatefulWidget {
static const String routeName = '/cleaner_search_details';
final int requestId;
final CleanerTaskSearchResult? taskResult;
const CleanerSearchDetailsView({
Key? key,
required this.requestId,
this.taskResult,
}) : super(key: key);
@override
_CleanerSearchDetailsViewState createState() => _CleanerSearchDetailsViewState();
}
class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
late CleanerProvider _cleanerProvider;
bool _isQRScanned = false;
CleanerTaskSearchResult? _taskData;
bool _isLoading = false;
@override
void initState() {
super.initState();
_cleanerProvider = Provider.of<CleanerProvider>(context, listen: false);
_taskData = widget.taskResult;
WidgetsBinding.instance.addPostFrameCallback((_) {
getInitialData();
});
}
Future<void> getInitialData() async {
// If taskResult is null, fetch data from recurrent work order API
if (widget.taskResult == null) {
setState(() {
_isLoading = true;
});
try {
final allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
await allRequestsProvider.getRecurrentWoById(id: widget.requestId);
// Map RecurrentWoData to CleanerTaskSearchResult
if (allRequestsProvider.recurrentWoData != null) {
final recurrentData = allRequestsProvider.recurrentWoData!;
_taskData = CleanerTaskSearchResult(
id: recurrentData.id,
taskNo: recurrentData.taskNo,
siteName: recurrentData.site?.siteName,
buildingName: recurrentData.building?.name,
floorName: recurrentData.floor?.name,
departmentName: recurrentData.department?.name,
roomName: recurrentData.room?.name,
locationDescription: null,
createdDate: recurrentData.scheduleDate,
);
}
} catch (e) {
debugPrint('Error fetching recurrent work order: $e');
} finally {
setState(() {
_isLoading = false;
});
}
} else {
await _cleanerProvider.getCleanerRequestById(id: widget.requestId);
}
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: DefaultAppBar(
title: 'Request Details'.addTranslation,
onBackPress: () => Navigator.pop(context),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SafeArea(
child: Selector<CleanerProvider, bool>(
selector: (_, model) => model.isLoading,
builder: (_, isLoading, __) {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Column(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCleanerRequestDetailCard(context),
],
),
).expanded,
FooterActionButton.footerContainer(
context: context,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!_isQRScanned) ...[
Row(
children: [
Icon(Icons.info_outline_rounded, size: 16, color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
8.width,
Expanded(
child: Text(
'By scanning the QR code you will accept this task'.addTranslation,
// textAlign: TextAlign.center,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
),
],
),
8.height,
],
AppFilledButton(
label: _isQRScanned ? 'Update' : 'Scan QR Code',
maxWidth: true,
buttonColor: AppColor.primary10,
onPressed: () async {
if (_isQRScanned) {
// If QR is already scanned, navigate to recurrent work order view
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId),
),
);
} else {
// Open QR scanner
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ScanQr(),
),
);
// If QR code was scanned successfully, check if it matches the room
if (result != null && result is String && result.isNotEmpty) {
// Show loading indicator
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AppLazyLoading(),
);
// Call the API to check if QR matches the same room
final isValid = await CleanerProvider().checkQRSameRoom(
planRecurrentTaskId: widget.requestId,
qrCode: result,
);
// Close loading dialog
if (mounted) Navigator.pop(context);
if (isValid) {
// Update state to show "Update" button instead of navigating
if (mounted) {
setState(() {
_isQRScanned = true;
});
}
}
}
}
},
),
],
),
).toShadowContainer(context, padding: 0, showShadow: false, borderRadius: 0),
],
);
},
),
),
);
}
Widget _buildCleanerRequestDetailCard(BuildContext context) {
final taskResult = _taskData;
if (taskResult == null) {
return const SizedBox();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Task Number and Date Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Request Information',
style: AppTextStyles.heading4.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
if (taskResult.createdDate != null)
Text(
taskResult.createdDate!.toServiceRequestCardFormat,
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
),
],
),
8.height,
if (taskResult.taskNo != null && taskResult.taskNo!.isNotEmpty)
Text(
'Task No: ${taskResult.taskNo!}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
if (taskResult.siteName != null && taskResult.siteName!.isNotEmpty)
Text(
'Site: ${taskResult.siteName}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
if (taskResult.buildingName != null && taskResult.buildingName!.isNotEmpty)
Text(
'Building: ${taskResult.buildingName}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
if (taskResult.floorName != null && taskResult.floorName!.isNotEmpty)
Text(
'Floor: ${taskResult.floorName}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
if (taskResult.departmentName != null && taskResult.departmentName!.isNotEmpty)
Text(
'Department: ${taskResult.departmentName}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
if (taskResult.roomName != null && taskResult.roomName!.isNotEmpty)
Text(
'Room: ${taskResult.roomName}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
if (taskResult.locationDescription != null && taskResult.locationDescription!.isNotEmpty)
Text(
'Location: ${taskResult.locationDescription}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
],
).expanded,
],
),
],
).paddingOnly(start: 16, end: 16, top: 16, bottom: 8),
],
).toShadowContainer(context, padding: 0);
}
}

@ -1,26 +1,23 @@
import 'package:flutter/material.dart';
import 'package:test_sa/controllers/providers/api/hospitals_provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/hospital.dart';
import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/new_models/department_room_combined.dart';
import 'package:test_sa/modules/cleaner_module/pages/cleaner_search_list_view.dart';
import 'package:test_sa/modules/cleaner_module/models/task_type.dart';
import 'package:test_sa/modules/cleaner_module/models/qr_scan_location_data.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/fms_building_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/fms_floor_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/fms_location_description_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/task_type_provider.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/providers/loading_list_notifier.dart';
import 'package:test_sa/models/new_models/building.dart';
import 'package:test_sa/models/new_models/floor.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/qr/scan_qr.dart';
class CleanerSearchFormView extends StatefulWidget {
static const String routeName = '/cleaner_search';
@ -32,161 +29,146 @@ class CleanerSearchFormView extends StatefulWidget {
}
class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
Hospital? selectedSite;
Building? selectedBuilding;
Floor? selectedFloor;
DepartmentRoomCombined? selectedDepartmentRoom;
Lookup? selectedLocationDescription;
late FocusNode _siteFocusNode;
final TextEditingController _siteController = TextEditingController();
List<Hospital> _siteOptions = [];
List<Building> _buildingOptions = [];
List<Floor> _floorOptions = [];
List<Lookup> _locationDescriptionOptions = [];
List<DepartmentRoomItem> _allDeptRoomList = [];
bool _isLoadingSites = false;
bool _isLoadingBuildings = false;
bool _isLoadingFloors = false;
bool _isLoadingLocationDescriptions = false;
bool _isSearching = false;
TaskType? selectedTaskType;
List<TaskType> _taskTypeOptions = [];
bool _isLoadingTaskTypes = false;
bool _isScanning = false;
bool _isProceeding = false;
QrScanLocationData? _scannedLocationData;
@override
void initState() {
super.initState();
_siteFocusNode = FocusNode();
_loadTaskTypes();
}
@override
void dispose() {
_siteController.dispose();
_siteFocusNode.dispose();
super.dispose();
}
Future<void> _searchSites(String query) async {
if (query.isEmpty) {
setState(() {
_siteOptions = [];
});
return;
}
Future<void> _loadTaskTypes() async {
setState(() {
_isLoadingSites = true;
_isLoadingTaskTypes = true;
});
try {
final sites = await HospitalsProvider().getHospitalsListByVal(searchVal: query);
final taskTypes = await TaskTypeProvider().getTaskTypesList();
setState(() {
_siteOptions = sites;
_isLoadingSites = false;
_taskTypeOptions = taskTypes;
_isLoadingTaskTypes = false;
});
} catch (e) {
setState(() {
_isLoadingSites = false;
_isLoadingTaskTypes = false;
_taskTypeOptions = [];
});
_showErrorDialog(context, 'Failed to load task types. Please try again.');
}
}
Future<void> _loadBuildings(int siteId) async {
setState(() {
_isLoadingBuildings = true;
});
Future<void> _onScanQRCode() async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ScanQr()),
);
try {
final buildings = await FmsBuildingProvider().getBuildingsListBySiteId(siteId);
if (result != null && result is String && result.isNotEmpty) {
setState(() {
_buildingOptions = buildings;
_isLoadingBuildings = false;
});
} catch (e) {
setState(() {
_isLoadingBuildings = false;
_buildingOptions = [];
_isScanning = true;
});
try {
final qrResult = await CleanerProvider().scanQRCode(qrCode: result);
final isSuccess = qrResult["success"] ?? false;
final backendMessage = qrResult["message"] ?? "Unknown error occurred";
if (isSuccess && qrResult["data"] != null) {
// Store the scanned location data
setState(() {
_scannedLocationData = QrScanLocationData.fromJson(qrResult["data"]);
_isScanning = false;
});
} else {
setState(() {
_isScanning = false;
});
if (mounted) {
_showErrorDialog(context, backendMessage);
}
}
} catch (e) {
setState(() {
_isScanning = false;
});
if (mounted) {
_showErrorDialog(context, 'Failed to scan QR code. Please try again.');
}
}
}
}
Future<void> _loadFloors(int siteId, int buildingId) async {
setState(() {
_isLoadingFloors = true;
});
Future<void> _onProceed() async {
// Validate that task type and location data are available
if (selectedTaskType == null) {
_showErrorDialog(context, 'Please select a task type');
return;
}
try {
final floors = await FmsFloorProvider().getFloorsListBySiteAndBuilding(siteId, buildingId);
setState(() {
_floorOptions = floors;
_isLoadingFloors = false;
});
} catch (e) {
setState(() {
_isLoadingFloors = false;
_floorOptions = [];
});
if (_scannedLocationData == null) {
_showErrorDialog(context, 'Please scan a QR code first');
return;
}
}
Future<void> _loadLocationDescriptions(int siteId, int buildingId, int floorId) async {
setState(() {
_isLoadingLocationDescriptions = true;
_isProceeding = true;
});
try {
final response = await FmsLocationDescriptionProvider().getLocationDescriptionsList(siteId, buildingId, floorId);
setState(() {
_locationDescriptionOptions = response?.locationDescriptions ?? [];
_allDeptRoomList = response?.deptRoom ?? [];
_isLoadingLocationDescriptions = false;
});
} catch (e) {
setState(() {
_isLoadingLocationDescriptions = false;
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
}
}
List<DepartmentRoomCombined> _getDepartmentRoomCombinations() {
if (selectedLocationDescription == null || _allDeptRoomList.isEmpty) return [];
// Filter dept room list by selected location description
List<DepartmentRoomItem> filteredList = _allDeptRoomList.where((item) => item.locationDescriptionName == selectedLocationDescription!.name).toList();
// Convert to DepartmentRoomCombined
return filteredList
.map((item) => DepartmentRoomCombined(
deptId: item.deptId,
roomId: item.roomId,
deptName: item.deptName,
roomName: item.roomName,
locationDescriptionName: item.locationDescriptionName,
deptAndRoomName: item.deptAndRoomName,
))
.toList();
}
void _autoSelectDepartmentRoom() {
final combinations = _getDepartmentRoomCombinations();
if (combinations.isEmpty) {
setState(() {
selectedDepartmentRoom = null;
});
return;
}
final result = await CleanerProvider().proceedOneTimeTask(
oneTimeSettingId: selectedTaskType!.id ?? 0,
siteId: _scannedLocationData!.siteId ?? 0,
buildingId: _scannedLocationData!.buildingId ?? 0,
floorId: _scannedLocationData!.floorId ?? 0,
departmentId: _scannedLocationData!.departmentId ?? 0,
roomId: _scannedLocationData!.roomId ?? 0,
);
// If only one item, auto-fill
if (combinations.length == 1) {
setState(() {
selectedDepartmentRoom = combinations.first;
});
} else {
// If more than one, auto-select first item
final isSuccess = result["success"] ?? false;
final data = result["data"];
final message = result["message"] ?? "Unknown error occurred";
if (isSuccess && data != null) {
// Get the task ID from the response
final taskId = data['id'];
if (mounted && taskId != null) {
// Convert the data to OneTimeTaskData and navigate with the data
final taskData = OneTimeTaskData.fromJson(data);
// Navigate to OneTimeTaskView with the data
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: taskId,
initialData: taskData,
),
),
);
}
} else {
if (mounted) {
_showErrorDialog(context, message);
}
}
} catch (e) {
if (mounted) {
_showErrorDialog(context, 'Failed to proceed. Please try again.');
}
} finally {
setState(() {
selectedDepartmentRoom = combinations.first;
_isProceeding = false;
});
}
}
@ -201,238 +183,88 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
spacing: 12,
children: [
// Site Autocomplete Field
Container(
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight),
decoration: BoxDecoration(
color: AppColor.dropDownBackground(context),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.border2Color(context), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Site',
style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.floatingTextStyleColor(context)),
),
SizedBox(
height: 24.toScreenHeight, // Fixed height for consistency
child: Autocomplete<Hospital>(
optionsBuilder: (TextEditingValue textEditingValue) async {
if (textEditingValue.text.isEmpty) {
return const Iterable<Hospital>.empty();
}
await _searchSites(textEditingValue.text);
return _siteOptions;
},
displayStringForOption: (Hospital option) => option.name ?? "",
onSelected: (Hospital selected) async {
// _siteFocusNode.unfocus();
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
selectedSite = selected;
_siteController.text = selected.name ?? '';
// Reset dependent dropdowns
selectedBuilding = null;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_buildingOptions = [];
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
// Load buildings for selected site
if (selected.id != null) {
await _loadBuildings(selected.id!);
}
},
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
if (_siteController.text.isNotEmpty && controller.text.isEmpty) {
controller.text = _siteController.text;
}
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
controller: controller,
// focusNode: _siteFocusNode,
focusNode: focusNode,
cursorColor: AppColor.loadingColor(context),
style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 14, color: AppColor.textStyleColor(context)),
decoration: InputDecoration(
hintText: 'Search site...',
hintStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.labelTextStyleColor(context)),
filled: false,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
onChanged: (value) {
_siteController.text = value;
},
),
),
if (_isLoadingSites)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: AppColor.loadingColor(context)),
)
else if (controller.text.isNotEmpty)
GestureDetector(
onTap: () {
controller.clear();
_siteController.clear();
setState(() {
selectedSite = null;
selectedBuilding = null;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_siteOptions = [];
_buildingOptions = [];
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
},
child: Icon(
Icons.clear,
color: AppColor.icon2Color(context),
size: 20,
),
),
],
);
},
),
),
],
),
),
// Building Dropdown
SingleItemDropDownMenu<Building, NullableLoadingProvider>(
// Task Type Dropdown
SingleItemDropDownMenu<TaskType, NullableLoadingProvider>(
context: context,
title: 'Building',
initialValue: selectedBuilding,
title: 'Task Type',
initialValue: selectedTaskType,
showShadow: false,
showAsBottomSheet: true,
// backgroundColor: AppColor.fieldBgColor(context),
enabled: _buildingOptions.isNotEmpty,
loading: _isLoadingBuildings,
staticData: _buildingOptions,
onSelect: (value) async {
_siteFocusNode.unfocus();
backgroundColor: AppColor.fieldBgColor(context),
enabled: _taskTypeOptions.isNotEmpty,
loading: _isLoadingTaskTypes,
staticData: _taskTypeOptions,
onSelect: (value) {
setState(() {
_siteOptions = [];
selectedBuilding = value;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
selectedTaskType = value;
});
// Load floors for selected building
if (value != null && selectedSite?.id != null && value.id != null) {
await _loadFloors(selectedSite!.id!, value.id!.toInt());
}
},
),
// Floor Dropdown
SingleItemDropDownMenu<Floor, NullableLoadingProvider>(
context: context,
title: 'Floor',
initialValue: selectedFloor,
showShadow: false,
showAsBottomSheet: true,
// backgroundColor: AppColor.fieldBgColor(context),
enabled: _floorOptions.isNotEmpty,
loading: _isLoadingFloors,
staticData: _floorOptions,
onSelect: (value) async {
setState(() {
selectedFloor = value;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
// Load location descriptions for selected floor
if (value != null && selectedSite?.id != null && selectedBuilding?.id != null && value.id != null) {
await _loadLocationDescriptions(
selectedSite!.id!,
selectedBuilding!.id!.toInt(),
value.id!.toInt(),
);
}
},
// Scan QR Code Button
GestureDetector(
onTap: selectedTaskType != null ? _onScanQRCode : null,
child: Container(
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: selectedTaskType != null ? AppColor.background(context) : Colors.grey[300],
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: selectedTaskType != null ? AppColor.borderColor(context) : Colors.grey,
width: 1,
),
),
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
"scan_asset".toSvgAsset(
height: 22,
fit: BoxFit.fitHeight,
color: selectedTaskType != null ? AppColor.icon2Color(context) : Colors.grey,
),
8.width,
//we can change the implementation when merge with uiux branch
_scannedLocationData == null
? "Scan QR Code".bodyText(context).custom(
color: selectedTaskType != null ? AppColor.icon2Color(context) : Colors.grey,
)
: 'Re Scan QR Code'.bodyText(context).custom(
color: selectedTaskType != null ? AppColor.icon2Color(context) : Colors.grey,
),
],
),
),
),
// Location Description Dropdown
SingleItemDropDownMenu<Lookup, NullableLoadingProvider>(
context: context,
title: 'Location Description',
initialValue: selectedLocationDescription,
showShadow: false,
showAsBottomSheet: true,
// backgroundColor: AppColor.fieldBgColor(context),
enabled: _locationDescriptionOptions.isNotEmpty,
loading: _isLoadingLocationDescriptions,
staticData: _locationDescriptionOptions,
onSelect: (value) {
setState(() {
selectedLocationDescription = value;
selectedDepartmentRoom = null;
});
// Auto-select department-room based on location description
_autoSelectDepartmentRoom();
},
),
// Loading Indicator
if (_isScanning)
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
),
// Department & Room Combined Dropdown
SingleItemDropDownMenu<DepartmentRoomCombined, NullableLoadingProvider>(
context: context,
title: 'Department - Room',
initialValue: selectedDepartmentRoom,
showShadow: false,
showAsBottomSheet: true,
backgroundColor: AppColor.fieldBgColor(context),
enabled: _getDepartmentRoomCombinations().isNotEmpty,
staticData: _getDepartmentRoomCombinations(),
onSelect: (value) {
setState(() {
selectedDepartmentRoom = value;
});
},
),
// Scanned Location Data Card
if (_scannedLocationData != null && !_isScanning) _buildLocationDataCard(context),
],
).toShadowContainer(context).paddingOnly(top: 20, start: 16, end: 16),
),
),
// Footer Action Button
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
buttonColor: AppColor.primary10,
label: context.translation.search,
buttonColor: _scannedLocationData != null && !_isProceeding ? AppColor.primary10 : Colors.grey,
label: 'Proceed',
maxWidth: true,
onPressed: _isSearching ? null : _onSubmit,
loading: _isSearching,
onPressed: (_scannedLocationData != null && !_isProceeding) ? _onProceed : null,
loading: _isProceeding,
),
),
],
@ -440,51 +272,74 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
);
}
Future<void> _onSubmit() async {
// Validate that at least site is selected
if (selectedSite == null) {
'Please select a site'.showToast;
return;
}
try {
setState(() {
_isSearching = true;
});
Widget _buildLocationDataCard(BuildContext context) {
final locationData = _scannedLocationData;
// Call the search API
final response = await CleanerProvider().searchPlanRecurrentTasksList(
pageSize: 10,
pageNumber: 1,
siteId: selectedSite?.id,
buildingId: selectedBuilding?.id?.toInt(),
floorId: selectedFloor?.id?.toInt(),
departmentId: selectedDepartmentRoom?.deptId,
roomId: selectedDepartmentRoom?.roomId,
);
if (locationData == null) {
return const SizedBox();
}
final results = response['data'] as List<dynamic>;
final totalRows = response['totalRows'] as int;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (locationData.siteName != null && locationData.siteName!.isNotEmpty)
InfoTextWidget(
label: 'Site', value: locationData.siteName,
// style: AppTextStyles.bodyText.copyWith(
// color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
// ),
),
if (locationData.buildingName != null && locationData.buildingName!.isNotEmpty)
InfoTextWidget(
label: 'Building',
value: locationData.buildingName,
),
if (locationData.floorName != null && locationData.floorName!.isNotEmpty)
InfoTextWidget(
label: 'Floor',
value: locationData.floorName,
),
if (locationData.departmentName != null && locationData.departmentName!.isNotEmpty)
InfoTextWidget(
label: 'Department',
value: locationData.departmentName,
),
if (locationData.roomName != null && locationData.roomName!.isNotEmpty)
InfoTextWidget(
label: 'Room',
value: locationData.roomName,
),
if (locationData.locationDescription != null && locationData.locationDescription!.isNotEmpty)
InfoTextWidget(
label: 'Location Description',
value: locationData.locationDescription,
),
// Text(
// 'Location Description: ${locationData.locationDescription}',
// style: AppTextStyles.bodyText.copyWith(
// color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
// ),
// softWrap: true,
// maxLines: null,
// ),
],
).paddingOnly(start: 16, end: 16, top: 16, bottom: 16),
],
).toShadowContainer(context, padding: 0);
}
// Navigate to search results page with the results
Navigator.of(context).pushNamed(
CleanerSearchListView.routeName,
arguments: {
'searchResults': results,
'totalRows': totalRows,
'selectedSite': selectedSite,
'selectedBuilding': selectedBuilding,
'selectedFloor': selectedFloor,
'selectedLocationDescription': selectedLocationDescription,
'selectedDepartmentRoom': selectedDepartmentRoom,
},
);
} catch (e) {
'Error searching'.showToast;
} finally {
setState(() {
_isSearching = false;
});
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
}

@ -1,186 +0,0 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/hospital.dart';
import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/new_models/building.dart';
import 'package:test_sa/models/new_models/department_room_combined.dart';
import 'package:test_sa/models/new_models/floor.dart';
import 'package:test_sa/modules/cleaner_module/models/cleaner_task_search_result.dart';
import 'package:test_sa/modules/cleaner_module/pages/cleaner_search_details_view.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'cleaner_request_item_view.dart';
class CleanerSearchListView extends StatefulWidget {
static const String routeName = '/cleaner_search_list';
const CleanerSearchListView({Key? key}) : super(key: key);
@override
State<CleanerSearchListView> createState() => _CleanerSearchListViewState();
}
class _CleanerSearchListViewState extends State<CleanerSearchListView> {
List<CleanerTaskSearchResult> _searchResults = [];
bool _isLoading = false;
bool _isLoadingMore = false;
bool _hasMoreData = true;
int _totalRows = 0; // Add this to track total rows from API
// Pagination
int _currentPage = 1;
final int _pageSize = 10;
// Search filters (stored for pagination)
Hospital? _selectedSite;
Building? _selectedBuilding;
Floor? _selectedFloor;
Lookup? _selectedLocationDescription;
DepartmentRoomCombined? _selectedDepartmentRoom;
final ScrollController _scrollController = ScrollController();
final CleanerProvider _cleanerProvider = CleanerProvider();
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Get the search results and filters from navigation arguments
final args = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
if (args != null && args['searchResults'] != null) {
final results = args['searchResults'] as List<dynamic>;
_searchResults = results.map((json) => CleanerTaskSearchResult.fromJson(json)).toList();
// Get totalRows from API response
_totalRows = args['totalRows'] as int? ?? 0;
// Store search filters for pagination
_selectedSite = args['selectedSite'] as Hospital?;
_selectedBuilding = args['selectedBuilding'] as Building?;
_selectedFloor = args['selectedFloor'] as Floor?;
_selectedLocationDescription = args['selectedLocationDescription'] as Lookup?;
_selectedDepartmentRoom = args['selectedDepartmentRoom'] as DepartmentRoomCombined?;
// Check if there's more data based on totalRows
_hasMoreData = _searchResults.length < _totalRows;
}
}
void _onScroll() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
// User is near the bottom, load more data
if (!_isLoadingMore && _hasMoreData && !_isLoading) {
_loadMoreData();
}
}
}
Future<void> _loadMoreData() async {
if (_isLoadingMore) return;
setState(() {
_isLoadingMore = true;
});
try {
_currentPage++;
final response = await _cleanerProvider.searchPlanRecurrentTasksList(
pageSize: _pageSize,
pageNumber: _currentPage,
siteId: _selectedSite?.id,
buildingId: _selectedBuilding?.id?.toInt(),
floorId: _selectedFloor?.id?.toInt(),
departmentId: _selectedDepartmentRoom?.deptId,
roomId: _selectedDepartmentRoom?.roomId,
);
final results = response['data'] as List<dynamic>;
final totalRows = response['totalRows'] as int? ?? 0;
final newResults = results.map((json) => CleanerTaskSearchResult.fromJson(json)).toList();
setState(() {
_searchResults.addAll(newResults);
_totalRows = totalRows;
// Check if we have loaded all data based on totalRows
_hasMoreData = _searchResults.length < _totalRows;
_isLoadingMore = false;
});
} catch (e) {
setState(() {
_isLoadingMore = false;
});
debugPrint('Error loading more data: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: DefaultAppBar(
title: 'Search Results'.addTranslation,
onBackPress: () => Navigator.pop(context),
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: (_searchResults.isEmpty && !_isLoading)
? NoDataFound(message: context.translation.noDataFound).center
: ListView.separated(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: _searchResults.length + (_isLoadingMore ? 1 : 0),
separatorBuilder: (_, __) => const SizedBox(height: 16),
itemBuilder: (context, index) {
if (_isLoading && _searchResults.isEmpty) {
return const SizedBox().toRequestShimmer(context, _isLoading);
}
// Show loading indicator at the bottom
if (index == _searchResults.length) {
return const ALoading().center;
}
final result = _searchResults[index];
return CleanerRequestItemView(
result: result,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CleanerSearchDetailsView(
requestId: result.id ?? 0,
taskResult: result,
),
),
);
},
);
},
),
),
],
),
),
);
}
}

@ -0,0 +1,346 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.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/timer/app_timer.dart';
import 'package:test_sa/models/timer_model.dart';
import 'dart:convert';
class OneTimeTaskPreviewScreen extends StatelessWidget {
static const String routeName = "/one_time_task_preview";
final OneTimeTaskData taskData;
final double totalWorkingHours;
final VoidCallback onConfirm;
const OneTimeTaskPreviewScreen({
Key? key,
required this.taskData,
required this.totalWorkingHours,
required this.onConfirm,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Debug logging
print('DEBUG Preview Screen:');
print('imageRooms count: ${taskData.imageRooms?.length ?? 0}');
print('attachments count: ${taskData.attachments?.length ?? 0}');
// Determine which images to show: captured images (imageRooms) or API attachments
final hasImageRooms = taskData.imageRooms != null && taskData.imageRooms!.isNotEmpty;
final hasAttachments = taskData.attachments != null && taskData.attachments!.isNotEmpty;
return Scaffold(
appBar: const DefaultAppBar(
title: 'Preview',
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Task Information Card
_buildTaskInfoCard(context),
16.height,
// Working Hours Card
if (totalWorkingHours > 0) ...[
_buildWorkingHoursCard(context),
16.height,
],
// Checklist Card
_buildChecklistCard(context),
16.height,
// Captured Images Card
if (hasImageRooms) ...[
_buildImagesCard(context),
16.height,
],
// Attachments Card (from API)
if (hasAttachments) ...[
_buildAttachmentsCard(context),
16.height,
],
],
),
),
),
// Footer with Confirm Button
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: context.translation.confirm,
buttonColor: AppColor.primary10,
onPressed: onConfirm,
),
),
],
),
),
);
}
Widget _buildTaskInfoCard(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget(
'Task Information',
),
12.height,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (taskData.oneTimeSetting?.taskTypeName != null) InfoTextWidget(label: 'Task Type', value: taskData.oneTimeSetting!.taskTypeName!),
if (taskData.siteName != null) InfoTextWidget(label: 'Site', value: taskData.siteName!),
if (taskData.buildingName != null) InfoTextWidget(label: 'Building', value: taskData.buildingName!),
if (taskData.floorName != null) InfoTextWidget(label: 'Floor', value: taskData.floorName!),
if (taskData.departmentName != null) InfoTextWidget(label: 'Department', value: taskData.departmentName!),
if (taskData.roomName != null) InfoTextWidget(label: 'Room', value: taskData.roomName!),
if (taskData.locationDescription != null) InfoTextWidget(label: 'Location', value: taskData.locationDescription!),
],
).paddingAll(16),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildWorkingHoursCard(BuildContext context) {
// Parse timer start time from taskData if available
DateTime? timerStartTime;
if (taskData.startTime != null) {
try {
timerStartTime = DateTime.parse(taskData.startTime!);
} catch (e) {
print('Error parsing start time: $e');
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget(
'Working Hours',
),
12.height,
if (timerStartTime != null)
AppTimer(
label: 'Working Time',
timer: TimerModel(
startAt: timerStartTime,
endAt: null,
durationInSecond: 0,
),
enabled: false,
// Disable controls, just show the timer
showTimer: true,
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
)
else
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InfoTextLabelWidget(
label: 'Total Working Hours',
),
InfoHeader16Widget(
CMRequestUtils.formatTotalWorkingHours(totalWorkingHours),
),
],
),
),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildChecklistCard(BuildContext context) {
final completedInstructions = taskData.instructions?.where((i) => i.isDone == true).toList() ?? [];
final totalInstructions = taskData.instructions?.length ?? 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InfoHeader16Widget(
'Checklist',
),
InfoTextLabelWidget(
label: '${completedInstructions.length}/$totalInstructions',
),
],
),
12.height,
ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemCount: completedInstructions.length,
separatorBuilder: (context, index) => const Divider(height: 1, color: AppColor.neutral30, thickness: 1).paddingOnly(top: 12, bottom: 12),
itemBuilder: (context, index) {
final instruction = completedInstructions[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.check_circle,
color: AppColor.primary10,
size: 20,
),
8.width,
Expanded(
child: InfoTextLabelWidget(
label: instruction.instructionText ?? '',
),
),
],
),
if (instruction.comment != null && instruction.comment!.isNotEmpty) ...[
8.height,
Container(
width: double.infinity, // Force full width
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoTextLabelWidget(
label: 'Comment:',
),
4.height,
InfoTextLabelWidget(
label: instruction.comment!,
),
],
),
),
],
],
);
},
).paddingAll(16),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildImagesCard(BuildContext context) {
// Show captured images (base64 from camera)
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget('Captured Images'),
12.height,
Wrap(
spacing: 8,
runSpacing: 8,
children: taskData.imageRooms!.map((imageString) {
return GestureDetector(
onTap: () {
context.fullScreenImageViewer(_buildImage(imageString));
},
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _buildImage(imageString),
),
),
);
}).toList(),
),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildAttachmentsCard(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget(
'Attachments',
),
FilesList(
images: taskData.attachments!.map((attachment) => URLs.getFileUrl(attachment.name ?? '') ?? '').toList(),
showAsListView: true,
),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildImage(String imageString) {
try {
// imageString format: "filename|base64data"
final parts = imageString.split('|');
final base64Data = parts.length > 1 ? parts[1] : imageString;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.cover,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48);
}
}
Widget _buildInfoRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: InfoTextLabelWidget(
label: '$label:',
),
),
Expanded(
child: InfoTextLabelWidget(
label: value,
),
),
],
),
);
}
}

@ -0,0 +1,444 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_preview_screen.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/pages/land_page/land_page.dart';
import 'package:test_sa/views/widgets/item_views/info_date_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/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/views/widgets/timer/app_timer.dart';
import 'package:test_sa/models/timer_model.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class OneTimeTaskView extends StatefulWidget {
static const String routeName = "/one_time_task";
final int taskId;
final OneTimeTaskData? initialData;
const OneTimeTaskView({Key? key, required this.taskId, this.initialData}) : super(key: key);
@override
State<OneTimeTaskView> createState() => _OneTimeTaskViewState();
}
class _OneTimeTaskViewState extends State<OneTimeTaskView> {
OneTimeTaskData? _taskData;
bool _isLoading = false;
DateTime? _timerStart;
List<ChecklistItem>? _checklistItems; // Cache checklist items
@override
void initState() {
super.initState();
// Use initial data if provided, otherwise load from API
if (widget.initialData != null) {
_taskData = widget.initialData;
// Initialize imageRooms as empty list if null
_taskData?.imageRooms ??= [];
// Start timer automatically if task is in progress
if (_taskData?.statusValue == 1 && _taskData?.startTime != null) {
_timerStart = DateTime.parse(_taskData!.startTime!);
}
} else {
_loadTaskData();
}
}
Future<void> _loadTaskData() async {
setState(() {
_isLoading = true;
});
try {
final result = await CleanerProvider().getOneTimeTaskById(id: widget.taskId);
if (result['success'] && result['data'] != null) {
setState(() {
_taskData = OneTimeTaskData.fromJson(result['data']);
// Initialize imageRooms as empty list if null
_taskData?.imageRooms ??= [];
// Convert attachments from API to imageRooms format for display
if ((_taskData?.imageRooms?.isEmpty ?? true) && (_taskData?.attachments != null && _taskData!.attachments!.isNotEmpty)) {
_taskData!.imageRooms = _taskData!.attachments!.map((attachment) => URLs.getFileUrl(attachment.name ?? '') ?? '').where((url) => url.isNotEmpty).toList();
}
_isLoading = false;
// Start timer automatically if task is in progress
if (_taskData?.statusValue == 1 && _taskData?.startTime != null) {
_timerStart = DateTime.parse(_taskData!.startTime!);
}
});
} else {
setState(() {
_isLoading = false;
});
if (mounted) {
_showErrorDialog(context, result['message'] ?? 'Failed to load task');
}
}
} catch (e) {
setState(() {
_isLoading = false;
});
if (mounted) {
_showErrorDialog(context, 'Failed to load task data');
}
}
}
bool get isClosed => _taskData?.statusValue == 2;
String _formatDuration(Duration duration) {
if (duration.inDays > 0) {
final hours = duration.inHours % 24;
final minutes = duration.inMinutes % 60;
return '${duration.inDays}d ${hours}h ${minutes}m';
} else if (duration.inHours > 0) {
final minutes = duration.inMinutes % 60;
final seconds = duration.inSeconds % 60;
return '${duration.inHours}h ${minutes}m ${seconds}s';
} else if (duration.inMinutes > 0) {
final seconds = duration.inSeconds % 60;
return '${duration.inMinutes}m ${seconds}s';
} else {
return '${duration.inSeconds}s';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: DefaultAppBar(
title: 'One Time Task'.addTranslation,
onWillPopScope: null,
onBackPress: () {
WidgetsBinding.instance.addPostFrameCallback((_) {
UserProvider userProvider = Provider.of<UserProvider>(context, listen: false);
DashBoardProvider dashBoardProvider = Provider.of<DashBoardProvider>(context, listen: false);
dashBoardProvider.getDashBoardCount(usersType: userProvider.user!.type!);
dashBoardProvider.resetRequestDataList();
dashBoardProvider.getRequestDetail(usersType: userProvider.user!.type!, status: dashBoardProvider.tabs[dashBoardProvider.currentListIndex!].tag);
});
Navigator.pop(context);
},
),
body: _isLoading
? const ALoading()
: _taskData != null
? SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTaskInfoCard(context),
12.height,
CleanerChecklistWidget(
isClosed: isClosed,
checklistItems: _convertInstructionsToChecklist(),
onChecklistChanged: (value) {
setState(() {
_updateInstructions(value);
});
},
capturedImages: _taskData!.imageRooms ?? [],
onImagesChanged: (images) {
setState(() {
_taskData!.imageRooms = images;
});
},
),
],
),
),
),
if (!isClosed) ...[
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: context.translation.complete,
buttonColor: AppColor.primary10,
onPressed: _onComplete,
),
),
]
],
),
)
: NoDataFound(message: context.translation.noDataFound).center,
).handlePopScope(
cxt: context,
showPopUp: false,
onSave: () {
Navigator.pop(context);
},
);
}
Widget _buildTaskInfoCard(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_taskData?.statusName != null)
StatusLabel(
radius: 4,
label: _taskData!.statusName!,
textColor: AppColor.getHistoryLogStatusTextColorByName(context, _taskData!.statusName!),
backgroundColor: AppColor.getHistoryLogStatusColorByName(context, _taskData!.statusName!),
),
if (_taskData?.createdDate != null) InfoDateWidget(_taskData!.createdDate!.toString()),
],
),
12.height,
if (_taskData?.oneTimeSetting?.taskTypeName != null)
InfoTextLabelWidget(
label: _taskData!.oneTimeSetting!.taskTypeName!,
),
// Text(
// _taskData!.oneTimeSetting!.taskTypeName!,
// style: AppTextStyles.heading4.copyWith(
// color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
// ),
// ),
// 8.height,
if (_taskData?.oneTimeSetting?.instructionDescriptionName != null)
InfoTextLabelWidget(
label: _taskData!.oneTimeSetting!.instructionDescriptionName!,
),
if (_taskData?.siteName != null) InfoTextWidget(label: 'Site', value: _taskData!.siteName!),
if (_taskData?.buildingName != null) InfoTextWidget(label: 'Building', value: _taskData!.buildingName!),
if (_taskData?.floorName != null) InfoTextWidget(label: 'Floor', value: _taskData!.floorName!),
if (_taskData?.departmentName != null) InfoTextWidget(label: 'Department', value: _taskData!.departmentName!),
if (_taskData?.roomName != null) InfoTextWidget(label: 'Room', value: _taskData!.roomName!),
if (_taskData?.locationDescription != null) InfoTextWidget(label: 'Location', value: _taskData!.locationDescription!),
if (_timerStart != null && !isClosed) ...[
12.height,
AppTimer(
label: 'Working Time',
timer: TimerModel(
startAt: _timerStart,
endAt: null,
durationInSecond: 0,
),
enabled: false,
// Disable controls, just show the timer
showTimer: true,
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
),
],
// Display total duration if task is completed
if (isClosed && _taskData?.startTime != null && _taskData?.endTime != null) ...[
// 12.height,
Text(
'Total Duration: ${_formatDuration(DateTime.parse(_taskData!.endTime!).difference(DateTime.parse(_taskData!.startTime!)))}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
],
],
),
],
).toShadowContainer(context);
}
List<ChecklistItem> _convertInstructionsToChecklist() {
// If checklist items are already created, reuse them
if (_checklistItems != null) {
// Update the existing items with latest data from instructions
for (var item in _checklistItems!) {
final instruction = _taskData?.instructions?.firstWhere((i) => i.id == item.id, orElse: () => Instruction());
if (instruction?.id != null) {
item.isChecked = instruction!.isDone ?? false;
// Only update comment if it's different to avoid cursor jumping
if (item.comment != (instruction.comment ?? '')) {
item.comment = instruction.comment ?? '';
}
}
}
return _checklistItems!;
}
// Create new checklist items only on first call
_checklistItems = _taskData?.instructions?.map((instruction) {
return ChecklistItem(
id: instruction.id ?? 0,
title: instruction.instructionText ?? '',
isChecked: instruction.isDone ?? false,
isMandatory: instruction.isRequired ?? false,
comment: instruction.comment ?? '',
);
}).toList() ??
[];
return _checklistItems!;
}
void _updateInstructions(List<ChecklistItem> checklist) {
_taskData?.instructions = checklist.map((item) {
return Instruction(
id: item.id,
instructionTextId: _taskData?.instructions?.firstWhere((i) => i.id == item.id).instructionTextId,
instructionText: item.title,
isDone: item.isChecked,
isRequired: item.isMandatory,
comment: item.comment,
);
}).toList();
}
void _onComplete() {
// Validate mandatory checklist items
final uncheckedMandatory = _taskData?.instructions?.where((item) => (item.isRequired ?? false) && !(item.isDone ?? false)).toList() ?? [];
if (uncheckedMandatory.isNotEmpty) {
_showErrorDialog(context, "Please complete all mandatory tasks (marked with *)");
return;
}
// Validate that at least one image is captured
final capturedImages = _taskData?.imageRooms ?? [];
if (capturedImages.isEmpty) {
_showErrorDialog(context, "Please capture at least one image before completing");
return;
}
double totalWorkingHours = 0;
if (_timerStart != null) {
final duration = DateTime.now().difference(_timerStart!);
totalWorkingHours = duration.inSeconds / 3600.0;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => OneTimeTaskPreviewScreen(
taskData: _taskData!,
totalWorkingHours: totalWorkingHours,
onConfirm: () {
Navigator.pop(context); // Close preview screen
_performComplete(); // Proceed with completion
},
),
),
);
}
Future<void> _performComplete() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AppLazyLoading(),
);
try {
final instructionsPayload = _taskData?.instructions?.map((instruction) {
return {
'id': instruction.id,
'isDone': instruction.isDone,
'comment': instruction.comment,
};
}).toList() ??
[];
final attachmentsPayload = _taskData?.imageRooms?.map((imageString) {
// imageString format: "filename|base64data"
final parts = imageString.split('|');
final originalName = parts.isNotEmpty ? parts[0] : 'image.jpg';
final base64Data = parts.length > 1 ? parts[1] : imageString;
return {
'id': 0,
'name': "$originalName|$base64Data", // Send full formatted string
'originalName': originalName,
};
}).toList() ??
[];
final result = await CleanerProvider().completeOneTimeTask(
id: _taskData!.id!,
instructions: instructionsPayload,
attachments: attachmentsPayload,
);
if (mounted) Navigator.pop(context); // Close loading dialog
if (result['success']) {
if (mounted) {
// Navigate to dashboard
Navigator.of(context).pushNamedAndRemoveUntil(
LandPage.routeName,
(routes) => false,
);
}
} else {
if (mounted) {
_showErrorDialog(context, result['message'] ?? 'Failed to complete task');
}
}
} catch (e) {
if (mounted) Navigator.pop(context);
if (mounted) {
_showErrorDialog(context, 'Failed to complete task. Please try again.');
}
}
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
@override
void dispose() {
// Dispose all checklist item controllers
if (_checklistItems != null) {
for (var item in _checklistItems!) {
item.dispose();
}
}
super.dispose();
}
}

@ -102,6 +102,7 @@ class CleanerProvider extends ChangeNotifier {
int? floorId,
int? departmentId,
int? roomId,
int? taskTypeId,
}) async {
Response response;
try {
@ -115,6 +116,7 @@ class CleanerProvider extends ChangeNotifier {
"floorId": floorId,
"departmentId": departmentId,
"roomId": roomId,
"taskTypeId": taskTypeId,
},
);
@ -145,6 +147,7 @@ class CleanerProvider extends ChangeNotifier {
"planRecurrentTaskId": planRecurrentTaskId,
"qrCode": qrCode,
},
showToast: false, // Disable automatic toast message
);
if (response.statusCode >= 200 && response.statusCode < 300) {
@ -159,6 +162,227 @@ class CleanerProvider extends ChangeNotifier {
}
}
/// Check if QR code matches the same room for a plan recurrent task
/// Returns a map with success status and message from backend
Future<Map<String, dynamic>> checkQRSameRoomWithMessage({
required int planRecurrentTaskId,
required String qrCode,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.checkQRSameRoom,
body: {
"planRecurrentTaskId": planRecurrentTaskId,
"qrCode": qrCode,
},
showToast: false, // Disable automatic toast message
);
// Parse the response body to extract the message
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
final isValid = responseData["data"] == true || responseData["isSuccess"] == true;
return {
"success": isValid,
"message": responseData["message"] ?? (isValid ? "QR code validated successfully" : "QR code validation failed"),
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"message": responseData["message"] ?? "QR code validation failed",
};
}
} catch (error) {
debugPrint('Error checking QR same room: $error');
return {
"success": false,
"message": "Failed to validate QR code. Please check your connection and try again.",
};
}
}
/// Scan QR Code and get task details
/// Returns a map with success status, message, and data from backend
Future<Map<String, dynamic>> scanQRCode({
required String qrCode,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.scanQRCode,
body: {
"qrCode": qrCode,
},
showToast: false, // Disable automatic toast message
);
// Parse the response body to extract the message
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "QR code scanned successfully",
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "QR code scan failed",
};
}
} catch (error) {
debugPrint('Error scanning QR code: $error');
return {
"success": false,
"data": null,
"message": "Failed to scan QR code. Please check your connection and try again.",
};
}
}
/// Proceed with OneTime Task request
/// Returns a map with success status, message, and data from backend
Future<Map<String, dynamic>> proceedOneTimeTask({
required int oneTimeSettingId,
required int siteId,
required int buildingId,
required int floorId,
required int departmentId,
required int roomId,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.proceedRequest,
body: {
"oneTimeSettingId": oneTimeSettingId,
"siteId": siteId,
"buildingId": buildingId,
"floorId": floorId,
"departmentId": departmentId,
"roomId": roomId,
},
showToast: false, // Disable automatic toast message
);
// Parse the response body to extract the message
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "Request proceeded successfully",
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to proceed with request",
};
}
} catch (error) {
debugPrint('Error proceeding with request: $error');
return {
"success": false,
"data": null,
"message": "Failed to proceed with request. Please check your connection and try again.",
};
}
}
/// Get OneTime Task by ID using getRequestById endpoint
Future<Map<String, dynamic>> getOneTimeTaskById({
required int id,
}) async {
Response response;
try {
response = await ApiManager.instance.get(
'${URLs.getRequestById}?id=$id',
);
// Parse the response body
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "Task retrieved successfully",
};
} else {
// Error response
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to retrieve task",
};
}
} catch (error) {
debugPrint('Error getting one time task: $error');
return {
"success": false,
"data": null,
"message": "Failed to retrieve task. Please check your connection and try again.",
};
}
}
/// Complete OneTime Task
Future<Map<String, dynamic>> completeOneTimeTask({
required int id,
required List<Map<String, dynamic>> instructions,
List<Map<String, dynamic>>? attachments,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.completeRequest,
body: {
"id": id,
"instructions": instructions,
"attachments": attachments ?? [],
},
showToast: false,
);
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "Task completed successfully",
};
} else {
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to complete task",
};
}
} catch (error) {
debugPrint('Error completing one time task: $error');
return {
"success": false,
"data": null,
"message": "Failed to complete task. Please check your connection and try again.",
};
}
}
void reset() {
_isLoading = false;
_searchResults = null;

@ -1,85 +0,0 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/new_models/floor.dart';
class FmsFloorProvider extends ChangeNotifier {
//reset provider data
void reset() {
_floors = null;
_stateCode = null;
}
// state code of current request to defied error message
int? _stateCode;
int? get stateCode => _stateCode;
// contain floors data
List<Floor>? _floors;
List<Floor>? get floors => _floors;
// loading state
bool _loading = false;
bool get isLoading => _loading;
set isLoading(bool isLoading) {
_loading = isLoading;
notifyListeners();
}
/// Get floors by site and building ID
Future<int> getFloorsBySiteAndBuilding(int siteId, int buildingId) async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerFloors,
body: {
"siteId": siteId,
"buildingId": buildingId,
},
);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
List floorsListJson = json.decode(response.body)["data"];
_floors = floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
}
_loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
_loading = false;
_stateCode = -1;
notifyListeners();
return -1;
}
}
/// Get floors list directly without provider state
Future<List<Floor>> getFloorsListBySiteAndBuilding(int siteId, int buildingId) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerFloors,
body: {
"siteId": siteId,
"buildingId": buildingId,
},
);
if (response.statusCode >= 200 && response.statusCode < 300) {
List floorsListJson = json.decode(response.body)["data"];
return floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
}
return [];
} catch (error) {
return [];
}
}
}

@ -1,140 +0,0 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/lookup.dart';
class LocationDescriptionResponse {
List<Lookup>? locationDescriptions;
List<DepartmentRoomItem>? deptRoom;
LocationDescriptionResponse({
this.locationDescriptions,
this.deptRoom,
});
factory LocationDescriptionResponse.fromJson(Map<String, dynamic> json) {
return LocationDescriptionResponse(
locationDescriptions: json['locationDescriptions'] != null
? (json['locationDescriptions'] as List)
.map((e) => Lookup(name: e['locationDescriptionName']))
.toList()
: null,
deptRoom: json['deptRoom'] != null
? (json['deptRoom'] as List)
.map((e) => DepartmentRoomItem.fromJson(e))
.toList()
: null,
);
}
}
class DepartmentRoomItem {
String? locationDescriptionName;
String? deptAndRoomName;
String? deptName;
String? roomName;
int? deptId;
int? roomId;
DepartmentRoomItem({
this.locationDescriptionName,
this.deptAndRoomName,
this.deptName,
this.roomName,
this.deptId,
this.roomId,
});
factory DepartmentRoomItem.fromJson(Map<String, dynamic> json) {
return DepartmentRoomItem(
locationDescriptionName: json['locationDescriptionName'],
deptAndRoomName: json['deptAndRoomName'],
deptName: json['deptName'],
roomName: json['roomName'],
deptId: json['deptId'],
roomId: json['roomId'],
);
}
}
class FmsLocationDescriptionProvider extends ChangeNotifier {
//reset provider data
void reset() {
_locationDescriptions = null;
_deptRoomList = null;
_stateCode = null;
}
// state code of current request to defied error message
int? _stateCode;
int? get stateCode => _stateCode;
// contain location descriptions data
List<Lookup>? _locationDescriptions;
List<Lookup>? get locationDescriptions => _locationDescriptions;
// contain dept room list
List<DepartmentRoomItem>? _deptRoomList;
List<DepartmentRoomItem>? get deptRoomList => _deptRoomList;
// loading state
bool _loading = false;
bool get isLoading => _loading;
set isLoading(bool isLoading) {
_loading = isLoading;
notifyListeners();
}
/// Get location descriptions by site, building, and floor ID
Future<int> getLocationDescriptions(int siteId, int buildingId, int floorId) async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.get(
"${URLs.getCustomerLocationDescription}?siteId=$siteId&buildingId=$buildingId&floorId=$floorId",
enableToastMessage: false,
);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
final jsonData = json.decode(response.body)["data"];
LocationDescriptionResponse locationResponse = LocationDescriptionResponse.fromJson(jsonData);
_locationDescriptions = locationResponse.locationDescriptions;
_deptRoomList = locationResponse.deptRoom;
}
_loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
_loading = false;
_stateCode = -1;
notifyListeners();
return -1;
}
}
/// Get location descriptions and dept room list directly without provider state
Future<LocationDescriptionResponse?> getLocationDescriptionsList(int siteId, int buildingId, int floorId) async {
Response response;
try {
response = await ApiManager.instance.get(
"${URLs.getCustomerLocationDescription}?siteId=$siteId&buildingId=$buildingId&floorId=$floorId",
enableToastMessage: false,
);
if (response.statusCode >= 200 && response.statusCode < 300) {
final jsonData = json.decode(response.body)["data"];
return LocationDescriptionResponse.fromJson(jsonData);
}
return null;
} catch (error) {
return null;
}
}
}

@ -4,22 +4,22 @@ import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/new_models/building.dart';
import 'package:test_sa/modules/cleaner_module/models/task_type.dart';
class FmsBuildingProvider extends ChangeNotifier {
class TaskTypeProvider extends ChangeNotifier {
//reset provider data
void reset() {
_buildings = null;
_taskTypes = null;
_stateCode = null;
}
// state code of current request to defied error message
// state code of current request to define error message
int? _stateCode;
int? get stateCode => _stateCode;
// contain buildings data
List<Building>? _buildings;
List<Building>? get buildings => _buildings;
// contain task types data
List<TaskType>? _taskTypes;
List<TaskType>? get taskTypes => _taskTypes;
// loading state
bool _loading = false;
@ -30,24 +30,19 @@ class FmsBuildingProvider extends ChangeNotifier {
notifyListeners();
}
/// Get buildings by site ID
Future<int> getBuildingsBySiteId(int siteId) async {
/// Get task types
Future<int> getTaskTypes() async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerBuildings,
body: {
"siteId": siteId,
},
);
response = await ApiManager.instance.get(URLs.getTaskType);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
List buildingsListJson = json.decode(response.body)["data"];
_buildings = buildingsListJson.map((building) => Building.fromJson(building)).toList();
List taskTypesListJson = json.decode(response.body)["data"];
_taskTypes = taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList();
}
_loading = false;
notifyListeners();
@ -60,20 +55,15 @@ class FmsBuildingProvider extends ChangeNotifier {
}
}
/// Get buildings list directly without provider state
Future<List<Building>> getBuildingsListBySiteId(int siteId) async {
/// Get task types list directly without provider state
Future<List<TaskType>> getTaskTypesList() async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerBuildings,
body: {
"siteId": siteId,
},
);
response = await ApiManager.instance.get(URLs.getTaskType);
if (response.statusCode >= 200 && response.statusCode < 300) {
List buildingsListJson = json.decode(response.body)["data"];
return buildingsListJson.map((building) => Building.fromJson(building)).toList();
List taskTypesListJson = json.decode(response.body)["data"];
return taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList();
}
return [];
} catch (error) {
@ -81,3 +71,4 @@ class FmsBuildingProvider extends ChangeNotifier {
}
}
}

@ -1,10 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_picker/image_picker.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
@ -13,15 +9,19 @@ import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
import 'package:test_sa/modules/cleaner_module/widgets/multi_image_picker.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
class CleanerChecklistWidget extends StatefulWidget {
final List<ChecklistItem> checklistItems;
final Function(List<ChecklistItem>)? onChecklistChanged;
final String? capturedImageBase64;
final Function(String?)? onImageCaptured;
bool isClosed;
final List<String> capturedImages;
final Function(List<String>)? onImagesChanged;
final bool isClosed;
CleanerChecklistWidget({Key? key, required this.checklistItems, this.onChecklistChanged, this.capturedImageBase64, this.onImageCaptured, this.isClosed = false}) : super(key: key);
const CleanerChecklistWidget({Key? key, required this.checklistItems, this.onChecklistChanged, required this.capturedImages, this.onImagesChanged, this.isClosed = false}) : super(key: key);
@override
State<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState();
@ -37,16 +37,17 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
Text(
'Checklist'.addTranslation,
style: AppTextStyles.heading4.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
16.height,
...widget.checklistItems.map((item) => _buildChecklistItem(context, item)).toList(),
// 16.height,
InfoHeader16Widget('Checklist'),
ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (cxt, index) => _buildChecklistItem(context, widget.checklistItems[index]),
separatorBuilder: (cxt, index) => const Divider().defaultStyle(context),
itemCount: widget.checklistItems.length)
.toShadowContainer(context),
_buildImageCaptureWidget(context),
],
);
@ -59,28 +60,68 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Checkbox with title
Row(
children: [
Checkbox(
// visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
AbsorbPointer(
absorbing: true,
child: Transform.scale(
scale: 1.2,
child: Checkbox(
value: item.isChecked,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: AppColor.loadingColor(context), width: 1.0),
visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
activeColor: AppColor.primary10,
onChanged: (bool? value) {},
),
),
),
10.width,
RichText(
text: TextSpan(
children: [
TextSpan(
text: item.title,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.lightTextColor(context)),
),
if (item.isMandatory)
TextSpan(
text: ' *',
style: AppTextStyles.bodyText.copyWith(
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
],
),
).expanded,
],
).onPress(() {
setState(() {
item.isChecked = !item.isChecked;
if (!item.isChecked) {
item.commentController.clear();
}
});
widget.onChecklistChanged?.call(widget.checklistItems);
}),
/* ListTileTheme(
horizontalTitleGap: 0,
contentPadding: EdgeInsets.zero,
minVerticalPadding: 0,
child: Container(
color: Colors.amberAccent,
child: CheckboxListTile(
value: item.isChecked,
minVerticalPadding: 0,
controlAffinity: ListTileControlAffinity.leading,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
contentPadding: EdgeInsets.zero,
checkboxScaleFactor: 1.2,
side: BorderSide(color: Colors.black.withValues(alpha: .05)),
visualDensity: const VisualDensity(horizontal: -0.0, vertical: -4.0),
activeColor: AppColor.primary10,
onChanged: (bool? value) {
setState(() {
item.isChecked = value ?? false;
if (!item.isChecked) {
item.commentController.clear();
}
});
// Notify parent of changes
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
8.width,
Expanded(
child: RichText(
title: RichText(
text: TextSpan(
children: [
TextSpan(
@ -101,32 +142,36 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
],
),
),
onChanged: (bool? value) {
setState(() {
item.isChecked = value ?? false;
if (!item.isChecked) {
item.commentController.clear();
}
});
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
],
),
// Comment box - only visible when checked
),
),*/
if (item.isChecked) ...[
16.height,
12.height,
AppTextFormField(
controller: item.commentController,
labelText: context.translation.comment,
backgroundColor: AppColor.fieldBgColor(context),
showShadow: false,
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
labelStyle: AppTextStyles.textFieldLabelStyle,
alignLabelWithHint: true,
textInputType: TextInputType.multiline,
onChange: (value) {
item.comment = value;
// Notify parent of changes
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
],
],
).toShadowContainer(context).paddingOnly(bottom: 16),
),
);
}
@ -135,217 +180,142 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Captured Image'.addTranslation,
style: AppTextStyles.heading6.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
InfoHeader16Widget('Captured Image'),
8.height,
_buildImageWidget(context),
],
).toShadowContainer(context);
}
static const int maxImages = 2;
/// Build the image widget with loading indicator for network images
Widget _buildImageWidget(BuildContext context) {
if (widget.capturedImageBase64 == null || widget.capturedImageBase64!.isEmpty) {
// Show camera icon when no image
return Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.camera_alt,
size: 48,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
).onPress(() async {
final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 70, maxWidth: 800, maxHeight: 800);
if (pickedFile != null) {
// Convert image to Base64 with filename prefix (same format as CM request)
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
if (widget.capturedImages.isEmpty) {
// Show add image button when no image
return GestureDetector(
onTap: widget.isClosed
? null
: () async {
// Only allow camera - pick one image at a time
final pickedFile = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 70,
maxWidth: 800,
maxHeight: 800,
);
// Update parent with captured image
widget.onImageCaptured?.call(formattedImage);
}
});
}
if (pickedFile != null) {
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
Widget imageWidget;
// Add to existing images (max 2)
final updatedImages = List<String>.from(widget.capturedImages);
updatedImages.add(formattedImage);
final imageString = widget.capturedImageBase64!;
widget.onImagesChanged?.call(updatedImages);
}
},
child: Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
width: 2,
style: BorderStyle.solid,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate,
size: 32,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
4.height,
InfoTextLabelWidget(label: 'Add Image'),
// First check if it's base64 encoded (contains | separator)
if (imageString.contains('|')) {
// Format: "filename|base64data"
try {
final base64Data = imageString.split('|').last;
imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover);
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
);
} catch (e) {
debugPrint('Error decoding base64 image: $e');
return _buildCameraIcon(context);
}
// Text(
// 'Add Image',
// style: AppTextStyles.tinyFont.copyWith(
// color: context.isDark ? AppColor.white10 : AppColor.black10,
// ),
// ),
],
),
),
);
}
// Check if it's a local file path
if (_isLocalUrl(imageString)) {
try {
imageWidget = Image.file(File(imageString), fit: BoxFit.cover);
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
);
} catch (e) {
debugPrint('Error loading local file: $e');
return _buildCameraIcon(context);
}
}
// Check if images are URLs from API (like ServiceRequestDetailView does)
final bool hasApiImages = widget.capturedImages.any((img) => img.startsWith('http://') || img.startsWith('https://'));
// If not local and not base64, it's a server filename - use NetworkImage with loading indicator
final imageUrl = URLs.getFileUrl(imageString);
if (imageUrl != null) {
imageWidget = Image.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return child;
}
return Center(
child: CircularProgressIndicator(
color: AppColor.primary10,
value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
),
);
},
errorBuilder: (context, error, stackTrace) {
debugPrint('Error loading network image: $error');
return _buildCameraIcon(context);
},
);
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
// If images are from API (URLs), use FilesList widget like ServiceRequestDetailView
if (hasApiImages && widget.isClosed) {
return FilesList(
images: widget.capturedImages,
showAsListView: false,
);
}
return _buildCameraIcon(context);
}
/// Build camera icon widget
Widget _buildCameraIcon(BuildContext context) {
return Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.camera_alt,
size: 48,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
// Otherwise, use MultiImagePicker for base64 images or when editing
return MultiImagePicker(
imageUrls: widget.capturedImages,
enabled: !widget.isClosed,
maxImages: 2,
// Enforce max 2 images
onImageTap: (index, imageUrl) {
context.fullScreenImageViewer(_buildImageForViewing(imageUrl));
},
onDelete: (index) {
// Handle image delete
if (index >= 0) {
setState(() {
widget.capturedImages.removeAt(index);
});
widget.onImagesChanged?.call(widget.capturedImages);
}
},
onAdd: (imageUrl) {
// Handle new image addition
setState(() {
widget.capturedImages.add(imageUrl);
});
widget.onImagesChanged?.call(widget.capturedImages);
},
);
}
/// Check if the URL is a local file path
/// Same logic as multi_image_picker_item.dart
bool _isLocalUrl(String url) {
if (url.isEmpty) return false;
return url.startsWith("/") || url.startsWith("file://") || (url.length > 1 && url.substring(1).startsWith(':\\'));
/// Build image widget for viewing based on source type
Widget _buildImageForViewing(String imageUrl) {
try {
// Check if it's a URL from API
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return Image.network(
imageUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
},
);
}
// Otherwise, it's base64 encoded
final parts = imageUrl.split('|');
final base64Data = parts.length > 1 ? parts[1] : imageUrl;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.contain,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
}
}
@override
@ -353,26 +323,6 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
// Note: Don't dispose controllers here as they're managed by ChecklistItem
super.dispose();
}
Widget _delete() {
if (widget.isClosed) return const SizedBox();
return Container(
height: 45.toScreenHeight,
width: 45.toScreenHeight,
margin: const EdgeInsets.only(top: 4, right: 4),
decoration: BoxDecoration(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.4),
border: Border.all(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.7),
),
borderRadius: BorderRadius.circular(30),
//boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)],
),
child: "delete".toSvgAsset())
.onPress(() {
widget.onImageCaptured?.call(null);
});
}
}
/// Model class for checklist items
@ -390,10 +340,7 @@ class ChecklistItem {
required this.isChecked,
required this.comment,
this.isMandatory = false,
}) : commentController = TextEditingController(text: comment) {
// Update the controller text whenever comment changes
commentController.text = comment;
}
}) : commentController = TextEditingController(text: comment);
// Convert to JSON for API submission
Map<String, dynamic> toJson() {

@ -0,0 +1,197 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
class MultiImagePicker extends StatelessWidget {
final List<String> imageUrls;
final Function(int index, String imageUrl)? onImageTap;
final Function(int index)? onDelete;
final Function(String imageUrl)? onAdd;
final bool enabled;
final int maxImages;
const MultiImagePicker({
Key? key,
required this.imageUrls,
this.onImageTap,
this.onDelete,
this.onAdd,
this.enabled = true,
this.maxImages = 2,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Display existing images horizontally with add button
Row(
children: [
// Display existing images
...List.generate(
imageUrls.length,
(index) {
return Padding(
padding: EdgeInsets.only(right: 8.toScreenWidth),
child: _buildImageItem(context, index),
);
},
),
// Add image button (only show if under max limit)
if (enabled && imageUrls.length < maxImages) _buildAddImageButton(context),
],
),
],
);
}
Widget _buildImageItem(BuildContext context, int index) {
final imageUrl = imageUrls[index];
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _getImageWidget(imageUrl),
),
).onPress(() {
if (onImageTap != null) {
onImageTap!(index, imageUrl);
} else {
context.fullScreenImageViewer(
_getImageWidget(imageUrl),
);
}
}),
if (enabled)
Container(
height: 32,
width: 32,
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.9),
border: Border.all(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.7),
),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.close, color: Color(0xffF63939), size: 20),
).onPress(() {
if (onDelete != null) {
onDelete!(index);
}
}),
],
);
}
Widget _buildAddImageButton(BuildContext context) {
return GestureDetector(
onTap: () => _pickImage(context),
child: Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
width: 2,
style: BorderStyle.solid,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate,
size: 32,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
4.height,
Text(
'Add Image',
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
),
],
),
),
);
}
Widget _getImageWidget(String imageUrl) {
// Check if it's base64 encoded (contains | separator)
if (imageUrl.contains('|')) {
try {
final base64Data = imageUrl.split('|').last;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.cover,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48);
}
}
// Check if it's a local file path
if (CMRequestUtils.isLocalUrl(imageUrl)) {
return Image.file(
File(imageUrl),
fit: BoxFit.cover,
);
}
// Otherwise treat as network URL
return Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48);
},
);
}
void _pickImage(BuildContext context) async {
// Directly open camera - no bottom sheet
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 70,
maxWidth: 800,
maxHeight: 800,
);
if (pickedFile != null) {
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
// Call the onAdd callback to notify parent
if (onAdd != null) {
onAdd!(formattedImage);
}
}
}
}

@ -0,0 +1,112 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/all_requests_and_count_model.dart';
import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/item_views/info_date_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/requests/request_status.dart';
class OneTimeTaskItemView extends StatelessWidget {
final Data? requestData;
final RequestsDetails? requestDetails;
final bool showShadow;
const OneTimeTaskItemView({Key? key, this.requestData, this.requestDetails, this.showShadow = true}) : super(key: key);
@override
Widget build(BuildContext context) {
if (requestData != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusLabel(
label: requestData!.statusName ?? '',
textColor: AppColor.getRequestStatusTextColorByName(context, requestData!.statusName ?? ''),
backgroundColor: AppColor.getRequestStatusColorByName(context, requestData!.statusName ?? ''),
),
1.width.expanded,
InfoDateWidget(requestData!.transactionDate ?? ""),
],
),
8.height,
InfoHeaderWidget(requestData?.typeTransaction ?? 'One Time Task'.addTranslation),
InfoTextWidget(label: context.translation.taskNo, value: requestData!.requestNo),
if (requestData?.siteName != null) InfoTextWidget(label: context.translation.site, value: requestData!.siteName),
if (requestData!.buildingName != null) InfoTextWidget(label: context.translation.building, value: requestData!.buildingName),
if (requestData!.departmentName != null) InfoTextWidget(label: context.translation.department, value: requestData!.departmentName),
if (requestData!.floorName != null) InfoTextWidget(label: context.translation.floor, value: requestData!.floorName),
InfoTextWidget(
label: context.translation.room,
value: requestData!.roomName,
showArrow: true,
),
],
).toShadowContainer(context, withShadow: showShadow).onPress(() {
// Navigate to editable view for in-progress tasks
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: requestData!.id ?? 0,
initialData: null,
),
),
);
});
}
if (requestDetails != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusLabel(
label: requestDetails!.status!,
textColor: AppColor.getRequestStatusTextColorByName(context, requestDetails!.status!),
backgroundColor: AppColor.getRequestStatusColorByName(context, requestDetails!.status!),
),
1.width.expanded,
InfoDateWidget(requestDetails!.date ?? ""),
],
),
8.height,
InfoHeaderWidget(requestDetails?.nameOfType ?? 'One Time Task'.addTranslation),
InfoTextWidget(label: context.translation.taskNo, value: requestDetails!.requestNo),
if (requestDetails!.siteName != null) InfoTextWidget(label: context.translation.site, value: requestDetails!.siteName),
if (requestDetails!.buildingName != null) InfoTextWidget(label: context.translation.building, value: requestDetails!.buildingName),
if (requestDetails!.departmentName != null) InfoTextWidget(label: context.translation.department, value: requestDetails!.departmentName),
if (requestDetails!.floorName != null) InfoTextWidget(label: context.translation.floor, value: requestDetails!.floorName),
InfoTextWidget(
label: context.translation.room,
value: requestDetails!.roomName,
showArrow: true,
),
],
).toShadowContainer(context, withShadow: showShadow).onPress(() async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: requestDetails!.id ?? 0,
initialData: null,
),
),
);
});
}
return const SizedBox();
}
}

@ -121,7 +121,10 @@ class _CMDetailPageState extends State<CMDetailPage> {
}),
if (isNurse)
IconButton(
icon: 'qr'.toSvgAsset(height: 24, width: 24),
icon: 'qr'.toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
tooltip: "QR Code",
onPressed: () {
ServiceRequestBottomSheet.getQRCodeBottomSheet(context: context);
},
@ -173,11 +176,16 @@ class _CMDetailPageState extends State<CMDetailPage> {
IconButton(
icon: "download".toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
tooltip: "Export",
onPressed: _handlePrint,
),
IconButton(
icon: const Icon(Icons.home),
padding: EdgeInsets.zero,
iconSize: 20,
visualDensity: VisualDensity.compact,
tooltip:"Home",
onPressed: () {
// stopTimer();
Navigator.pop(context);

@ -17,6 +17,7 @@ import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/models/new_models/work_order_detail_model.dart';
import 'package:test_sa/models/service_request/spare_parts.dart';
import 'package:test_sa/models/service_request/supplier_details.dart';
import 'package:test_sa/models/wo_by_asset_id_model.dart';
import 'cm_request_utils.dart';
@ -1003,4 +1004,19 @@ class CMDetailProvider extends ChangeNotifier {
return -1;
}
}
Future<List<WoByAssetIdModel>> getWorkOrderByAssetId(String searchText, int assetId) async {
late Response response;
try {
response = await ApiManager.instance.post(URLs.workOrderByAssetIdAutoComplete, body: {"pageSize": 1, "pageNumber": 20, "search": searchText, "assetId": assetId});
List<WoByAssetIdModel> woList = [];
if (response.statusCode >= 200 && response.statusCode < 300) {
List categoriesListJson = json.decode(response.body)["data"];
woList = categoriesListJson.map((wo) => WoByAssetIdModel.fromJson(wo)).toList();
}
return woList;
} catch (error) {
return [];
}
}
}

@ -175,7 +175,7 @@ class _ActivitiesListViewState extends State<ActivitiesListView> {
FilesList(images: activity.activitySparePart!.acitiySparePartAttachments!.map((toElement) => URLs.getFileUrl(toElement.name!)!).toList()),
],
],
).toShadowContainer(context, padding: 12, showShadow: false).onPress(() {
).toShadowContainer(context, showShadow: false).onPress(() {
if (showActionButtons) return;
if (requestDetailProvider.isReadOnlyRequest) {
editSparePartRequest(context: context, requestDetailProvider: requestDetailProvider, activity: activity);
@ -272,7 +272,7 @@ class _ActivitiesListViewState extends State<ActivitiesListView> {
// // ),
// ]
],
).toShadowContainer(context, padding: 12, showShadow: false).onPress(() {
).toShadowContainer(context, showShadow: false).onPress(() {
if (showActionButtons) return;
if (requestDetailProvider.isReadOnlyRequest) {
editMaintenanceRequest(context: context, requestDetailProvider: requestDetailProvider, activity: activity);

@ -15,6 +15,8 @@ import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/buttons/app_back_button.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/no_data_found.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
@ -106,7 +108,7 @@ class HistoryLogView extends StatelessWidget {
child: Text(
object.date.toString().toServiceRequestCardFormat,
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120),
style: AppTextStyles.tinyFont.copyWith(color: AppColor.labelTextStyleColor(context)),
),
).expanded,
Expanded(
@ -120,29 +122,16 @@ class HistoryLogView extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 12.toScreenWidth),
child: InteractiveViewer(child: Image.network(URLs.getFileUrl(object.photoVerfication ?? '')!, height: 143.toScreenHeight)),
).onPress(() async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
InteractiveViewer(child: Image.network(URLs.getFileUrl(object.photoVerfication ?? '')!)).center,
const ABackButton(),
],
),
),
),
),
);
context.fullScreenImageViewer(Image.network(URLs.getFileUrl(object.photoVerfication ?? '')!));
}),
],
title.bodyText(context).custom(color: AppColor.textColor(context)),
object.timeDifference.isNotEmpty ? object.timeDifference.tinyFont(context).custom(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120) : const SizedBox(),
InfoHeaderWidget(title),
if (object.timeDifference.isNotEmpty) InfoTextLabelWidget(label: object.timeDifference, textStyle: AppTextStyles.tinyFont),
],
),
),
],
).toShadowContainer(context, padding: 16).paddingOnly(bottom: 16);
).toShadowContainer(context).paddingOnly(bottom: 16);
}
Widget statusCard(BuildContext context, String status) {

@ -55,7 +55,7 @@ class _InitialVisitCardState extends State<InitialVisitCard> {
requestDetailProvider.needVisitHelperModel!.comment!.bodyText2(context).custom(color: AppColor.neutral120),
]
],
).toShadowContainer(context, padding: 16);
).toShadowContainer(context);
});
}
}

@ -18,6 +18,7 @@ import 'package:test_sa/modules/cm_module/views/forms/asset_retired/asset_retire
import 'package:test_sa/modules/cm_module/views/forms/cost/cost_detail_form_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/views/widgets/equipment/asset_detail_page.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
import 'package:test_sa/views/widgets/item_views/info_date_widget.dart';
@ -141,10 +142,10 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
// spacing: 8,
runSpacing: 8,
children: [
if (Provider.of<SettingProvider>(context, listen: false).showPriority) ...[
if (Provider.of<SettingProvider>(context, listen: false).showPriority && workOrder.priority != null) ...[
StatusLabel(
label: workOrder.priority?.name,
id: workOrder.priority!.value!,
id: workOrder.priority?.value,
radius: 4,
textColor: AppColor.getPriorityStatusTextColor(context, workOrder.priority!.value!),
backgroundColor: AppColor.getPriorityStatusColor(context, workOrder.priority!.value!),
@ -192,12 +193,27 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
],
),
8.height,
InfoHeader16Widget(context.translation.assetInformation),
Row(
spacing: 4,
children: [
Flexible(
child: InfoHeader16Widget(context.translation.assetInformation),
),
const Icon(Icons.open_in_new_rounded, color: AppColor.primary10, size: 16).onPress(() {
Navigator.of(context).pushNamed(AssetDetailPage.id, arguments: workOrder.asset!.id);
})
],
),
if (context.userProvider.isEngineer && !(workOrder.asset!.hasTrainingImageData ?? true)) ...[
InfoTextLabelWidget(label: '*Images are required for the asset', color: AppColor.red30, textStyle: AppTextStyles.tinyFont2),
4.height,
],
InfoTextWidget(label: context.translation.assetName, value: workOrder.assetNdModel!.name?.cleanupWhitespace.capitalizeFirstOfEach),
InfoTextWidget(label: context.translation.assetNumber, value: workOrder.asset!.assetNumber),
InfoTextWidget(label: context.translation.assetNumber, value: workOrder.asset!.assetNumber, copyValue: true),
InfoTextWidget(label: context.translation.serialNo, value: workOrder.asset!.assetSerialNo),
InfoTextWidget(label: context.translation.manufacture, value: workOrder.manufacturer?.name?.cleanupWhitespace.capitalizeFirstOfEach),
InfoTextWidget(label: context.translation.model, value: workOrder.model?.name!.cleanupWhitespace.capitalizeFirstOfEach),
InfoTextWidget(label: context.translation.assetType, value: workOrder.assetType?.name!.cleanupWhitespace.capitalizeFirstOfEach),
InfoTextWidget(label: context.translation.site, value: workOrder.site?.siteName?.cleanupWhitespace.capitalizeFirstOfEach),
InfoTextWidget(label: context.translation.building, value: workOrder.building?.name?.cleanupWhitespace.capitalizeFirstOfEach),
InfoTextWidget(label: context.translation.floor, value: workOrder.floor?.name?.cleanupWhitespace.capitalizeFirstOfEach ?? ""),
@ -211,7 +227,7 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
//... request details starts here....
InfoHeader16Widget(context.translation.callDetails),
InfoTextWidget(label: context.translation.cmNumber, value: workOrder.workOrderNo),
InfoTextWidget(label: context.translation.cmNumber, value: workOrder.workOrderNo, copyValue: true),
InfoTextWidget(label: context.translation.createdBy, value: workOrder.workOrderCreatedBy?.userName ?? "-"),
InfoTextWidget(label: context.translation.arrivedDate, value: workOrder.iHaveArrivedDate != null ? workOrder.iHaveArrivedDate.toString().toServiceRequestDetailsFormat : '-'),
InfoTextWidget(label: context.translation.closedDate, value: workOrder.closedDate != null ? workOrder.closedDate!.toString().toServiceRequestDetailsFormat : '-'),
@ -281,9 +297,7 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
assetRetiredButton(context: context),
]
],
).toShadowContainer(
context,
);
).toShadowContainer(context);
}
Widget contactInfoCard(BuildContext context, WorkOrderData workOrder) {
@ -339,7 +353,7 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
InfoTextWidget(label: 'MR No'.addTranslation, value: provider.currentWorkOrder?.data?.mrNo?.toString() ?? '-'),
InfoTextWidget(label: 'Exchange Cost'.addTranslation, value: provider.currentWorkOrder?.data?.exchangeCost?.toString() ?? '-'),
],
).toShadowContainer(context, padding: 16);
).toShadowContainer(context);
}
Widget assetRetiredButton({required BuildContext context}) {

@ -388,7 +388,7 @@ class _ChatPageState extends State<ChatPage> {
clipBehavior: Clip.antiAliasWithSaveLayer,
builder: (BuildContext context) => ChatFilePicker())) as File?;
if (file != null) {
Utils.showUploadingDialog(context);
Utils.showLoadingWithTextDialog(context);
await chatProvider.uploadAttachments(context.userProvider.user!.username!, file, "1");
Utils.hideLoading(context);
}
@ -638,7 +638,7 @@ class _ChatPageState extends State<ChatPage> {
hoverColor: Colors.transparent,
onPressed: () async {
if (chatState == ChatState.voiceRecordingCompleted) {
Utils.showUploadingDialog(context);
Utils.showLoadingWithTextDialog(context);
try {
await chatProvider.uploadAttachments(context.userProvider.user!.username!, File(recordedFilePath!), "1");
Utils.hideLoading(context);

@ -24,7 +24,6 @@ class ChatFileViewer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder<File?>(
future: checkFileInLocalStorage(),
builder: (BuildContext context, AsyncSnapshot<File?> snapshot) {
@ -80,16 +79,7 @@ class ChatFileViewer extends StatelessWidget {
padding: EdgeInsets.zero,
onPressed: () async {
if (isImage) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: const DefaultAppBar(),
body: SafeArea(
child: InteractiveViewer(child: Image.file(file)).center,
),
),
),
);
context.fullScreenImageViewer(Image.file(file));
} else {
OpenFile.open(file.path);
}

@ -182,16 +182,7 @@ class AttachmentFileViewer extends StatelessWidget {
),
).onPress(() {
if (isImage) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: const DefaultAppBar(),
body: SafeArea(
child: InteractiveViewer(child: Image.file(snapshot.data!)).center,
),
),
),
);
context.fullScreenImageViewer(Image.file(snapshot.data!));
} else {
OpenFile.open(snapshot.data!.path);
}
@ -253,16 +244,7 @@ class AttachmentFileViewer extends StatelessWidget {
padding: EdgeInsets.zero,
onPressed: () async {
if (isImage) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: const DefaultAppBar(),
body: SafeArea(
child: InteractiveViewer(child: Image.file(file)).center,
),
),
),
);
context.fullScreenImageViewer(Image.file(file));
} else {
OpenFile.open(file.path);
}

@ -43,6 +43,8 @@ import 'package:test_sa/providers/gas_request_providers/site_provider.dart';
import 'package:test_sa/providers/loading_list_notifier.dart';
import 'package:test_sa/providers/work_order/vendor_provider.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker.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 '../../../../../../new_views/common_widgets/default_app_bar.dart';
import '../../models/new_models/department.dart';
import 'demo_detail_view_page.dart';
@ -92,76 +94,23 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// SingleItemDropDownMenu<Lookup, DemoPeriodLookupProvider>(
// context: context,
// title: "Demo Period",
// initialValue: demoPeriodLookup,
// showShadow: false,
// validator: (value) {
// if (value == null) return "Mandatory";
// return null;
// },
// backgroundColor: AppColor.fieldBgColor(context),
// showAsBottomSheet: true,
// onSelect: (value) {
// demoPeriodLookup = value;
// setState(() {});
// },
// ),
// 16.height,
...siteSection(),
16.height,
...doctorInfoSection(),
16.height,
...assetSection(),
16.height,
...vendorDetailsSection(),
16.height,
"Attachments".bodyText(context).custom(color: AppColor.black10),
8.height,
SingleItemDropDownMenu<Lookup, DemoDocumentLookupProvider>(
context: context,
height: 56.toScreenHeight,
title: "Document Type",
initialValue: demoDocumentLookup,
showShadow: false,
validator: (value) {
if (value == null) return "Mandatory";
return null;
},
backgroundColor: AppColor.fieldBgColor(context),
showAsBottomSheet: true,
onSelect: (value) {
demoDocumentLookup = value;
setState(() {});
},
),
16.height,
AttachmentPicker(
label: context.translation.attachments,
attachment: _attachments,
buttonColor: AppColor.black10.withOpacity(demoDocumentLookup != null ? 1 : .4),
onlyImages: false,
enabled: demoDocumentLookup != null,
documentType: demoDocumentLookup,
showAsListView: true,
buttonIcon: 'image-plus'.toSvgAsset(color: (AppColor.primary10).withOpacity(demoDocumentLookup != null ? 1 : .4)),
onChange: (attachments) {
_attachments = attachments;
setState(() {});
},
),
16.height,
...attachmentSection(),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Checkbox(
value: _demoFormModel.demoEaluationAcknowledge,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
activeColor: AppColor.blueStatus(context),
activeColor: AppColor.primary10,
side: BorderSide(color: AppColor.loadingColor(context), width: 1.0),
visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: (value) {
setState(() {
@ -172,24 +121,29 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
"DEMO outcome evaluation form acknowledgement".bodyText(context).custom(color: context.isDark ? AppColor.primary50 : AppColor.neutral50),
4.height,
"I acknowledge at the end of the trial period I will complete and submit the Medical Technology Trial Outcome Form within two weeks."
.tinyFont(context)
.custom(color: context.isDark ? AppColor.primary50 : AppColor.neutral120),
InfoTextLabelWidget(
label: "DEMO outcome evaluation form acknowledgement",
textStyle: AppTextStyles.bodyText,
color: AppColor.textColor(context),
),
InfoTextLabelWidget(
label: "I acknowledge at the end of the trial period I will complete and submit the Medical Technology Trial Outcome Form within two weeks.",
color: AppColor.lightTextColor(context),
),
],
).expanded,
],
),
16.height,
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Checkbox(
value: _demoFormModel.acknowledgeNotEquipment,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
activeColor: AppColor.blueStatus(context),
activeColor: AppColor.primary10,
side: BorderSide(color: AppColor.loadingColor(context), width: 1.0),
visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: (value) {
setState(() {
@ -200,18 +154,23 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
"Acknowledgement of not using the equipment".bodyText(context).custom(color: context.isDark ? AppColor.primary50 : AppColor.neutral50),
4.height,
"I acknowledge at the end of the trial period I will not use the DEMO medical equipment & I will not book any patient on the DEMO medical equipment."
.tinyFont(context)
.custom(color: context.isDark ? AppColor.primary50 : AppColor.neutral120),
InfoTextLabelWidget(
label: "Acknowledgement of not using the equipment",
textStyle: AppTextStyles.bodyText,
color: AppColor.textColor(context),
),
InfoTextLabelWidget(
label: "I acknowledge at the end of the trial period I will not use the DEMO medical equipment & I will not book any patient on the DEMO medical equipment.",
color: AppColor.lightTextColor(context),
),
],
).expanded,
],
),
],
).toShadowContainer(context, borderRadius: 20),
).toShadowContainer(context),
).expanded,
FooterActionButton.footerContainer(
context: context,
@ -231,8 +190,7 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
List<Widget> siteSection() {
return [
'Site Details'.addTranslation.bodyText(context).custom(color: AppColor.black10),
8.height,
InfoHeader16Widget('Site Details'.addTranslation),
SingleItemDropDownMenu<Site, SiteProvider>(
context: context,
title: context.translation.site,
@ -252,7 +210,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
setState(() {});
},
),
8.height,
SingleItemDropDownMenu<Building, NullableLoadingProvider>(
context: context,
title: context.translation.building,
@ -273,7 +230,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
setState(() {});
},
),
8.height,
SingleItemDropDownMenu<Floor, NullableLoadingProvider>(
context: context,
title: context.translation.floor,
@ -293,7 +249,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
setState(() {});
},
),
8.height,
SingleItemDropDownMenu<Department, NullableLoadingProvider>(
context: context,
title: context.translation.department,
@ -318,8 +273,7 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
List<Widget> doctorInfoSection() {
return [
"Doctor Details".bodyText(context).custom(color: AppColor.black10),
8.height,
InfoHeader16Widget('Doctor Details'.addTranslation),
AppTextFormField(
initialValue: "",
labelText: "Name",
@ -336,7 +290,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.docName = value;
},
),
8.height,
AppTextFormField(
initialValue: "",
labelText: "Contact Number",
@ -353,7 +306,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.docNumber = value;
},
),
8.height,
AppTextFormField(
initialValue: "",
labelText: "Email",
@ -373,7 +325,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.docEmail = value;
},
),
8.height,
AppTextFormField(
labelText: "Item Description",
validator: (value) {
@ -391,7 +342,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.itemDescription = value;
},
),
8.height,
AppTextFormField(
labelText: "Request Description",
// validator: (value) {
@ -414,8 +364,7 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
List<Widget> assetSection() {
return [
'Asset Details'.addTranslation.bodyText(context).custom(color: AppColor.black10),
8.height,
InfoHeader16Widget('Asset Details'.addTranslation),
AppTextFormField(
labelText: "Asset Name",
validator: (value) {
@ -433,7 +382,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.assetName = value;
},
),
8.height,
AppTextFormField(
labelText: "Model",
validator: (value) {
@ -451,7 +399,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.model = value;
},
),
8.height,
AppTextFormField(
labelText: "Manufacturer",
validator: (value) {
@ -469,7 +416,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.manufacturer = value;
},
),
8.height,
SingleItemDropDownMenu<Lookup, DemoPeriodLookupProvider>(
context: context,
height: 56.toScreenHeight,
@ -492,8 +438,7 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
List<Widget> vendorDetailsSection() {
return [
'Vendor Details'.addTranslation.bodyText(context).custom(color: AppColor.black10),
8.height,
InfoHeader16Widget('Vendor Details'.addTranslation),
AppTextFormField(
labelText: 'Vendor Name'.addTranslation,
style: Theme.of(context).textTheme.titleMedium,
@ -506,7 +451,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.vendorName = value;
},
),
8.height,
AppTextFormField(
labelText: 'Representative Name'.addTranslation,
style: Theme.of(context).textTheme.titleMedium,
@ -519,7 +463,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.vendorRepresentativeName = value;
},
),
8.height,
AppTextFormField(
// controller: vendorPhoneController,
labelText: "Contact Number",
@ -533,7 +476,6 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
_demoFormModel.vendorContactNumber = value;
},
),
8.height,
AppTextFormField(
labelText: "Email",
// controller: vendorEmailController,
@ -550,6 +492,43 @@ class _CreateDemoRequestPageState extends State<CreateDemoRequestPage> with Tick
];
}
List<Widget> attachmentSection() {
return [
InfoHeader16Widget('Attachments'.addTranslation),
SingleItemDropDownMenu<Lookup, DemoDocumentLookupProvider>(
context: context,
height: 56.toScreenHeight,
title: "Document Type",
initialValue: demoDocumentLookup,
showShadow: false,
validator: (value) {
if (value == null) return "Mandatory";
return null;
},
backgroundColor: AppColor.fieldBgColor(context),
showAsBottomSheet: true,
onSelect: (value) {
demoDocumentLookup = value;
setState(() {});
},
),
AttachmentPicker(
label: context.translation.attachments,
attachment: _attachments,
buttonColor: AppColor.primary10.withOpacity(demoDocumentLookup != null ? 1 : .4),
onlyImages: false,
enabled: demoDocumentLookup != null,
documentType: demoDocumentLookup,
showAsListView: true,
buttonIcon: 'image-plus'.toSvgAsset(color: (AppColor.primary10).withOpacity(demoDocumentLookup != null ? 1 : .4)),
onChange: (attachments) {
_attachments = attachments;
setState(() {});
},
),
];
}
Future<void> _submit() async {
FocusScope.of(context).unfocus();
if (_formKey.currentState!.validate()) {

@ -16,6 +16,8 @@ import 'package:test_sa/modules/loan_module/models/loan_attachment_model.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/swipe_module/dialoge/acknowledge_work_dialog.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/no_data_found.dart';
import '../../controllers/api_routes/urls.dart';
@ -111,7 +113,6 @@ class _DemoDetailViewPageState extends State<DemoDetailViewPage> {
),
8.height,
],
12.height,
...requesterDetails(context, demoData),
const Divider().defaultStyle(context),
...requestDetails(context, demoData),
@ -123,10 +124,7 @@ class _DemoDetailViewPageState extends State<DemoDetailViewPage> {
...vendorDetails(context, demoData),
if (allAttachments.isNotEmpty) ...[
const Divider().defaultStyle(context),
Text(
"Attachments".addTranslation,
style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
),
InfoHeader16Widget("Attachments"),
FilesList(
showAsListView: true,
images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList(),
@ -134,10 +132,7 @@ class _DemoDetailViewPageState extends State<DemoDetailViewPage> {
],
if (suppAttachments.isNotEmpty) ...[
const Divider().defaultStyle(context),
Text(
"Supplier Attachments".addTranslation,
style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
),
InfoHeader16Widget("Supplier Attachments"),
FilesList(
showAsListView: true,
images: suppAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList(),
@ -191,53 +186,53 @@ class _DemoDetailViewPageState extends State<DemoDetailViewPage> {
List<Widget> requestDetails(BuildContext context, DemoRequestModel? model) {
return [
Text("Request Details", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'Request No: ${model?.requestNo ?? "-"}'.bodyText(context),
'Demo No: ${model?.demoNo ?? "-"}'.bodyText(context),
'Demo Period: ${model?.demoPeriod?.name ?? "-"}'.bodyText(context),
'${context.translation.site}: ${model?.site?.custName ?? "-"}'.bodyText(context),
'${context.translation.building}: ${model?.building?.name ?? "-"}'.bodyText(context),
'${context.translation.floor}: ${model?.floor?.name ?? "-"}'.bodyText(context),
'${context.translation.department}: ${model?.department?.departmentName ?? "-"}'.bodyText(context),
'Item Description: ${model?.itemDescription ?? "-"}'.bodyText(context),
'Request Description: ${model?.requestDescription ?? "-"}'.bodyText(context),
InfoHeader16Widget("Request Information"),
InfoTextWidget(label: "Request No", value: model?.requestNo ?? "-", copyValue: true),
InfoTextWidget(label: "Demo No", value: model?.demoNo ?? "-"),
InfoTextWidget(label: "Demo Period", value: model?.demoPeriod?.name ?? "-"),
InfoTextWidget(label: context.translation.site, value: model?.site?.custName ?? "-"),
InfoTextWidget(label: context.translation.building, value: model?.building?.name ?? "-"),
InfoTextWidget(label: context.translation.floor, value: model?.floor?.name ?? "-"),
InfoTextWidget(label: context.translation.department, value: model?.department?.departmentName ?? "-"),
InfoTextWidget(label: "Item Description", value: model?.itemDescription ?? "-"),
InfoTextWidget(label: "Request Description", value: model?.requestDescription ?? "-"),
];
}
List<Widget> vendorDetails(BuildContext context, DemoRequestModel? model) {
return [
Text("Vendor Information", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'Name: ${model?.supplier?.name ?? model?.vendorName ?? "-"}'.bodyText(context),
'Representative Name: ${model?.suppPerson?.name ?? model?.vendorRepresentativeName ?? "-"}'.bodyText(context),
'Contact: ${model?.supplier?.contact ?? model?.vendorContactNumber ?? "-"}'.bodyText(context),
'Email: ${model?.supplier?.email ?? model?.vendorEmail ?? "-"}'.bodyText(context),
InfoHeader16Widget("Vendor Information"),
InfoTextWidget(label: "Name", value: model?.supplier?.name ?? model?.vendorName ?? "-"),
InfoTextWidget(label: "Representative Name", value: model?.suppPerson?.name ?? model?.vendorRepresentativeName ?? "-"),
InfoTextWidget(label: "Contact", value: model?.supplier?.contact ?? model?.vendorContactNumber ?? "-"),
InfoTextWidget(label: "Email", value: model?.supplier?.email ?? model?.vendorEmail ?? "-"),
];
}
List<Widget> assetDetails(BuildContext context, DemoRequestModel? model) {
return [
Text("Asset Information", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'${context.translation.assetName}: ${model?.assetName ?? "-"}'.bodyText(context),
'${context.translation.manufacture}: ${model?.manufacturer ?? "-"}'.bodyText(context),
'${context.translation.model}: ${model?.model ?? "-"}'.bodyText(context),
InfoHeader16Widget("Asset Information"),
InfoTextWidget(label: context.translation.assetName, value: model?.assetName ?? "-"),
InfoTextWidget(label: context.translation.manufacture, value: model?.manufacturer ?? "-"),
InfoTextWidget(label: context.translation.model, value: model?.model ?? "-"),
];
}
List<Widget> doctorDetails(BuildContext context, DemoRequestModel? model) {
return [
Text("Doctor Information", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'Name: ${model?.doctorName ?? "-"}'.bodyText(context),
'Contact: ${model?.doctorContactNumber ?? "-"}'.bodyText(context),
'Email: ${model?.doctorContactEmail ?? "-"}'.bodyText(context),
InfoHeader16Widget("Doctor Information"),
InfoTextWidget(label: "Name", value: model?.doctorName ?? "-"),
InfoTextWidget(label: "Contact", value: model?.doctorContactNumber ?? "-"),
InfoTextWidget(label: "Email", value: model?.doctorContactEmail ?? "-"),
];
}
List<Widget> requesterDetails(BuildContext context, DemoRequestModel? model) {
return [
Text("Requester Information", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'Name: ${model?.requesterUser?.requesterName ?? "-"}'.bodyText(context),
'Email: ${model?.requesterUser?.requesterEmail ?? "-"}'.bodyText(context),
'Contact: ${model?.requesterUser?.requesterMobile ?? "-"}'.bodyText(context),
InfoHeader16Widget("Requester Information"),
InfoTextWidget(label: "Name", value: model?.requesterUser?.requesterName ?? "-"),
InfoTextWidget(label: "Email", value: model?.requesterUser?.requesterEmail ?? "-"),
InfoTextWidget(label: "Contact", value: model?.requesterUser?.requesterMobile ?? "-"),
// 'Extension: ${loanData.requesterExtensionNo ?? "-"}'.bodyText(context),
];
}

@ -50,6 +50,7 @@ class ExportApiProvider extends ChangeNotifier {
valueColor: textColor,
elevation: 4,
msgFontSize: 14,
borderRadius: 14,
backgroundColor: backgroundColor,
cancel: Cancel(
cancelClicked: () {

@ -200,21 +200,7 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
context.fullScreenImageViewer(imageWidget);
}),
_delete(),
],
@ -241,21 +227,7 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
context.fullScreenImageViewer(imageWidget);
}),
_delete(),
],
@ -300,21 +272,7 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
context.fullScreenImageViewer(imageWidget);
}),
_delete(),
],

@ -7,6 +7,7 @@ import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
/// Reusable success dialog for file operations (export, print, etc.)
class FileSuccessDialog {
@ -31,47 +32,25 @@ class FileSuccessDialog {
return Dialog(
backgroundColor: AppColor.background(context),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: AppColor.borderColor(context)),
borderRadius: BorderRadius.circular(14),
),
child: Padding(
padding: const EdgeInsets.all(24.0),
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Close button
Align(
alignment: Alignment.topRight,
child: 'clear'.toSvgAsset(
color: AppColor.icon2Color(context),
width: 18,
height: 18,
).onPress(() {
child: 'clear'.toSvgAsset(color: AppColor.icon2Color(context), width: 18, height: 18).onPress(() {
Navigator.of(dialogContext).pop();
}),
),
// Success icon
'done'.toSvgAsset(color: AppColor.green70, width: 56, height: 56),
const SizedBox(height: 24),
// Title
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColor.headingTextColor(context),
),
textAlign: TextAlign.center,
),
title.heading5(context).custom(fontWeight: FontWeight.w600, color: AppColor.textStyleColor(context)),
const SizedBox(height: 12),
// Message
Text(
message,
style: TextStyle(
fontSize: 18,
color: AppColor.lightTextColor(context),
),
textAlign: TextAlign.center,
),
message.heading6(context).custom(align: TextAlign.center, fontWeight: FontWeight.w500, color: AppColor.lightTextColor(context)),
const SizedBox(height: 32),
// Action buttons
Row(
@ -84,14 +63,13 @@ class FileSuccessDialog {
final result = await OpenFile.open(filePath);
if (result.type != ResultType.done) {
log('⚠️ Could not open file: ${result.message}');
'No app found to open this file type. File saved to: $filePath'
.showToast;
'No app found to open this file type. File saved to: $filePath'.showToast;
}
},
label: 'Open',
// buttonColor: AppColor.primary10,
showIcon: true,
icon: Icon(Icons.folder_open, color: AppColor.background(context)),
icon: Icon(Icons.folder_open, color: AppColor.background(context)),
height: 48,
),
),
@ -131,4 +109,3 @@ class FileSuccessDialog {
);
}
}

@ -16,12 +16,14 @@ import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/new_models/building.dart';
import 'package:test_sa/models/new_models/floor.dart';
import 'package:test_sa/models/new_models/site.dart';
import 'package:test_sa/models/wo_by_asset_id_model.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/incident_module/incident_lookup_provider.dart';
import 'package:test_sa/modules/incident_module/incident_provider.dart';
import 'package:test_sa/modules/loan_module/models/medical_department_model.dart';
import 'package:test_sa/modules/loan_module/provider/medical_department_provider.dart';
import 'package:test_sa/modules/traf_module/asset_auto_complete_field.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
@ -31,6 +33,7 @@ import 'package:test_sa/providers/gas_request_providers/site_provider.dart';
import 'package:test_sa/views/widgets/date_and_time/time_picker.dart';
import 'package:test_sa/providers/loading_list_notifier.dart';
import 'package:test_sa/providers/lookups/yes_no_lookup_provider.dart';
import 'package:test_sa/views/widgets/auto_complete/wo_auto_complete_field.dart';
import 'package:test_sa/views/widgets/date_and_time/date_picker.dart';
import 'package:test_sa/views/widgets/equipment/asset_picker.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
@ -64,6 +67,7 @@ class _CreateIncidentRequestPageState extends State<CreateIncidentRequestPage> {
Department? _selectedDepartment;
Asset? device;
WoByAssetIdModel? woByAssetId;
Lookup? incidentType;
Lookup? ovrSystem;
@ -128,10 +132,10 @@ class _CreateIncidentRequestPageState extends State<CreateIncidentRequestPage> {
showBorder: true,
onPick: (asset) async {
device = asset;
payload["assetId"] = asset.modelDefinition?.id;
payload["oracleCode"] = asset.modelDefinition?.oracleCodes?.first.codeValue;
payload["assetId"] = asset?.id;
payload["oracleCode"] = (asset.modelDefinition?.oracleCodes?.isNotEmpty ?? false) ? asset.modelDefinition?.oracleCodes?.first.codeValue : null;
payload["model"] = asset.modelDefinition?.modelName;
payload["manufacturer"] = asset.modelDefinition?.manufacturerId;
payload["manufacturer"] = asset.modelDefinition?.manufacturerName;
payload["assetOrigin"] = asset.modelDefinition?.assetName;
payload["siteId"] = asset.site?.id;
payload["buildingId"] = asset.building?.id;
@ -139,7 +143,19 @@ class _CreateIncidentRequestPageState extends State<CreateIncidentRequestPage> {
payload["departmentId"] = asset.department?.id;
setState(() {});
},
)
),
12.height,
WoAutoCompleteField(
clearAfterPick: false,
byName: true,
enabled: device != null,
assetId: device?.id?.toInt(),
initialValue: "",
onPick: (asset) {
woByAssetId = asset;
setState(() {});
},
),
],
if (incidentType?.value != null && incidentType?.value != 1) ...[
16.height,
@ -358,6 +374,7 @@ class _CreateIncidentRequestPageState extends State<CreateIncidentRequestPage> {
onSelect: (value) {
incidentType = value;
payload["incidentClassificationId"] = value?.id;
payload["workOrderId"] = null;
setState(() {});
},
),
@ -661,17 +678,19 @@ class _CreateIncidentRequestPageState extends State<CreateIncidentRequestPage> {
"Please scan or pick asset".showToast;
return;
}
if (incidentType?.value == 1 && device != null && woByAssetId?.id == null) {
"Please choose CM Work Order".showToast;
return;
}
if (ovrSystem?.value == 1 && isOvrVerified == false) {
"Please verify OVR Ticket Number".showToast;
return;
}
if (occurrenceDate == null) {
"Please select occurrence Data".showToast;
return;
}
payload["workOrderId"] = woByAssetId?.id;
payload["occurrenceDate"] = occurrenceDate!.toIso8601String();
_formKey.currentState!.save();

@ -74,6 +74,8 @@ class IncidentDataModel {
String? occurrenceDate;
String? createdDate;
List<IncidentAttachments>? incidentAttachments;
int? workOrderId;
String? workOrderNo;
IncidentDataModel(
{this.id,
@ -148,6 +150,8 @@ class IncidentDataModel {
this.approvalSignature,
this.occurrenceDate,
this.createdDate,
this.workOrderId,
this.workOrderNo,
this.incidentAttachments});
IncidentDataModel.fromJson(Map<String, dynamic> json) {
@ -223,6 +227,8 @@ class IncidentDataModel {
approvalSignature = json['approvalSignature'];
occurrenceDate = json['occurrenceDate'];
createdDate = json['createdDate'];
workOrderId = json['workOrderId'];
workOrderNo = json['workOrderNo'];
if (json['incidentAttachments'] != null) {
incidentAttachments = <IncidentAttachments>[];
json['incidentAttachments'].forEach((v) {
@ -305,6 +311,8 @@ class IncidentDataModel {
data['approvalSignature'] = this.approvalSignature;
data['occurrenceDate'] = this.occurrenceDate;
data['createdDate'] = this.createdDate;
data['workOrderId'] = this.workOrderId;
data['workOrderNo'] = this.workOrderNo;
if (this.incidentAttachments != null) {
data['incidentAttachments'] = this.incidentAttachments!.map((v) => v.toJson()).toList();
}

@ -1,3 +1,6 @@
import 'dart:developer';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
@ -6,6 +9,8 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/cm_module/cm_detail_page.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/incident_module/incident_attachment_model.dart';
import 'package:test_sa/modules/incident_module/incident_data_model.dart';
import 'package:test_sa/modules/incident_module/incident_provider.dart';
@ -36,41 +41,39 @@ class IncidentDetailPage extends StatelessWidget {
List<IncidentAttachments> allAttachments = snapshot.data!.incidentAttachments!;
return ListView(
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusLabel(
label: snapshot.data!.incidentStatusName!,
textColor: AppColor.getRequestStatusTextColorByName(context, snapshot.data!.incidentStatusName!),
backgroundColor: AppColor.getRequestStatusColorByName(context, snapshot.data!.incidentStatusName!),
),
1.width.expanded,
InfoDateWidget(snapshot.data!.createdDate?.toString() ?? ""),
],
),
// 12.height,
...requesterDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...requestDetails(context, snapshot.data!),
if (snapshot.data!.ovrTicketNumber?.isNotEmpty ?? false) ovrInformation(context, snapshot.data!.ovrTicketNumber!),
const Divider().defaultStyle(context),
...assetDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...personInvolvedDetails(context, snapshot.data!),
if (allAttachments.isNotEmpty) ...[
const Divider().defaultStyle(context),
InfoHeader16Widget("Attachments".addTranslation),
FilesList(images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusLabel(
label: snapshot.data!.incidentStatusName!,
textColor: AppColor.getRequestStatusTextColorByName(context, snapshot.data!.incidentStatusName!),
backgroundColor: AppColor.getRequestStatusColorByName(context, snapshot.data!.incidentStatusName!),
),
1.width.expanded,
InfoDateWidget(snapshot.data!.createdDate?.toString() ?? ""),
],
),
// 12.height,
...requesterDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...requestDetails(context, snapshot.data!),
if (snapshot.data!.ovrTicketNumber?.isNotEmpty ?? false) ovrInformation(context, snapshot.data!.ovrTicketNumber!),
const Divider().defaultStyle(context),
...assetDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...personInvolvedDetails(context, snapshot.data!),
if (allAttachments.isNotEmpty) ...[
const Divider().defaultStyle(context),
InfoHeader16Widget("Attachments".addTranslation),
FilesList(images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []),
],
).toShadowContainer(context, padding: 16),
],
],
).toShadowContainer(context),
);
}));
}
@ -78,7 +81,7 @@ class IncidentDetailPage extends StatelessWidget {
List<Widget> requestDetails(BuildContext context, IncidentDataModel incidentData) {
return [
InfoHeader16Widget("Request Details"),
InfoTextWidget(label: 'Incident No', value: incidentData.reqCode ?? "-"),
InfoTextWidget(label: 'Incident No', value: incidentData.reqCode ?? "-", copyValue: true),
InfoTextWidget(label: 'Incident Type', value: incidentData.incidentClassificationName ?? "-"),
InfoTextWidget(label: 'Incident Title', value: incidentData.incidentTitle ?? "-"),
InfoTextWidget(label: 'Incident Description', value: incidentData.incidentDescription ?? "-"),
@ -88,6 +91,18 @@ class IncidentDetailPage extends StatelessWidget {
InfoTextWidget(label: 'Floor', value: incidentData.floorName ?? "-"),
InfoTextWidget(label: 'Department', value: incidentData.departmentName ?? "-"),
InfoTextWidget(label: 'Occurrence Date', value: incidentData.occurrenceDate?.toServiceRequestDetailsFormat ?? "-"),
if (incidentData.workOrderId != null)
RichText(
text: TextSpan(text: "Work Order: ", style: AppTextStyles.bodyText2.copyWith(color: AppColor.lightTextColor(context)), children: [
TextSpan(
text: "${incidentData.workOrderNo}",
style: AppTextStyles.bodyText.copyWith(color: AppColor.primary10, decoration: TextDecoration.underline),
recognizer: TapGestureRecognizer()
..onTap = () async {
await Navigator.of(context).push(MaterialPageRoute(builder: (_) => CMDetailPage(requestId: incidentData.workOrderId!, moduleId: 3)));
}),
]),
),
InfoTextWidget(label: 'Root Cause', value: incidentData.rootCauseName ?? "-"),
if (incidentData.rootCauseValue == 5) InfoTextWidget(label: 'Root reason', value: incidentData.otherRootCause ?? "-"),
InfoTextWidget(label: 'Comments', value: incidentData.comments ?? "-"),

@ -185,7 +185,7 @@ class _EquipmentInternalAuditDetailPageState extends State<EquipmentInternalAudi
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget("Request Details"),
InfoTextWidget(label: context.translation.requestNo, value: model?.requestNo ?? '-'),
InfoTextWidget(label: context.translation.requestNo, value: model?.requestNo ?? '-', copyValue: true),
if (model != null && model!.equipmentsFindings != null && model!.equipmentsFindings!.isNotEmpty)
...model!.equipmentsFindings!.map((item) {
final findingName = item.finding?.name ?? 'Unknown';
@ -204,7 +204,7 @@ class _EquipmentInternalAuditDetailPageState extends State<EquipmentInternalAudi
children: [
InfoHeader16Widget("Asset Information"),
InfoTextWidget(label: context.translation.assetName, value: model?.asset?.assetName ?? '-'),
InfoTextWidget(label: context.translation.assetNo, value: model?.asset?.assetNumber ?? '-'),
InfoTextWidget(label: context.translation.assetNo, value: model?.asset?.assetNumber ?? '-', copyValue: true),
InfoTextWidget(label: context.translation.serialNumber, value: model?.asset?.assetNumber ?? '-'),
InfoTextWidget(label: context.translation.manufacture, value: model?.asset?.manufacturerName ?? '-'),
InfoTextWidget(label: context.translation.model, value: model?.asset?.modelName ?? '-'),

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/validator/validator.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/enum_extensions.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
@ -324,6 +325,7 @@ class _CreateLoanRequestPageState extends State<CreateLoanRequestPage> with Tick
SingleItemDropDownMenu<TrafDepartment, DepartmentLookupProvider>(
context: context,
title: context.translation.department,
matchBy: DropDownMatchType.identifier,
showShadow: false,
enabled: !isBackUpRequest,
validator: (value) {

@ -98,7 +98,7 @@ class _LoanEquipmentDetailPageState extends State<LoanEquipmentDetailPage> {
FilesList(images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []),
],
],
).toShadowContainer(context, padding: 12)
).toShadowContainer(context)
],
).expanded,
// @todo ask backend to add loanTypeValue, so we can show or hide the buttons
@ -184,7 +184,7 @@ class _LoanEquipmentDetailPageState extends State<LoanEquipmentDetailPage> {
return [
InfoHeader16Widget("Request Details"),
InfoTextWidget(label: 'Loan Type', value: loanData.loanTypeName ?? "-"),
InfoTextWidget(label: 'Loan No', value: loanData.reqCode ?? "-"),
InfoTextWidget(label: 'Loan No', value: loanData.reqCode ?? "-", copyValue: true),
InfoTextWidget(label: 'Loan Period', value: loanData.loanPeriodName ?? "-"),
InfoTextWidget(label: context.translation.site, value: loanData.siteName ?? "-"),
InfoTextWidget(label: context.translation.department, value: loanData.departmentName ?? "-"),

@ -236,6 +236,7 @@ class _CreateMedicalGasRequestPageState extends State<CreateMedicalGasRequestPag
itemCount: medicalGasInfo!.attributeInfo!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
AttributeInfo model = medicalGasInfo!.attributeInfo![index];
return AppTextFormField(

@ -90,7 +90,7 @@ class _MedicalGasInspectionDetailPageState extends State<MedicalGasInspectionDet
),
8.height,
InfoHeader16Widget("Request Details"),
InfoTextWidget(label: context.translation.requestNo, value: model?.requestNumber ?? '-'),
InfoTextWidget(label: context.translation.requestNo, value: model?.requestNumber ?? '-', copyValue: true),
InfoTextWidget(label: 'Requested by', value: model?.requestedBy ?? '-'),
if (model?.lastModifiedBy != null)
InfoTextWidget(label: 'Last Modified by', value: model?.lastModifiedBy ?? '-'),
@ -106,7 +106,7 @@ class _MedicalGasInspectionDetailPageState extends State<MedicalGasInspectionDet
FilesList(images: model!.deliveryNoteAttachmentDto.map((e) => URLs.getFileUrl(e.name ?? '') ?? '').toList() ?? []),
],
],
).toShadowContainer(context, padding: 16),
).toShadowContainer(context,),
],
).expanded,
if (model?.requestStatus?.value != null && model!.requestStatus!.value! < 3)
@ -123,10 +123,7 @@ class _MedicalGasInspectionDetailPageState extends State<MedicalGasInspectionDet
}
return;
}
bool? isRefresh = await Navigator.of(context).push(MaterialPageRoute(
builder: (_) => CreateMedicalGasRequestPage(
dataModel: model,
)));
bool? isRefresh = await Navigator.of(context).push(MaterialPageRoute(builder: (_) => CreateMedicalGasRequestPage(dataModel: model)));
if (isRefresh == true) {
_loadData();
}

@ -49,7 +49,7 @@ class AttributeInfo {
AttributeInfo.fromJson(Map<String, dynamic> json) {
id = json['id'];
medicalGasAttributeId = json['medicalGasAttributeId'];
requestedQuantity = json['requestedQuantity'];
requestedQuantity = json['value'] ?? json['requestedQuantity'];
value = json['value'];
itemName = json['itemName'];
oracleName = json['oracleName'];

@ -1,3 +1,7 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/providers/api/all_requests_provider.dart';
@ -6,6 +10,8 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/medical_gas_inspection/models/delivery_notes_form_model.dart';
import 'package:test_sa/modules/medical_gas_inspection/models/medical_gas_data_model.dart';
@ -38,9 +44,7 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
bool pressureTestAcknowledgement = false;
bool leakTestAcknowledgement = false;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
// List<MedicalGasRequestDetailModel> selectedItemList = [];
List<GenericAttachmentModel> _attachments = [];
@override
void initState() {
@ -63,9 +67,13 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
isInspectedGasLeak: dataModel.isInspectedGasLeak,
isPressureChecked: dataModel.isPressureChecked,
deliveredDate: dataModel.deliveredDate,
attachments: dataModel.deliveryNoteAttachmentDto,
// attachments: dataModel.deliveryNoteAttachmentDto,
isSubmitted: dataModel.isSubmitted,
deliveryNoteNumber: dataModel.deliveryNote);
if (dataModel.deliveryNoteAttachmentDto.isNotEmpty) {
_attachments.addAll(dataModel.deliveryNoteAttachmentDto.map((e) => GenericAttachmentModel(id: e.id, name: e.name)).toList());
}
//Need to Confirm this condition
// selectedItemList = dataModel.requestDetailDtos.where((item) => item.deliveredQuantity != null && item.deliveredQuantity! > 0).toList();
setState(() {});
@ -202,7 +210,7 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
AttachmentPicker(
label: 'Upload Attachment',
attachment: formModel.attachments,
attachment: _attachments,
buttonColor: AppColor.primary10,
// showAsListView: true,
onlyImages: false,
@ -319,6 +327,15 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
deliveredQuantity: item.deliveredQuantity,
));
}
formModel.attachments = [];
try {
for (var item in _attachments) {
String fileName = CMRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? '';
formModel.attachments.add(GenericAttachmentModel(id: item.id, name: fileName));
}
} catch (error) {
print(error);
}
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
await medicalGasInspectionProvider.updatedDeliveryNotes(model: formModel).then((status) async {
Navigator.pop(context);
@ -370,9 +387,9 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
return "Enter a valid number";
}
if (intVal <= 0) {
return "Value must be greater than 0";
}
// if (intVal <= 0) {
// return "Value must be greater than 0";
// }
return null;
},
@ -383,7 +400,8 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
vertical: 6, // keeps normal height ~30
),
filled: true,
fillColor: AppColor.fieldBgColor(context),
fillColor: AppColor.dropDownBackground(context),
// fillColor: AppColor.fieldBgColor(context),
border: border,
enabledBorder: border,
focusedBorder: border,

@ -70,6 +70,7 @@ class _PpmDetailsPageState extends State<PpmDetailsPage> {
: planPreventiveVisit != null
? Column(children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -86,7 +87,7 @@ class _PpmDetailsPageState extends State<PpmDetailsPage> {
InfoHeader16Widget(planPreventiveVisit.assetName?.cleanupWhitespace.capitalizeFirstOfEach ?? ''),
InfoTextWidget(label: context.translation.assetNumber, value: planPreventiveVisit.asset?.assetNumber ?? '-'),
InfoTextWidget(label: context.translation.assetSN, value: planPreventiveVisit.asset?.assetSerialNo ?? '-'),
InfoTextWidget(label: context.translation.requestNo, value: planPreventiveVisit.visitNo ?? '-'),
InfoTextWidget(label: context.translation.requestNo, value: planPreventiveVisit.visitNo ?? '-', copyValue: true),
if (planPreventiveVisit.acutalDateOfVisit != null) ...[
const Divider().defaultStyle(context),
InfoTextWidget(label: context.translation.actualDate, value: planPreventiveVisit.acutalDateOfVisit?.toIso8601String().toAssetDetailsFormat ?? ""),
@ -99,7 +100,7 @@ class _PpmDetailsPageState extends State<PpmDetailsPage> {
InfoTextWidget(label: context.translation.department, value: planPreventiveVisit.departmentName?.cleanupWhitespace.capitalizeFirstOfEach ?? '-'),
InfoTextWidget(label: context.translation.room, value: (planPreventiveVisit.roomName ?? "").cleanupWhitespace.capitalizeFirstOfEach),
],
).toShadowContainer(context, padding: 16),
).toShadowContainer(context),
).expanded,
if (userProvider!.user!.type == UsersTypes.engineer && (!ppmProvider.isReadOnly)) ...[
FooterActionButton.footerContainer(

@ -7,7 +7,6 @@ import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/ppm/recurrent_wo.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
import 'package:test_sa/new_views/common_widgets/working_time_tile.dart';
@ -20,9 +19,11 @@ import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart
class RecurrentTaskInfoWidget extends StatefulWidget {
final RecurrentWoData? model;
bool isClosed;
final bool isClosed;
final bool isHouseKeeper;
final DateTime? timerStartTime;
RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false});
const RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false, this.isHouseKeeper = false, this.timerStartTime});
@override
State<RecurrentTaskInfoWidget> createState() => _RecurrentTaskInfoWidgetState();
@ -79,11 +80,16 @@ class _RecurrentTaskInfoWidgetState extends State<RecurrentTaskInfoWidget> {
),
8.height,
InfoHeader16Widget(widget.model!.title!),
InfoTextWidget(label: context.translation.taskNo, value: widget.model!.taskNo!),
InfoTextWidget(label: context.translation.taskNo, value: widget.model!.taskNo!, copyValue: true),
InfoTextWidget(label: 'Plan Name:'.addTranslation, value: widget.model?.planRecurrent?.planName),
InfoTextWidget(label: context.translation.site, value: widget.model!.site!.siteName!),
InfoTextWidget(label: context.translation.assignedEmployee, value: widget.model!.engineer!.userName ?? ""),
InfoTextWidget(label: context.translation.scheduledDate, value: widget.model!.scheduleDate!.toMonthYearFormat),
if (widget.isHouseKeeper) ...[
'${context.translation.department}: ${widget.model!.department?.name ?? "-"}'.bodyText2(context).custom(color: AppColor.neutral120),
'${context.translation.floor}: ${widget.model!.floor?.name ?? "-"}'.bodyText2(context).custom(color: AppColor.neutral120),
'${context.translation.room}: ${widget.model!.room?.name ?? "-"}'.bodyText2(context).custom(color: AppColor.neutral120),
],
InfoTextWidget(label: context.translation.assignedEmployee, value: widget.model!.engineer?.userName ?? ""),
InfoTextWidget(label: context.translation.scheduledDate, value: widget.model!.scheduleDate!.toServiceRequestDetailsFormat),
],
).toShadowContainer(context),
12.height,
@ -92,16 +98,20 @@ class _RecurrentTaskInfoWidgetState extends State<RecurrentTaskInfoWidget> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildingInfoWidget(label: context.translation.department, value: widget.model!.department?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context)
.ignorePointer(widget.isClosed),
8.height,
buildingInfoWidget(label: context.translation.floor, value: widget.model!.floor?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed),
8.height,
buildingInfoWidget(label: context.translation.room, value: widget.model!.room?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed),
8.height,
if (!widget.isHouseKeeper) ...[
buildingInfoWidget(label: context.translation.department, value: widget.model!.department?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context)
.ignorePointer(widget.isClosed),
8.height,
buildingInfoWidget(label: context.translation.floor, value: widget.model!.floor?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed),
8.height,
buildingInfoWidget(label: context.translation.room, value: widget.model!.room?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed),
8.height,
],
_timerWidget(context, totalWorkingHours),
8.height,
_commentWidget(context: context, model: widget.model).ignorePointer(widget.isClosed),
if (!widget.isHouseKeeper) ...[
8.height,
_commentWidget(context: context, model: widget.model).ignorePointer(widget.isClosed),
],
],
// ],
).toShadowContainer(context, padding: 12)
@ -113,6 +123,27 @@ class _RecurrentTaskInfoWidgetState extends State<RecurrentTaskInfoWidget> {
}
Widget _timerWidget(BuildContext context, double totalWorkingHours) {
// For housekeeper: show read-only timer counter with history
if (widget.isHouseKeeper) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!widget.isClosed) ...[
_HousekeeperTimerWidget(
startTime: widget.timerStartTime,
isClosed: widget.isClosed,
),
12.height,
],
if (totalWorkingHours > 0.0)
WorkingTimeTile(
timerList: timerList,
totalWorkingTime: totalWorkingHours,
),
],
);
}
return Consumer<AllRequestsProvider>(builder: (context, snapshot, child) {
return Column(
mainAxisSize: MainAxisSize.min,
@ -169,6 +200,92 @@ class _RecurrentTaskInfoWidgetState extends State<RecurrentTaskInfoWidget> {
}
}
// Read-only timer widget for housekeeper
class _HousekeeperTimerWidget extends StatefulWidget {
final DateTime? startTime;
final bool isClosed;
const _HousekeeperTimerWidget({this.startTime, this.isClosed = false});
@override
State<_HousekeeperTimerWidget> createState() => _HousekeeperTimerWidgetState();
}
class _HousekeeperTimerWidgetState extends State<_HousekeeperTimerWidget> {
String _elapsedTime = "0:00:00";
@override
void initState() {
super.initState();
if (widget.startTime != null && !widget.isClosed) {
_startTimer();
} else if (widget.startTime != null && widget.isClosed) {
// If closed, calculate the final time
_updateElapsedTime();
}
}
void _updateElapsedTime() {
if (widget.startTime != null) {
final duration = DateTime.now().difference(widget.startTime!);
final hours = duration.inHours;
final minutes = duration.inMinutes % 60;
final seconds = duration.inSeconds % 60;
_elapsedTime = "$hours:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}";
}
}
void _startTimer() {
Future.delayed(const Duration(seconds: 1), () {
if (mounted && widget.startTime != null && !widget.isClosed) {
setState(() {
_updateElapsedTime();
});
_startTimer();
}
});
}
@override
Widget build(BuildContext context) {
if (widget.startTime == null && !widget.isClosed) {
return SizedBox.shrink();
}
return Container(
height: 56.toScreenHeight,
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth),
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(10),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.05),
// blurRadius: 10,
// )
// ],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
context.translation.timer.tinyFont(context),
_elapsedTime.bodyText(context).custom(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
],
),
),
// No play/pause icon for housekeeper - timer runs automatically
],
),
);
}
}
Widget buildingInfoWidget({required String label, required String? value, required BuildContext context}) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,

@ -1,7 +1,4 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/extensions/context_extension.dart';
@ -9,7 +6,6 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/ppm/recurrent_wo.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.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';
@ -17,6 +13,7 @@ import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/pages/land_page/land_page.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import '../../../../../../controllers/providers/api/all_requests_provider.dart';
import 'components/room_tabs_widget.dart';
@ -40,7 +37,6 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
@override
void initState() {
allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
// calculateWorkingTime();
Provider.of<AllRequestsProvider>(context, listen: false).getRecurrentWoById(id: widget.taskId!);
super.initState();
}
@ -77,7 +73,12 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RecurrentTaskInfoWidget(isClosed: isClosed, model: requestProvider.recurrentWoData),
RecurrentTaskInfoWidget(
isClosed: isClosed,
model: requestProvider.recurrentWoData,
isHouseKeeper: false,
timerStartTime: null,
),
IgnorePointer(
ignoring: isClosed,
child: requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty
@ -89,24 +90,6 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
)
: const SizedBox(),
),
16.height,
requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty
? CleanerChecklistWidget(
isClosed: isClosed,
checklistItems: requestProvider.recurrentWoData!.planRecurrentTaskCheckLists ?? [],
onChecklistChanged: (value) {
setState(() {
requestProvider.recurrentWoData!.planRecurrentTaskCheckLists = value;
});
},
capturedImageBase64: requestProvider.recurrentWoData!.imageRoom,
onImageCaptured: (imageBase64) {
setState(() {
requestProvider.recurrentWoData!.imageRoom = imageBase64;
});
},
)
: const SizedBox(),
],
),
).expanded,
@ -127,7 +110,6 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
AppFilledButton(
label: context.translation.complete,
buttonColor: AppColor.primary10,
// textColor: context.isDark?Colors.white: AppColor.black10,
onPressed: () => _updateTask(context: context, status: 1),
).expanded,
],
@ -146,114 +128,157 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
});
});
}
}
void _updateTask({required BuildContext context, required int status}) async {
AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
void _updateTask({required BuildContext context, required int status}) async {
// Proceed with update
_performUpdate(context: context, status: status);
}
// Only validate checklist if it exists and status is complete
if (status == 1 && allRequestsProvider.recurrentWoData?.planRecurrentTaskCheckLists != null && allRequestsProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty) {
// Validate mandatory checklist items only
if (!_validateChecklist(allRequestsProvider.recurrentWoData!)) {
return;
}
void _performUpdate({required BuildContext context, required int status}) async {
AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
// Image validation is handled by backend - no need to force capture here
}
if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status)) {
// Use existing timer logic
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? [];
if (validate(model: allRequestsProvider.recurrentWoData!, status: status)) {
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? [];
if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) {
int durationInSecond = allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt!.difference(allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(),
endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
}
allRequestsProvider.recurrentWoData?.timerModelList?.forEach((timer) {
int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: timer.startAt!.toIso8601String(),
endTime: timer.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
});
if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) {
int durationInSecond = allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt!.difference(allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(),
endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
}
allRequestsProvider.recurrentWoData?.timerModelList?.forEach((timer) {
int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: timer.startAt!.toIso8601String(),
endTime: timer.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
});
log('payload for update is ${allRequestsProvider.recurrentWoData?.toJson()}');
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
final uniqueTimersMap = <String, PlanRecurrentTaskTimers>{};
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) {
if (timer.startTime != null && timer.endTime != null) {
final key = '${timer.id}_${timer.startTime}_${timer.endTime}';
uniqueTimersMap[key] = timer;
}
});
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = uniqueTimersMap.values.toList();
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
await allRequestsProvider
.updateRecurrentWo(
status: status,
)
.then((success) {
Navigator.pop(context); // Close loading dialog
if (success) {
if (status == 1) {
// When complete is clicked, navigate to LandPage (Dashboard)
allRequestsProvider.reset();
allRequestsProvider.getAllRequests(context, typeTransaction: 5);
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
await allRequestsProvider.updateRecurrentWo(status: status, showToast: false).then((result) {
Navigator.pop(context); // Close loading dialog
// Navigate to LandPage and clear all previous routes
Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => false);
bool success = result['success'] ?? false;
String message = result['message'] ?? '';
if (success) {
if (status == 1) {
// When complete is clicked, navigate to LandPage (Dashboard)
allRequestsProvider.reset();
allRequestsProvider.getAllRequests(context, typeTransaction: 5);
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
// Navigate to LandPage and clear all previous routes
Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => false);
} else {
// For save (status 0), just pop the current screen
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
Navigator.pop(context);
}
} else {
// For save (status 0), just pop the current screen
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
Navigator.pop(context);
// Show error dialog with the exact message from API
_showErrorDialog(context, message.isNotEmpty ? message : "Failed to update the task. Please try again.");
}
}
});
}).catchError((error) {
Navigator.pop(context); // Close loading dialog
_showErrorDialog(context, error.toString());
});
}
}
}
bool _validateChecklist(RecurrentWoData model) {
// Check if all mandatory checklist items are checked
final uncheckedMandatory = model.planRecurrentTaskCheckLists!.where((item) => item.isMandatory && !item.isChecked).toList();
if (uncheckedMandatory.isNotEmpty) {
Fluttertoast.showToast(
msg: "Please complete all mandatory tasks (marked with *)",
toastLength: Toast.LENGTH_LONG,
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
return true;
}
bool validate({required RecurrentWoData model, required int status}) {
bool validate({required BuildContext context, required RecurrentWoData model, required int status}) {
// Timer validation logic
bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false;
if (isTimerPickerEnable) {
if (model.recurrentWoTimerModel?.startAt == null && model.recurrentWoTimePicker == null) {
Fluttertoast.showToast(msg: "Working Hours Required");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Working Hours Required",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
if (model.recurrentWoTimePicker == null) {
if (model.recurrentWoTimerModel?.startAt == null) {
Fluttertoast.showToast(msg: "Working Hours Required");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Working Hours Required",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
if (model.recurrentWoTimerModel?.endAt == null) {
Fluttertoast.showToast(msg: "Please Stop The Timer");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Please Stop The Timer",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
}
} else {
if (model.recurrentWoTimerModel?.startAt == null) {
Fluttertoast.showToast(msg: "Working Hours Required");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Working Hours Required",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
if (model.recurrentWoTimerModel?.endAt == null) {
Fluttertoast.showToast(msg: "Please Stop The Timer");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Please Stop The Timer",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
}

@ -91,6 +91,7 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
return Scaffold(
appBar: DefaultAppBar(
title: context.translation.filter,
showBackButtonAsCancel: true,
actions: [
if (loading == false && _showResetButton())
TextButton(
@ -168,7 +169,7 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
Row(
children: [
ADatePicker(
label: context.translation.to,
label: context.translation.from,
from: DateTime(2000),
date: DateTime.tryParse(startDate ?? ""),
onDatePicker: (date) {

@ -51,7 +51,6 @@ class _DeviceSearchScreenState extends State<DeviceSearchScreen> {
_modelController.dispose();
_supplierController.dispose();
_siteController.dispose();
_siteController.dispose();
_manufactureController.dispose();
_mdController.dispose();
super.dispose();
@ -62,6 +61,7 @@ class _DeviceSearchScreenState extends State<DeviceSearchScreen> {
return Scaffold(
appBar: DefaultAppBar(
title: context.translation.search,
showBackButtonAsCancel: true,
actions: [
if (_showResetButton())
TextButton(

@ -94,7 +94,7 @@ class _DeviceTransferDetailsState extends State<DeviceTransferDetails> {
),
8.height,
InfoHeader16Widget(context.translation.callDetails),
InfoTextWidget(label: context.translation.trNumber, value: _model?.transferCode ?? "-"),
InfoTextWidget(label: context.translation.trNumber, value: _model?.transferCode ?? "-", copyValue: true),
InfoTextWidget(label: context.translation.createdBy, value: _model?.name ?? "-"),
const Divider().defaultStyle(context),
InfoHeader16Widget(context.translation.assetInformation),

@ -204,7 +204,7 @@ class _GasRefillDetailsPageState extends State<GasRefillDetailsPage> {
),
8.height,
InfoHeader16Widget(context.translation.callDetails),
InfoTextWidget(label: context.translation.requestNo, value: gasRefillModel.gasRefillNo ?? '-'),
InfoTextWidget(label: context.translation.requestNo, value: gasRefillModel.gasRefillNo ?? '-', copyValue: true),
InfoTextWidget(label: context.translation.createdBy, value: gasRefillModel.name ?? '-'),
InfoTextWidget(label: context.translation.gasType, value: gasRefillModel.gasRefillDetails?[0].gasType?.name ?? '-'),
InfoTextWidget(label: context.translation.cylinderType, value: gasRefillModel.gasRefillDetails?[0].cylinderType?.name ?? '-'),

@ -103,6 +103,7 @@ class _TaskRequestDetailsViewState extends State<TaskRequestDetailsView> {
? const ALoading()
: Column(children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -136,7 +137,7 @@ class _TaskRequestDetailsViewState extends State<TaskRequestDetailsView> {
const Divider().defaultStyle(context),
InfoHeader16Widget("Task Details".addTranslation),
InfoTextWidget(label: context.translation.taskType, value: taskModel?.taskType?.typeName ?? "-"),
InfoTextWidget(label: context.translation.taskNo, value: taskModel?.taskJobNo ?? "-"),
InfoTextWidget(label: context.translation.taskNo, value: taskModel?.taskJobNo ?? "-", copyValue: true),
InfoTextWidget(label: context.translation.createdBy, value: taskModel?.userCreated?.userName ?? "-"),
if (taskProvider.taskRequestModel?.taskType?.isRecallAndAlert == true) ...[
recallAlertTypeWidget(taskModel: taskProvider.taskRequestModel!),
@ -162,7 +163,7 @@ class _TaskRequestDetailsViewState extends State<TaskRequestDetailsView> {
],
buildTechnicalComments(taskModel: taskModel),
],
).toShadowContainer(context, padding: 16),
).toShadowContainer(context),
).expanded,
if (taskProvider.taskRequestModel?.taskType?.isRecallAndAlert == false &&
context.settingProvider.isUserFlowMedical &&
@ -243,12 +244,13 @@ class _TaskRequestDetailsViewState extends State<TaskRequestDetailsView> {
return RichText(
text: TextSpan(
text: "${context.translation.totalWorkingTime}: ",
style: TextStyle(
fontSize: 14.toScreenWidth, fontWeight: FontWeight.w500, fontStyle: FontStyle.normal, decoration: TextDecoration.none, color: context.isDark ? AppColor.neutral10 : AppColor.neutral20),
style: AppTextStyles.bodyText2.copyWith(color: AppColor.lightTextColor(context)),
// TextStyle(
// fontSize: 14.toScreenWidth, fontWeight: FontWeight.w500, fontStyle: FontStyle.normal, decoration: TextDecoration.none, color: context.isDark ? AppColor.neutral10 : AppColor.neutral20),
children: [
TextSpan(text: "$totalHourString ", style: AppTextStyles.bodyText),
const WidgetSpan(
child: Icon(Icons.info_outline_rounded, color: AppColor.primary10, size: 14),
TextSpan(text: "$totalHourString ", style: AppTextStyles.bodyText2.copyWith(color: AppColor.lightTextColor(context))),
WidgetSpan(
child: Icon(Icons.info_outline_rounded, color: AppColor.loadingColor(context), size: 14),
),
],
),

@ -57,12 +57,12 @@ class _AssetAutoCompleteFieldState extends State<AssetAutoCompleteField> {
@override
Widget build(BuildContext context) {
final border = UnderlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(10));
final border = UnderlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(16));
return Container(
decoration: BoxDecoration(
color: AppColor.background(context),
borderRadius: BorderRadius.circular(AppStyle.borderRadius * AppStyle.getScaleFactor(context)),
// boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)],
border: Border.all(color: AppColor.border2Color(context), width: 1),
borderRadius: BorderRadius.circular(16),
),
child: Autocomplete<AssetNDAutoCompleteByDynamicCodesModel>(
optionsBuilder: (TextEditingValue textEditingValue) async {
@ -90,7 +90,7 @@ class _AssetAutoCompleteFieldState extends State<AssetAutoCompleteField> {
return TextField(
controller: _controller,
focusNode: fieldFocusNode,
style: AppTextStyles.bodyText.copyWith(color: AppColor.black10),
style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 14, color: AppColor.textStyleColor(context)),
textAlign: TextAlign.start,
decoration: InputDecoration(
border: border,
@ -98,15 +98,15 @@ class _AssetAutoCompleteFieldState extends State<AssetAutoCompleteField> {
focusedBorder: border,
enabledBorder: border,
errorBorder: border,
contentPadding: EdgeInsets.symmetric(vertical: 8.toScreenHeight, horizontal: 16.toScreenWidth),
contentPadding: EdgeInsets.symmetric(vertical: 12.toScreenHeight, horizontal: 16.toScreenWidth),
constraints: const BoxConstraints(),
suffixIconConstraints: const BoxConstraints(maxHeight: 24, maxWidth: 24 + 8),
filled: true,
fillColor: AppColor.fieldBgColor(context),
fillColor: AppColor.dropDownBackground(context),
errorStyle: AppTextStyle.tiny.copyWith(color: context.isDark ? AppColor.red50 : AppColor.red60),
floatingLabelStyle: AppTextStyle.body1.copyWith(fontWeight: FontWeight.w500, color: context.isDark ? null : AppColor.neutral20),
floatingLabelStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.floatingTextStyleColor(context)),
labelStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.labelTextStyleColor(context)),
labelText: "Name/Oracle Code",
labelStyle: AppTextStyles.tinyFont.copyWith(color: AppColor.textColor(context)),
suffixIcon: loading ? CircularProgressIndicator(color: AppColor.loadingColor(context), strokeWidth: 3.0).paddingOnly(end: 8) : null,
),
textInputAction: TextInputAction.search,

@ -1,3 +1,7 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
@ -7,13 +11,14 @@ import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/helper/utils.dart';
import 'package:test_sa/models/device/asset.dart';
import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/new_models/traf_customer.dart';
import 'package:test_sa/models/new_models/traf_department.dart';
import 'package:test_sa/models/new_models/users_based_on_search_model.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/traf_module/asset_auto_complete_field.dart';
import 'package:test_sa/modules/traf_module/traf_request_detail_page.dart';
import 'package:test_sa/modules/traf_module/update_traf_request_page.dart';
import 'package:test_sa/modules/traf_module/users_auto_complete_field.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
@ -21,11 +26,12 @@ 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/multiple_item_drop_down_menu.dart';
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
import 'package:test_sa/providers/loading_list_notifier.dart';
import 'package:test_sa/providers/lookups/customer_lookup_provider.dart';
import 'package:test_sa/providers/lookups/department_lookup_provider.dart';
import 'package:test_sa/providers/lookups/request_type_lookup_provider.dart';
import 'package:test_sa/providers/lookups/yes_no_lookup_provider.dart';
import 'package:test_sa/views/widgets/equipment/asset_picker.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
import 'traf_request_model.dart';
import 'traf_request_provider.dart';
@ -52,27 +58,47 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
Lookup? otherServicesEffects;
Lookup? useInCombination;
TrafCustomer? selectedSite;
TrafDepartment? selectedDepartment;
TrafRequestDataModel? trafRequest;
List<Lookup> abc = [];
List<TrafDepartment> departments = [];
List<UsersBasedOnSearchModel> userBasedOnSearch = [];
List<GenericAttachmentModel> attachments = [];
FocusNode otherServicesEffectsNode = FocusNode();
FocusNode useInCombinationNode = FocusNode();
FocusNode departmentNode = FocusNode();
FocusNode siteDepartmentNode = FocusNode();
@override
void initState() {
super.initState();
trafRequest = TrafRequestDataModel(id: 0, attachments: [], departments: [], qty: 1);
resetProviders();
WidgetsBinding.instance.addPostFrameCallback((_) {
final requestTypeProvider = Provider.of<RequestTypeLookupProvider>(context, listen: false);
final yesNoProvider = Provider.of<YesNoLookupProvider>(context, listen: false);
final customerProvider = Provider.of<CustomerLookupProvider>(context, listen: false);
if (requestTypeProvider.items.isEmpty) {
requestTypeProvider.reset();
}
if (yesNoProvider.items.isEmpty) {
yesNoProvider.reset();
}
final userId = context.userProvider.user?.userID;
if (customerProvider.items.isEmpty && userId != null && userId.isNotEmpty) {
customerProvider.getDataByUserId(userId);
}
log('user id is ${userId}');
});
}
void resetProviders() {
Provider.of<RequestTypeLookupProvider>(context, listen: false).reset();
// Provider.of<YesNoLookupProvider>(context, listen: false).reset();
Provider.of<YesNoLookupProvider>(context, listen: false).reset();
}
@override
@ -83,7 +109,7 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const DefaultAppBar(title: "TRAF Request"),
appBar: const DefaultAppBar(title: "Assessment of Need & Technology"),
body: Form(
key: _formKey,
child: Column(
@ -181,6 +207,48 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, fontWeight: FontWeight.w600),
),
12.height,
SingleItemDropDownMenu<TrafCustomer, CustomerLookupProvider>(
context: context,
showAsBottomSheet: true,
backgroundColor: AppColor.neutral100,
showShadow: false,
title: "Site",
requestById: int.tryParse(context.userProvider.user?.userID ?? ''),
initialValue: selectedSite,
onSelect: (value) {
selectedSite = value;
trafRequest?.siteId = value?.customerId;
// Reset department when site changes
selectedDepartment = null;
trafRequest?.departmentId = null;
// Reset and fetch department data based on selected site
Provider.of<DepartmentLookupProvider>(context, listen: false).reset();
if (selectedSite != null && selectedSite!.customerId != null) {
// Fetch departments for the selected site
Provider.of<DepartmentLookupProvider>(context, listen: false).getData(id: selectedSite!.customerId);
siteDepartmentNode.requestFocus();
}
setState(() {});
},
),
12.height,
SingleItemDropDownMenu<TrafDepartment, DepartmentLookupProvider>(
context: context,
showAsBottomSheet: true,
backgroundColor: AppColor.neutral100,
showShadow: false,
title: "Department",
requestById: null,
// initialValue: selectedDepartment,
enabled: selectedSite != null,
onSelect: (value) {
selectedDepartment = value;
trafRequest?.departmentId = value?.id?.toString();
setState(() {});
},
),
12.height,
AppTextFormField(
initialValue: "",
labelText: "How would the requested technology solve the current situation and/or serve the purpose?",
@ -444,7 +512,18 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
trafRequest?.usedWithCombination = value;
},
),
]
],
12.height,
AttachmentPicker(
label: context.translation.attachFiles,
attachment: attachments,
buttonColor: AppColor.black10,
onlyImages: false,
buttonIcon: 'image-plus'.toSvgAsset(color: AppColor.primary10),
onChange: (attachment) {
attachments = attachment;
},
),
],
).toShadowContainer(context, padding: 12, borderRadius: 20),
16.height,
@ -452,6 +531,7 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
children: [
Checkbox(
value: _acknowledgement,
side: BorderSide(color: AppColor.loadingColor(context), width: 1.0),
visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
activeColor: AppColor.blueStatus(context),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
@ -492,10 +572,25 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
"Pick asset".showToast;
return;
}
// Validate site and department selection
if (selectedSite == null) {
"Please select a site".showToast;
return;
}
if (selectedDepartment == null) {
"Please select a department".showToast;
return;
}
if (!_formKey.currentState!.validate()) return;
trafRequest?.employeeId = context.userProvider.user?.userID;
trafRequest?.siteId = context.userProvider.user?.clientId;
// Set site and department IDs from selected dropdowns
trafRequest?.siteId = selectedSite?.customerId;
trafRequest?.departmentId = selectedDepartment?.id;
trafRequest?.trafContacts = [];
trafRequest?.trafContacts = userBasedOnSearch.map((item) => TrafContacts(id: 0, trafId: 0, name: item.userName, userId: item.userId)).toList();
@ -522,8 +617,17 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
return;
}
// Process attachments similar to CreateCMRequest
trafRequest?.attachments = [];
for (var item in attachments) {
String fileName = CMRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? '';
trafRequest?.attachments!.add(
Attachments(id: 0, trafId: 0, attachmentName: fileName),
);
}
Utils.showLoading(context);
bool isSuccess = await Provider.of<TrafRequestProvider>(context, listen: false).addTraf(trafRequest!.toJson());
bool isSuccess = await Provider.of<TrafRequestProvider>(context, listen: false).addTraf(trafRequest!.toJsonNew());
Utils.hideLoading(context);
if (isSuccess) {
Navigator.pop(context);

@ -1,18 +1,25 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart';
import 'package:test_sa/modules/traf_module/traf_request_provider.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.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 '../../controllers/api_routes/urls.dart';
import 'asset_analytics_model.dart';
import 'traf_request_model.dart';
@ -30,10 +37,14 @@ class TrafRequestDetailPage extends StatefulWidget {
}
class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
List<File> _attachments = [];
@override
void initState() {
super.initState();
Provider.of<TrafRequestProvider>(context, listen: false).getTRAFById(widget.trafId);
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<TrafRequestProvider>(context, listen: false).getTRAFById(widget.trafId);
});
}
@override
@ -45,12 +56,15 @@ class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
Widget build(BuildContext context) {
bool isEngineer = (Provider.of<UserProvider>(context, listen: false).user?.type) == UsersTypes.engineer;
return Scaffold(
appBar: const DefaultAppBar(title: "TRAF Request"),
appBar: const DefaultAppBar(title: "Assessment of Need & Technology"),
body: Selector<TrafRequestProvider, bool>(
selector: (_, myModel) => myModel.isLoading, // Selects only the userName
builder: (_, isLoading, __) {
if (isLoading) return const ALoading();
TrafRequestProvider trafProvider = Provider.of<TrafRequestProvider>(context, listen: false);
if (trafProvider.trafRequestDataModel?.attachments != null) {
_attachments = trafProvider.trafRequestDataModel!.attachments!.map((e) => File(URLs.getFileUrl(e.attachmentName ?? '') ?? '')).toList();
}
return trafProvider.trafRequestDataModel == null
? const NoDataFound().center
: Column(
@ -105,28 +119,42 @@ class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader16Widget("Request Information"),
InfoTextWidget(label: 'TRAF No', value: data.reqCode ?? '-'),
InfoTextWidget(label: 'Request No', value: data.reqCode ?? '-', copyValue: true),
InfoTextWidget(label: 'Request type', value: data.requestTypeName ?? '-'),
InfoTextWidget(label: 'How would the requested technology solve the current situation and/or serve the purpose?', value: data.purposeAnswer ?? '-'),
InfoTextWidget(label: 'What is the current practice?', value: data.currentPractise ?? '-'),
if (data.requestTypeValue == 1) ...[
4.height,
'Requested Quantity: ${data.qty ?? '-'}'.bodyText(context), //todo @waseem replace with infotext
],
InfoTextWidget(label: 'How would the requested technology solve the current situation and/or serve the purpose?', value: "\n${data.purposeAnswer ?? '-'}"),
InfoTextWidget(label: 'What is the current practice?', value: "\n${data.currentPractise ?? '-'}"),
InfoTextWidget(label: 'Census Q1', value: data.censusQ1 ?? '-'),
InfoTextWidget(label: 'Census Q2', value: data.censusQ2 ?? '-'),
InfoTextWidget(label: 'Census Q3', value: data.censusQ3 ?? '-'),
InfoTextWidget(label: 'Census Q4', value: data.censusQ4 ?? '-'),
InfoTextWidget(
label: 'List down names & contact information of users going to use the technology & specify if they are part-time or full time?',
value: data.trafContacts?.map((item) => item.name).toList().toString() ?? '-'),
InfoTextWidget(label: 'Is the requesting department going to use the technology solely or shared with other departments?', value: data.usingSolelyOrSharedName ?? '-'),
value: "\n${data.trafContacts?.map((item) => item.name).toList().toString() ?? '-'}"),
InfoTextWidget(label: 'Is the requesting department going to use the technology solely or shared with other departments?', value: "\n${data.usingSolelyOrSharedName ?? '-'}"),
if ((data.departments ?? []).isNotEmpty) ...[
InfoTextWidget(label: 'Technology used with other departments', value: data.departments?.map((item) => item.departmentName).toList().toString() ?? '-'),
InfoTextWidget(label: 'Technology used with other departments', value: "\n${data.departments?.map((item) => item.departmentName).toList().toString() ?? '-'}"),
],
InfoTextWidget(label: 'Would other services be effected by acquiring the new equipment?', value: data.isEffectedName ?? '-'),
InfoTextWidget(label: 'Would other services be effected by acquiring the new equipment?', value: "\n${data.isEffectedName ?? '-'}"),
if ((data.effectedServices ?? "").isNotEmpty) ...[
InfoTextWidget(label: 'List of services would it be effected', value: data.effectedServices ?? '-'),
InfoTextWidget(label: 'List of services would it be effected', value: "\n${data.effectedServices ?? '-'}"),
],
InfoTextWidget(label: 'Is the equipment going to be used with combination of other equipment to accomplish a specific procedure?', value: data.isCombinationName ?? '-'),
InfoTextWidget(label: 'Is the equipment going to be used with combination of other equipment to accomplish a specific procedure?', value: "\n${data.isCombinationName ?? '-'}"),
if ((data.usedWithCombination ?? "").isNotEmpty) ...[
InfoTextWidget(label: 'Equipment going to be used with combination of other equipments', value: data.usedWithCombination ?? '-'),
InfoTextWidget(label: 'Equipment going to be used with combination of other equipments', value: "\n${data.usedWithCombination ?? '-'}"),
],
if (_attachments.isNotEmpty) ...[
4.height,
Text(
// todo @waseem conver to header
"Attachments".addTranslation,
style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
),
// 8.height,
FilesList(images: _attachments.map((e) => URLs.getFileUrl(e.path ?? '') ?? '').toList() ?? []),
],
],
).toShadowContainer(context);

@ -43,9 +43,9 @@ class TrafRequestItemView extends StatelessWidget {
InfoDateWidget(requestData!.transactionDate ?? ""),
],
),
InfoHeaderWidget(requestData?.typeTransaction ?? "TRAF Request"),
InfoHeaderWidget(requestData?.typeTransaction ?? "Assessment of Need & Technology"),
InfoTextWidget(label: context.translation.requestType, value: requestData?.requestTypeName),
InfoTextWidget(label: "TRAF No", value: requestData?.requestNo, showArrow: true),
InfoTextWidget(label: "Request No", value: requestData?.requestNo, showArrow: true),
],
).toShadowContainer(context, withShadow: showShadow).onPress(() async {
Navigator.push(context, MaterialPageRoute(builder: (context) => TrafRequestDetailPage(trafId: requestData!.id!)));
@ -73,10 +73,10 @@ class TrafRequestItemView extends StatelessWidget {
InfoDateWidget(requestDetails!.date ?? ""),
],
),
InfoHeaderWidget(requestDetails?.nameOfType ?? "TRAF Request"),
InfoHeaderWidget(requestDetails?.nameOfType ?? "Assessment of Need & Technology"),
InfoTextWidget(label: context.translation.requestType, value: requestDetails!.requestType),
InfoTextWidget(label: "TRAF No", value: requestDetails!.requestNo),
InfoTextWidget(label: context.translation.site, value: requestDetails!.site, showEmptyValue: true,showArrow: true),
InfoTextWidget(label: "Request No", value: requestDetails!.requestNo),
InfoTextWidget(label: context.translation.site, value: requestDetails!.site, showEmptyValue: true, showArrow: true),
],
).toShadowContainer(context, withShadow: showShadow).onPress(() async {
Navigator.push(context, MaterialPageRoute(builder: (context) => TrafRequestDetailPage(trafId: requestDetails!.id!)));

@ -51,7 +51,7 @@ class TrafRequestDataModel {
String? employeeEmail;
int? positionId;
String? positionName;
String? departmentId;
dynamic departmentId;
String? departmentName;
List<Departments>? departments;
List<Departments>? trafDepartments;
@ -88,10 +88,12 @@ class TrafRequestDataModel {
String? isEffectedName;
String? effectedServices;
int? isCombinationId;
int?requestTypeValue;
String? isCombinationName;
String? usedWithCombination;
String? comment;
List<Attachments>? attachments;
bool? isFromMobile;
int? isBudgetId;
String? isBudgetName;
String? firstLineManagerId;
@ -186,9 +188,11 @@ class TrafRequestDataModel {
this.isEffectedName,
this.effectedServices,
this.isCombinationId,
this.requestTypeValue,
this.isCombinationName,
this.usedWithCombination,
this.comment,
this.isFromMobile = true,
this.attachments,
this.isBudgetId,
this.isBudgetName,
@ -299,9 +303,11 @@ class TrafRequestDataModel {
isEffectedName = json['isEffectedName'];
effectedServices = json['effectedServices'];
isCombinationId = json['isCombinationId'];
requestTypeValue = json['requestTypeValue'];
isCombinationName = json['isCombinationName'];
usedWithCombination = json['usedWithCombination'];
comment = json['comment'];
isFromMobile = json['isFromMobile'];
if (json['attachments'] != null) {
attachments = <Attachments>[];
json['attachments'].forEach((v) {
@ -415,9 +421,11 @@ class TrafRequestDataModel {
data['isEffectedName'] = this.isEffectedName;
data['effectedServices'] = this.effectedServices;
data['isCombinationId'] = this.isCombinationId;
data['requestTypeValue'] = this.requestTypeValue;
data['isCombinationName'] = this.isCombinationName;
data['usedWithCombination'] = this.usedWithCombination;
data['comment'] = this.comment;
data['isFromMobile'] = this.isFromMobile;
if (this.attachments != null) {
data['attachments'] = this.attachments!.map((v) => v.toJson()).toList();
} else {
@ -474,6 +482,68 @@ class TrafRequestDataModel {
data['modifiedDate'] = this.modifiedDate;
return data;
}
Map<String, dynamic> toJsonNew() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['reqNo'] = reqNo;
data['reqCode'] = reqCode;
data['employeeId'] = employeeId;
data['positionId'] = positionId;
data['departments'] =
departments?.map((v) => v.toJson()).toList() ?? [];
if (this.trafDepartments != null) {
data['trafDepartments'] = this.trafDepartments!.map((v) => v.toJson()).toList();
}
data['siteId'] = siteId;
data['departmentId'] = departmentId;
data['siteName'] = siteName;
data['requesterExtensionNumber'] = requesterExtensionNumber;
data['requesterExtensionName'] = requesterExtensionName;
data['requesterExtensionPositionId'] = requesterExtensionPositionId;
data['requestTypeId'] = requestTypeId;
data['assetNDId'] = assetNDId;
data['assetId'] = assetId;
data['qty'] = qty;
data['purposeAnswer'] = purposeAnswer;
data['currentPractise'] = currentPractise;
data['censusQ1'] = censusQ1;
data['censusQ2'] = censusQ2;
data['censusQ3'] = censusQ3;
data['censusQ4'] = censusQ4;
data['isFromMobile'] = isFromMobile;
data['trafContacts'] =
trafContacts?.map((v) => v.toJson()).toList() ?? [];
data['usingSolelyOrSharedId'] = usingSolelyOrSharedId;
data['isEffectedId'] = isEffectedId;
data['effectedServices'] = effectedServices;
data['isCombinationId'] = isCombinationId;
data['usedWithCombination'] = usedWithCombination;
data['comment'] = comment;
data['isBudgetId'] = isBudgetId;
data['attachments'] =
attachments?.map((v) => v.toJson()).toList() ?? [];
return data;
}
}
class TrafContacts {

@ -19,12 +19,13 @@ class AppBottomNavigationBar extends StatelessWidget {
bool isEngineer = (Provider.of<UserProvider>(context, listen: false).user?.type) == UsersTypes.engineer;
bool isUserFMS = Provider.of<SettingProvider>(context, listen: false).isUserFMS;
bool commissioningEng = context.userProvider.isCommissioningEngineer;
bool isHouseKeeper = context.userProvider.isHouseKeeper;
List<NavItemModel> navItems = [
NavItemModel(0, "overview", context.translation.overview),
NavItemModel(1, "request_icon", context.translation.workOrder),
if (!commissioningEng) NavItemModel(2, "add_icon", context.translation.calendar, showLabel: false),
NavItemModel(3, "assets", context.translation.assets),
if(!isHouseKeeper) NavItemModel(1, "request_icon", context.translation.workOrder),
if (!commissioningEng && !isHouseKeeper) NavItemModel(2, "add_icon", context.translation.calendar, showLabel: false),
if(!isHouseKeeper) NavItemModel(3, "assets", context.translation.assets),
NavItemModel(4, "contact", context.translation.contact),
];

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
@ -20,7 +22,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
titleSpacing: 16,
title: Row(
children: [
const Icon(Icons.arrow_back_ios).onPress(() {
Icon(Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back).onPress(() {
Navigator.of(context).pop();
}),
Text(

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
@ -17,8 +19,11 @@ class DefaultAppBar extends StatelessWidget implements PreferredSizeWidget {
final bool showBackButton;
final VoidCallback? onWillPopScope;
final TextStyle? titleStyle;
final bool showBackButtonAsCancel;
const DefaultAppBar({this.title, this.onBackPress, this.onWillPopScope, this.actions,this.backgroundColor, this.titleStyle, this.showHomeActionButton = false, this.showBackButton = true, Key? key}) : super(key: key);
const DefaultAppBar(
{this.title, this.onBackPress, this.onWillPopScope, this.actions, this.titleStyle, this.showHomeActionButton = false, this.showBackButton = true,this.backgroundColor, this.showBackButtonAsCancel = false, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
@ -46,7 +51,7 @@ class DefaultAppBar extends StatelessWidget implements PreferredSizeWidget {
title: Row(
children: [
if (showBackButton)
const Icon(Icons.arrow_back_ios).onPress(() {
Icon(showBackButtonAsCancel ? Icons.clear : (Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back)).onPress(() {
if (onWillPopScope != null) {
showDialog(
context: context,
@ -65,11 +70,10 @@ class DefaultAppBar extends StatelessWidget implements PreferredSizeWidget {
Navigator.of(context).pop();
}
}),
10.width,
12.width,
Text(
title ?? "",
style: titleStyle ??
(context.isTablet() ? AppTextStyles.heading4 : AppTextStyles.heading3).copyWith(fontWeight: FontWeight.w500, color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
style: titleStyle ?? (AppTextStyles.heading5).copyWith(fontWeight: FontWeight.w500, color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
).expanded,
],
),

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -77,7 +79,11 @@ class ContactUsBottomSheet extends StatelessWidget {
iconSize: 20,
tooltip: "Copy",
onPressed: () {
Clipboard.setData(ClipboardData(text: sites[index].phoneNumber!));
Clipboard.setData(ClipboardData(text: sites[index].phoneNumber!)).then((value) {
if (Platform.isIOS) {
"Copied.".addTranslation.showToast;
}
});
},
),
IconButton(

@ -172,8 +172,8 @@ class CreateRequestModel {
if (context.userProvider.isQualityUser && context.settingProvider.isUserFlowMedical) {
list.add(CreateRequestModel("Equipment Internal Audit Checklist".addTranslation, "wo/equipment_audit_wo", CreateEquipmentInternalAuditForm.id));
list.add(CreateRequestModel("System Internal Audit Checklist".addTranslation, "wo/system_audit_wo", CreateSystemInternalAuditForm.id));
} else if (context.userProvider.isAssessor && context.settingProvider.isUserFlowMedical) {
list.add(CreateRequestModel("TRAF".addTranslation, "wo/traf_wo", CreateTRAFRequestPage.id));
} else if (context.userProvider.isAssessor && context.settingProvider.isUserFlowMedical) { // todo: handled in permission api, so no need to check here
// list.add(CreateRequestModel("TRAF".addTranslation, "wo/traf_wo", CreateTRAFRequestPage.id));
} else if (context.userProvider.isEngineer) {
if (Provider.of<SettingProvider>(context, listen: false).engineerCanCreateCM) {
list.add(CreateRequestModel(context.translation.correctiveMaintenance, "wo/cm_wo", CreateCMRequest.id));
@ -201,10 +201,15 @@ class CreateRequestModel {
// list.add(CreateRequestModel('Demo Request'.addTranslation, "add_icon", CreateDemoRequestPage.id));
}
}
// if (context.settingProvider.isUserFlowMedical && (context.userProvider.isAssessor || context.userProvider.isEngineer || context.userProvider.isNurse)) {
// //Traf name changed to Assessment of Need & Technology by client .
// list.add(CreateRequestModel("Assessment of Need & Technology".addTranslation, "add_icon", CreateTRAFRequestPage.id));
// }
context.userProvider.modulesPermissionsList.forEach((module) {
///TODO uncommit this to enable TRAF in production.
if (module.module!.value! == 9 && (module.canAdd ?? false)) {
list.add(CreateRequestModel("TRAF".addTranslation, "wo/traf_wo", CreateTRAFRequestPage.id));
list.add(CreateRequestModel(module.module?.name ?? "Assessment of Need & Technology".addTranslation, "wo/traf_wo", CreateTRAFRequestPage.id));
}
if (module.module!.value! == 15 && (module.canAdd ?? false)) {
list.add(CreateRequestModel('Demo Request'.addTranslation, "wo/demo_wo", CreateDemoRequestPage.id));

@ -98,7 +98,7 @@ class _LandPageState extends State<LandPage> {
Navigator.pop(context);
Navigator.of(context).pushNamed(SettingsPage.id);
},
label: "Enable".addTranslation),
label: "Continue".addTranslation),
],
)),
),
@ -115,7 +115,9 @@ class _LandPageState extends State<LandPage> {
WidgetsBinding.instance.addPostFrameCallback((_) {
_userProvider!.getSwipeLastTransaction(userId: _userProvider!.user!.userID!);
_userProvider!.getSiteContactInfo();
Provider.of<VendorProvider>(context, listen: false).getData();
if(!_userProvider!.isHouseKeeper) {
Provider.of<VendorProvider>(context, listen: false).getData();
}
});
}
_pages = <Widget>[

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save