export feature added for asset and my request

export_module
WaseemAbbasi22 2 months ago
parent 023a47f69f
commit 77f5d41757

@ -4,11 +4,11 @@ class URLs {
static const String appReleaseBuildNumber = "40";
// 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://atomsmdev.hmg.com"; // local DEV 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 V2 apis
static final String _baseUrl = "$_host/mobile"; // host local UAT
// static final String _baseUrl = "$_host/v1/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
@ -65,6 +65,7 @@ class URLs {
static get getAssets => "$_baseUrl/Asset/GetAssets"; // get
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 getModels => "$_baseUrl/ModelDefinition/GetModelDefinitionAsset"; // get ?client=2
static get getAllRequestsAndCount => "$_baseUrl/CallRequest/GetAllRequestsAndCount"; // get
static get updateUserContactInformation => "$_baseUrl/Account/UpdateContactInfo"; // get
@ -264,11 +265,17 @@ class URLs {
static get getYesNoRequestTypeLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=4";
static get getDemoPeriodLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=4101";
static get incidentStatusLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=3060";
static get incidentTypeLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=3061";
static get incidentRootCauseLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=3062";
static get incidentPersonInvolvedLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=3063";
static get clinicalNonClinicalLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=3064";
static get genderLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=3065";
static get getDemoDocumentTypes => "$_baseUrl/Lookups/GetLookup?lookupEnum=4106";
@ -421,15 +428,21 @@ class URLs {
static get getLoanPeriod => "$_baseUrl/Lookups/GetLookup?lookupEnum=3051";
static get getMedDepartmentBasedOnSite => "$_baseUrl/TRAFDataSource/GetDepartmentBasedOnSite";
static get getFlowMedicalDepartments => "$_baseUrl/FlowDepartments/All";
static get getLoanById => '$_baseUrl/Loan/GetLoanById';
static get getLoanByWorkOrderId => '$_baseUrl/Loan/GetLoanByWorkOrderId';
static get getIncidentById => '$_baseUrl/Incident/GetIncidentById';
static get getOvrTicketDetails => '$_baseUrl/Incident/GetOvrTicketDetails';
static get addLoan => '$_baseUrl/Loan/AddLoan';
static get loanWorkflowAction => '$_baseUrl/Loan/LoanWorkflowAction';
static get addIncident => '$_baseUrl/Incident/AddIncident';
static get addDemoTrialOutcome => '$_baseUrl/DemoRequest/addDemoTrialOutcome';
@ -448,7 +461,9 @@ class URLs {
static get getAssetDeliveryInspectionData => '$_baseUrl/AssetDeliveryExternalDeliveryInspection/GetAssetDeliveryExternalDeliveryInspectionByDeliveryId';
static get getOracleUserList => '$_baseUrl/AssetDeliveryExternal/GetEmployeesFromOracle';
static get saveMasterInfoDeliveryInspection => '$_baseUrl/AssetDeliveryExternalDeliveryInspection/SaveMaseterInfoDeliveryInspection';
static get sendEmailToAddSignature => '$_baseUrl/Signature/SendEmailToAddSignature';
static get getLineFromExternalPo => '$_baseUrl/AssetDeliveryExternalDeliveryInspection/GetLineFromExternalPo';
@ -533,5 +548,13 @@ class URLs {
static get updateDemoPeriodByRequester => "$_baseUrl/DemoRequest/UpdateDemoPeriodByRequester";
//export urls
static get exportFormatTypeLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=4015";
static get exportAsset => "$_baseUrl/Asset/Export";
static get getAllWorkOrderColumnsMetadata => "$_baseUrl/CallRequest/columns/metadata";
static get exportAllWorkOrder => "$_baseUrl/CallRequest/Export";
//4104 attachment type lookup
}
}

@ -105,9 +105,12 @@ import 'package:test_sa/providers/work_order/service_type_provider.dart';
import 'package:test_sa/providers/work_order/supplier_engineer_provider.dart';
import 'package:test_sa/providers/work_order/vendor_provider.dart';
import 'package:test_sa/providers/work_order/wo_frame_provider.dart';
import 'package:test_sa/modules/export_module/provider/export_columns_provider.dart';
import 'package:test_sa/modules/export_module/provider/export_format_provider.dart';
import 'package:test_sa/modules/export_module/provider/export_provider.dart';
import 'package:test_sa/modules/export_module/pages/export_form_view.dart';
import 'package:test_sa/views/pages/user/notifications/notifications_page.dart';
import 'package:test_sa/views/pages/user/profile_page.dart';
import 'package:test_sa/views/pages/user/requests/create_service_request_page.dart';
import 'package:test_sa/views/update_available_screen.dart';
import 'package:test_sa/views/widgets/departments/single_department_picker.dart';
import 'package:test_sa/views/widgets/equipment/asset_detail_page.dart';
@ -353,6 +356,11 @@ class MyApp extends StatelessWidget {
// CLEANER MODULE PROVIDERS (1)
ChangeNotifierProvider(create: (_) => CleanerProvider(), lazy: true),
// EXPORT MODULE PROVIDERS (3)
ChangeNotifierProvider(create: (_) => AssetColumnsProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => ExportFormatProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => ExportApiProvider(), lazy: true),
// UTILITY PROVIDERS (1)
ChangeNotifierProvider(create: (_) => OracleCodeProvider(), lazy: true),
],

@ -81,10 +81,12 @@ class DeviceSearch {
Lookup? assetNotScraped; // Now nullable
String? assetNo; // Now nullable
String? modelDefinition; // Now nullable
String? site; // Now nullable
List<String>? sites; // Changed to List for multiple selection
String? site; // Keep for backward compatibility
String? manufacturer; // Now nullable
String? model; // Now nullable
String? department; // Now nullable
List<String>? departments; // Changed to List for multiple selection
String? department; // Keep for backward compatibility
String? supplier; // Now nullable
String? tagCode; // Now nullable
String? assetGroupName; // Now nullable
@ -181,10 +183,18 @@ class DeviceSearch {
}
if (assetNo != null && assetNo!.isNotEmpty) map['assetNo'] = assetNo;
if (modelDefinition != null && modelDefinition!.isNotEmpty) map['modelDefinition'] = modelDefinition;
if (site != null && site!.isNotEmpty) map['site'] = site;
if (sites != null && sites!.isNotEmpty) {
map['site'] = sites;
} else if (site != null && site!.isNotEmpty) {
map['site'] = site;
}
if (manufacturer != null && manufacturer!.isNotEmpty) map['manufacturer'] = manufacturer;
if (model != null && model!.isNotEmpty) map['model'] = model;
if (department != null && department!.isNotEmpty) map['department'] = department;
if (departments != null && departments!.isNotEmpty) {
map['department'] = departments;
} else if (department != null && department!.isNotEmpty) {
map['department'] = department;
}
if (supplier != null && supplier!.isNotEmpty) map['supplier'] = supplier;
if (tagCode != null && tagCode!.isNotEmpty) map['tagCode'] = tagCode;
if (assetGroupName != null && assetGroupName!.isNotEmpty) map['assetGroupName'] = assetGroupName;

@ -75,6 +75,75 @@ class SearchAllRequestsModel {
isArchived = false;
}
Map<String, dynamic> toJson({
List<int>? typeTransaction,
List<int>? statusTransaction,
List<int>? priority,
List<int>? displayData,
}) {
Map<String, dynamic> json = {};
// Add computed fields if provided
if (typeTransaction != null) {
json["typeTransaction"] = typeTransaction;
} else if (this.typeTransaction != null && this.typeTransaction!.isNotEmpty) {
json["typeTransaction"] = this.typeTransaction;
}
if (statusTransaction != null) {
json["statusTransaction"] = statusTransaction;
} else if (statuses != null && statuses!.isNotEmpty) {
json["statusTransaction"] = statuses;
}
if (priority != null) {
json["priority"] = priority;
} else {
json["priority"] = [];
}
if (displayData != null) {
json["displayData"] = displayData;
} else {
json["displayData"] = [];
}
// Add search fields
if (requestNumber?.value?.isNotEmpty ?? false) {
json["requestNumber"] = requestNumber!.value;
}
if (assetName?.value?.isNotEmpty ?? false) {
json["assetName"] = assetName!.value;
}
if (assetNo?.value?.isNotEmpty ?? false) {
json["assetNumber"] = assetNo!.value;
}
if (sn?.value?.isNotEmpty ?? false) {
json["assetSerialNo"] = sn!.value;
}
if (model?.value?.isNotEmpty ?? false) {
json["model"] = model!.value;
}
if (manufacture?.value?.isNotEmpty ?? false) {
json["manufacturer"] = manufacture!.value;
}
if (startDate != null) {
json["from"] = startDate!.toIso8601String();
}
if (endDate != null) {
json["to"] = endDate!.toIso8601String();
}
return json;
}
}
abstract class SearchAllRequestsBaseModel {

@ -0,0 +1,35 @@
import 'package:test_sa/models/base.dart';
class AssetColumnMetadata extends Base {
String? propertyName;
String? displayName;
AssetColumnMetadata({
this.propertyName,
this.displayName,
}) : super(identifier: propertyName, name: displayName);
factory AssetColumnMetadata.fromJson(Map<String, dynamic> json) {
return AssetColumnMetadata(
propertyName: json['propertyName'],
displayName: json['displayName'],
);
}
Map<String, dynamic> toJson() {
return {
'propertyName': propertyName,
'displayName': displayName,
};
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AssetColumnMetadata &&
runtimeType == other.runtimeType &&
propertyName == other.propertyName;
@override
int get hashCode => propertyName.hashCode;
}

@ -0,0 +1,15 @@
class ExportRequest<T> {
final List<String> selectedColumns;
final String format;
final T searchModel;
final int pageNo;
final int pageSize;
ExportRequest({
required this.selectedColumns,
required this.format,
required this.searchModel,
required this.pageNo,
required this.pageSize,
});
}

@ -0,0 +1,18 @@
/// Enum to identify different module types for export functionality
enum ExportModuleType {
asset(1, 'Asset'),
callRequest(2, 'AllWorkOrder');
final int id;
final String displayName;
const ExportModuleType(this.id, this.displayName);
/// Get module type from id
static ExportModuleType fromId(int id) {
return ExportModuleType.values.firstWhere(
(type) => type.id == id,
orElse: () => ExportModuleType.asset,
);
}
}

@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.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/lookup.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/export_module/models/asset_column_metadata.dart';
import 'package:test_sa/modules/export_module/models/export_form_model.dart';
import 'package:test_sa/modules/export_module/models/export_module_type.dart';
import 'package:test_sa/modules/export_module/provider/export_provider.dart';
import 'package:test_sa/modules/export_module/provider/export_columns_provider.dart';
import 'package:test_sa/modules/export_module/provider/export_format_provider.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/common_widgets/multiple_item_drop_down_menu.dart';
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
class ExportFormView<T> extends StatefulWidget {
final T searchModel;
final ExportModuleType moduleType;
final int pageNo;
final int pageSize;
const ExportFormView({
Key? key,
required this.searchModel,
required this.moduleType,
required this.pageNo,
required this.pageSize,
}) : super(key: key);
@override
State<ExportFormView<T>> createState() => _ExportFormViewState<T>();
}
class _ExportFormViewState<T> extends State<ExportFormView<T>> {
List<AssetColumnMetadata> selectedColumns = [];
Lookup? selectedFormat;
void _updateSelectedColumns(List<AssetColumnMetadata>? value) {
if (value != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
selectedColumns = value;
});
}
});
}
}
void _updateSelectedFormat(Lookup? value) {
if (value != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
selectedFormat = value;
});
}
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: DefaultAppBar(
title: 'Export',
titleStyle: AppTextStyles.heading3.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).brightness == Brightness.dark ? AppColor.neutral30 : AppColor.neutral50,
),
),
body: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
MultipleItemDropDownMenu<AssetColumnMetadata, AssetColumnsProvider>(
context: context,
title: 'Columns',
showAsBottomSheet: true,
backgroundColor: AppColor.fieldBgColor(context),
showShadow: false,
height: 56.toScreenHeight,
showCancel: true,
requestById: widget.moduleType.id,
onSelect: _updateSelectedColumns,
),
18.height,
SingleItemDropDownMenu<Lookup, ExportFormatProvider>(
context: context,
title: 'Format',
backgroundColor: AppColor.fieldBgColor(context),
height: 56.toScreenHeight,
showShadow: false,
showAsBottomSheet: false,
showAsFullScreenDialog: true,
requestById: 1,
onSelect: _updateSelectedFormat,
),
],
).toShadowContainer(context).paddingOnly(top: 20, start: 16, end: 16),
),
),
18.height,
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: 'Export',
buttonColor: AppColor.primary10,
onPressed: () async {
if (selectedColumns.isEmpty) {
'Please select at least one column to export.'.showToast;
return;
}
if (selectedFormat == null) {
'Please select an export format.'.showToast;
return;
}
final exportRequest = ExportRequest<T>(
selectedColumns: selectedColumns.map((col) => col.propertyName ?? '').toList(),
format: selectedFormat?.name ?? '',
searchModel: widget.searchModel,
pageNo: widget.pageNo,
pageSize: widget.pageSize,
);
final exportProvider = Provider.of<ExportApiProvider>(context, listen: false);
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
final success = await exportProvider.exportData(exportRequest, widget.moduleType);
Navigator.pop(context);
// this is for this This should be handled as per requirement in dialog with share and other options
if (success) {
'Export completed successfully!'.showToast;
} else {
String errorMessage = exportProvider.errorMessage ?? 'Export failed. Please try again.';
if (errorMessage.contains('No app installed')) {
String fileType = selectedFormat?.name ?? 'this file type';
errorMessage = 'No app found to open $fileType files. Please install a compatible app';
}
errorMessage.showToast;
}
}),
),
],
),
);
}
//check if we need we can show this as well @waseem
String _getAppSuggestion(String format) {
final formatLower = format.toLowerCase();
switch (formatLower) {
case 'excel':
return 'Microsoft Excel, Google Sheets, or WPS Office';
case 'pdf':
return 'Adobe Acrobat Reader or any PDF viewer';
case 'word':
case 'doc':
return 'Microsoft Word, Google Docs, or WPS Office';
case 'text':
case 'txt':
return 'any text editor';
case 'csv':
return 'Microsoft Excel, Google Sheets, or any spreadsheet app';
default:
return 'a compatible viewer app';
}
}
}

@ -0,0 +1,54 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.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/modules/export_module/models/asset_column_metadata.dart';
import 'package:test_sa/modules/export_module/models/export_module_type.dart';
import 'package:test_sa/providers/loading_list_notifier.dart';
class AssetColumnsProvider extends LoadingListNotifier<AssetColumnMetadata> {
@override
Future<void> getData({int? id}) async {
loading = true;
notifyListeners();
try {
final moduleType = id != null ? ExportModuleType.fromId(id) : ExportModuleType.asset;
String apiUrl;
switch (moduleType) {
case ExportModuleType.asset:
apiUrl = URLs.getAssetColumnsMetadata;
break;
case ExportModuleType.callRequest:
apiUrl = URLs.getAllWorkOrderColumnsMetadata;
break;
}
Response response = await ApiManager.instance.get(apiUrl);
stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
final responseData = json.decode(response.body);
final List<dynamic> data = responseData['data'] ?? [];
log('Raw data received: ${data.length} items for ${moduleType.displayName}');
items = data.map((item) => AssetColumnMetadata.fromJson(item)).toList();
log('First item: ${items.isNotEmpty ? items.first.displayName : "empty"}');
} else {
log('Failed with status code: ${response.statusCode}');
items = [];
}
} catch (error) {
debugPrint('Error fetching columns: $error');
stateCode = -1;
items = [];
} finally {
loading = false;
log('Loading finished. Items count: ${items.length}, Loading: $loading, StateCode: $stateCode');
notifyListeners();
}
}
}

@ -0,0 +1,35 @@
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';
import 'package:test_sa/providers/loading_list_notifier.dart';
import 'dart:convert';
import 'package:http/http.dart';
import 'package:test_sa/models/lookup.dart';
class ExportFormatProvider extends LoadingListNotifier<Lookup> {
@override
Future getData({int? id}) async {
if (loading ?? false) return -2;
loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.get(URLs.exportFormatTypeLookup);
} catch (error) {
loading = false;
stateCode = -1;
notifyListeners();
return -1;
}
stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
List listJson = json.decode(response.body)["data"];
items = listJson.map((department) => Lookup.fromJson(department)).toList();
}
loading = false;
notifyListeners();
return response.statusCode;
}
}

@ -0,0 +1,158 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/modules/export_module/models/export_form_model.dart';
import 'package:test_sa/modules/export_module/models/export_module_type.dart';
class ExportApiProvider extends ChangeNotifier {
bool _isExporting = false;
bool get isExporting => _isExporting;
int? _stateCode;
int? get stateCode => _stateCode;
String? _errorMessage;
String? get errorMessage => _errorMessage;
Future<bool> exportData<T>(ExportRequest<T> exportRequest, ExportModuleType moduleType) async {
_isExporting = true;
_errorMessage = null;
notifyListeners();
try {
final String apiUrl = _getApiUrlForModule(moduleType);
final Map<String, dynamic> body = _buildRequestBody(exportRequest, moduleType);
Response response = await ApiManager.instance.post(apiUrl, body: body);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
String ext = _getFileExtension(exportRequest.format);
String encodedStr = base64.encode(response.bodyBytes);
String filePath = await _createFileFromString(encodedStr, ext);
await OpenFile.open(filePath);
return true;
} else {
try {
final responseData = json.decode(response.body);
_errorMessage = responseData['message'] ?? 'Export failed';
} catch (e) {
_errorMessage = 'Export failed with status: ${response.statusCode}';
}
return false;
}
} catch (error, stackTrace) {
log('Error exporting data: $error');
_stateCode = -1;
_errorMessage = 'An error occurred during export: $error';
return false;
} finally {
_isExporting = false;
notifyListeners();
}
}
String _getApiUrlForModule(ExportModuleType moduleType) {
switch (moduleType) {
case ExportModuleType.asset:
return URLs.exportAsset;
case ExportModuleType.callRequest:
return URLs.exportAllWorkOrder;
default:
return URLs.exportAsset;
}
}
String _getFileExtension(String format) {
final formatLower = format.toLowerCase();
switch (formatLower) {
case 'excel':
return 'xlsx';
case 'pdf':
return 'pdf';
case 'word':
case 'doc':
return 'docx';
case 'text':
case 'txt':
return 'txt';
case 'csv':
return 'csv';
default:
return formatLower;
}
}
Map<String, dynamic> _buildRequestBody<T>(ExportRequest<T> exportRequest, ExportModuleType moduleType) {
final Map<String, dynamic> body = {};
if (exportRequest.searchModel != null) {
try {
final searchModelJson = (exportRequest.searchModel as dynamic).toJson() as Map<String, dynamic>;
body.addAll(searchModelJson);
} catch (e) {
body['searchModel'] = exportRequest.searchModel;
}
}
// Add CallRequest-specific fields only for callRequest type
if (moduleType == ExportModuleType.callRequest) {
_addCallRequestFields(body);
}
body['pageNo'] = exportRequest.pageNo;
body['pageSize'] = exportRequest.pageSize;
body['exportRequest'] = {
'selectedColumns': exportRequest.selectedColumns,
'format': exportRequest.format,
};
return body;
}
void _addCallRequestFields(Map<String, dynamic> body) {
//This is if no filter is selected we will send all the options to the backend to filter accordingly as per we are sending on allworkorder api
if (!body.containsKey('typeTransaction') || (body['typeTransaction'] as List?)?.isEmpty == true) {
body['typeTransaction'] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
}
if (!body.containsKey('statusTransaction') || (body['statusTransaction'] as List?)?.isEmpty == true) {
body['statusTransaction'] = [1, 2, 4];
}
if (!body.containsKey('priority')) {
body['priority'] = [];
}
if (!body.containsKey('displayData')) {
body['displayData'] = [];
}
}
Future<String> _createFileFromString(String encodedStr, String ext) async {
Uint8List bytes = base64.decode(encodedStr);
String dir = (await getApplicationDocumentsDirectory()).path;
File file = File("$dir/${DateTime.now().millisecondsSinceEpoch}.$ext");
await file.writeAsBytes(bytes);
return file.path;
}
void resetError() {
_errorMessage = null;
notifyListeners();
}
}

@ -0,0 +1,412 @@
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';
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_text_form_field.dart';
import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
class CleanerChecklistWidget extends StatefulWidget {
final List<ChecklistItem> checklistItems;
final Function(List<ChecklistItem>)? onChecklistChanged;
final String? capturedImageBase64;
final Function(String?)? onImageCaptured;
bool isClosed;
CleanerChecklistWidget({Key? key, required this.checklistItems, this.onChecklistChanged, this.capturedImageBase64, this.onImageCaptured, this.isClosed = false}) : super(key: key);
@override
State<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState();
}
class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
@override
Widget build(BuildContext context) {
// Only show if checklist items are not empty
if (widget.checklistItems.isEmpty) {
return const SizedBox();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
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,
_buildImageCaptureWidget(context),
],
);
}
Widget _buildChecklistItem(BuildContext context, ChecklistItem item) {
return IgnorePointer(
ignoring: widget.isClosed,
child: Column(
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,
value: item.isChecked,
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(
text: TextSpan(
children: [
TextSpan(
text: item.title,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
fontWeight: FontWeight.w500,
),
),
if (item.isMandatory)
TextSpan(
text: ' *',
style: AppTextStyles.bodyText.copyWith(
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
),
// Comment box - only visible when checked
if (item.isChecked) ...[
16.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,
),
alignLabelWithHint: true,
textInputType: TextInputType.multiline,
onChange: (value) {
item.comment = value;
// Notify parent of changes
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
],
],
).toShadowContainer(context).paddingOnly(bottom: 16),
);
}
Widget _buildImageCaptureWidget(BuildContext context) {
// Base64 image display and capture widget
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Captured Image'.addTranslation,
style: AppTextStyles.heading6.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
8.height,
_buildImageWidget(context),
],
).toShadowContainer(context);
}
/// 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";
// Update parent with captured image
widget.onImageCaptured?.call(formattedImage);
}
});
}
Widget imageWidget;
final imageString = widget.capturedImageBase64!;
// 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);
}
}
// 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);
}
}
// 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(),
],
);
}
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,
),
);
}
/// 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(':\\'));
}
@override
void dispose() {
// 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: const Icon(Icons.delete_rounded, color: Color(0xffF63939)))
.onPress(() {
widget.onImageCaptured?.call(null);
});
}
}
/// Model class for checklist items
class ChecklistItem {
final int id;
final String title;
bool isChecked;
String comment;
final bool isMandatory;
final TextEditingController commentController;
ChecklistItem({
required this.id,
required this.title,
required this.isChecked,
required this.comment,
this.isMandatory = false,
}) : commentController = TextEditingController(text: comment) {
// Update the controller text whenever comment changes
commentController.text = comment;
}
// Convert to JSON for API submission
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'isChecked': isChecked,
'comment': commentController.text,
'isMandatory': isMandatory,
};
}
void dispose() {
commentController.dispose();
}
}

@ -9,7 +9,7 @@ import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/new_models/site.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/single_item_drop_down_menu.dart';
import 'package:test_sa/new_views/common_widgets/multiple_item_drop_down_menu.dart';
import 'package:test_sa/providers/department_provider.dart';
import 'package:test_sa/providers/gas_request_providers/site_provider.dart';
import 'package:test_sa/views/widgets/date_and_time/date_picker.dart';
@ -32,8 +32,8 @@ class DeviceFilterScreen extends StatefulWidget {
class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
late DeviceSearch filter;
Lookup? _commissioningStatus;
Site? _site;
Lookup? _department;
List<Site> _selectedSites = [];
List<Lookup> _selectedDepartments = [];
String? startDate, endDate;
bool loading = false;
@ -49,8 +49,17 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
final siteProvider = Provider.of<SiteProvider>(context, listen: false);
if (departmentProvider.items.isEmpty ?? true) await departmentProvider.getData();
if (siteProvider.items.isEmpty ?? true) await siteProvider.getData();
_department = departmentProvider.items.where((element) => element.name == filter.department).firstOrNull;
_site = siteProvider.items.where((element) => element.name == filter.site).firstOrNull;
// Initialize selected departments from filter
if (filter.departments != null && filter.departments!.isNotEmpty) {
_selectedDepartments = departmentProvider.items.where((dept) => filter.departments!.contains(dept.name)).toList();
}
// Initialize selected sites from filter
if (filter.sites != null && filter.sites!.isNotEmpty) {
_selectedSites = siteProvider.items.where((site) => filter.sites!.contains(site.name)).toList();
}
loading = false;
setState(() {});
});
@ -59,107 +68,122 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: DefaultAppBar(
title: context.translation.filter,
actions: [
if (loading == false && _showResetButton())
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
context.translation.reset,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.primary50),
).paddingAll(8).onPress(() {
setState(() {
startDate = null;
endDate = null;
_department = null;
_site = null;
});
}),
],
)
],
),
body: Column(
children: [
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CommissioningStatusButtons(
initialValue: _commissioningStatus,
onSelect: (value) {
if (value != null) {
_commissioningStatus = value;
}
},
),
16.height,
SingleItemDropDownMenu<Lookup, DepartmentProvider>(
context: context,
title: context.translation.byDepartment,
initialValue: _department,
loading: loading == false ? null : loading,
onSelect: (value) {
return SafeArea(
top: false,
child: Scaffold(
appBar: DefaultAppBar(
title: context.translation.filter,
actions: [
if (loading == false && _showResetButton())
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
context.translation.reset,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.primary50),
).paddingAll(8).onPress(() {
setState(() {
_department = value;
startDate = null;
endDate = null;
_selectedDepartments = [];
_selectedSites = [];
});
},
).paddingOnly(start: 16, end: 16),
8.height,
SingleItemDropDownMenu<Site, SiteProvider>(
context: context,
title: context.translation.bySite,
initialValue: _site,
loading: loading == false ? null : loading,
onSelect: (value) {
setState(() {
_site = value;
});
},
).paddingOnly(start: 16, end: 16),
16.height,
context.translation.pmDateRange.heading5(context).paddingOnly(start: 16, end: 16),
8.height,
Row(
children: [
ADatePicker(
label: context.translation.to,
from: DateTime(2000),
date: DateTime.tryParse(startDate ?? ""),
onDatePicker: (date) {
}),
],
)
],
),
body: Column(
children: [
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CommissioningStatusButtons(
initialValue: _commissioningStatus,
onSelect: (value) {
if (value != null) {
_commissioningStatus = value;
}
},
),
16.height,
MultipleItemDropDownMenu<Lookup, DepartmentProvider>(
context: context,
title: context.translation.byDepartment,
showAsBottomSheet: true,
backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white,
// backgroundColor: AppColor.fieldBgColor(context),
showShadow: true,
height: 56.toScreenHeight,
showCancel: true,
onSelect: (value) {
if (value != null) {
setState(() {
startDate = date.toIso8601String();
_selectedDepartments = value;
});
},
).expanded,
8.width,
ADatePicker(
label: context.translation.to,
from: DateTime(2000),
date: DateTime.tryParse(endDate ?? ""),
onDatePicker: (date) {
}
},
).paddingOnly(start: 16, end: 16),
8.height,
MultipleItemDropDownMenu<Site, SiteProvider>(
context: context,
title: context.translation.bySite,
showAsBottomSheet: true,
// backgroundColor: AppColor.fieldBgColor(context),
backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white,
showShadow: true,
height: 56.toScreenHeight,
showCancel: true,
onSelect: (value) {
if (value != null) {
setState(() {
endDate = date.toIso8601String();
_selectedSites = value;
});
},
).expanded,
],
).paddingOnly(start: 16, end: 16),
],
),
).expanded,
AppFilledButton(onPressed: _search, label: context.translation.applyFilter).paddingOnly(start: 16, end: 16),
],
).paddingOnly(top: 16, bottom: 16),
}
},
).paddingOnly(start: 16, end: 16),
16.height,
context.translation.pmDateRange.heading5(context).paddingOnly(start: 16, end: 16),
8.height,
Row(
children: [
ADatePicker(
label: context.translation.to,
from: DateTime(2000),
date: DateTime.tryParse(startDate ?? ""),
onDatePicker: (date) {
setState(() {
startDate = date.toIso8601String();
});
},
).expanded,
8.width,
ADatePicker(
label: context.translation.to,
from: DateTime(2000),
date: DateTime.tryParse(endDate ?? ""),
onDatePicker: (date) {
setState(() {
endDate = date.toIso8601String();
});
},
).expanded,
],
).paddingOnly(start: 16, end: 16),
],
),
).expanded,
AppFilledButton(onPressed: _search, label: context.translation.applyFilter).paddingOnly(start: 16, end: 16),
],
).paddingOnly(top: 16, bottom: 16),
),
);
}
void _search() async {
filter.site = _site?.name;
filter.department = _department?.name;
filter.sites = _selectedSites.map((site) => site.name ?? '').where((name) => name.isNotEmpty).toList();
filter.departments = _selectedDepartments.map((dept) => dept.name ?? '').where((name) => name.isNotEmpty).toList();
filter.supplyDateFrom = startDate;
filter.supplyDateTo = endDate;
final deviceProvider = Provider.of<AssetProvider>(context, listen: false);
@ -171,6 +195,6 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
}
bool _showResetButton() {
return (_department != null) || (_site != null) || (startDate != null) || (endDate != null);
return (_selectedDepartments.isNotEmpty) || (_selectedSites.isNotEmpty) || (startDate != null) || (endDate != null);
}
}

@ -15,6 +15,8 @@ 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/search_all_requests_model.dart';
import 'package:test_sa/modules/export_module/models/export_module_type.dart';
import 'package:test_sa/modules/export_module/pages/export_form_view.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/pages/land_page/my_request/all_requests_filter_page.dart';
import 'package:test_sa/new_views/pages/land_page/my_request/all_requests_search_page.dart';
@ -147,6 +149,24 @@ class _MyRequestsPageState extends State<MyRequestsPage> {
}
}).expanded,
16.width,
Text(
'Export',
style: AppTextStyles.bodyText2.copyWith(color: AppColor.blueStatus(context)),
).onPress(() async {
final provider = Provider.of<AllRequestsProvider>(context, listen: false);
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExportFormView(
searchModel: _search,
moduleType: ExportModuleType.callRequest,
pageNo: provider.pageItemNumber,
pageSize: (provider.allRequestsAndCount?.requestsDetails?.length ?? 0) ~/ (provider.pageItemNumber > 0 ? provider.pageItemNumber : 1) + 1,
),
),
);
}),
16.width,
Text(
context.translation.filter,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.blueStatus(context)),

@ -9,6 +9,8 @@ import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/tm_module/device_transfer/device_filter_screen.dart';
import 'package:test_sa/modules/tm_module/device_transfer/device_search_screen.dart';
import 'package:test_sa/modules/export_module/pages/export_form_view.dart';
import 'package:test_sa/modules/export_module/models/export_module_type.dart';
import 'package:test_sa/views/widgets/equipment/asset_detail_page.dart';
import 'package:test_sa/views/widgets/equipment/asset_item_gridview.dart';
import 'package:test_sa/views/widgets/equipment/asset_item_listview.dart';
@ -20,6 +22,7 @@ import 'package:test_sa/views/widgets/qr/scan_qr.dart';
import '../../../models/device/asset.dart';
import '../../../models/device/asset_search.dart';
import '../../../new_views/app_style/app_color.dart';
import '../../../new_views/app_style/app_text_style.dart';
class MyAssetsPage extends StatefulWidget {
static const String id = "/single-device-Picker";
@ -136,6 +139,24 @@ class _MyAssetsPageState extends State<MyAssetsPage> {
_searchableList.clear();
_searchableList.addAll(_devicesProvider.devices);
}),
16.width,
Text(
'Export',
style: AppTextStyles.bodyText2.copyWith(color: AppColor.blueStatus(context)),
).onPress(() async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExportFormView(
searchModel: _searchAsset,
moduleType: ExportModuleType.asset,
pageNo: _devicesProvider.pageItemNumber,
pageSize:_devicesProvider.devices.length ~/ _devicesProvider.pageItemNumber + 1 ,
),
),
);
}),
16.width,
],
).paddingOnly(start: 16, end: 16),
),

@ -109,6 +109,7 @@ dependencies:
local_auth_darwin: ^1.4.0
haptic_feedback: ^0.6.4+3
dev_dependencies:
flutter_test:
sdk: flutter

Loading…
Cancel
Save