diff --git a/assets/images/asset_image_upload/side_bottom.svg b/assets/images/asset_image_upload/side_bottom.svg new file mode 100644 index 00000000..7d9ec605 --- /dev/null +++ b/assets/images/asset_image_upload/side_bottom.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/images/asset_image_upload/side_left.svg b/assets/images/asset_image_upload/side_left.svg new file mode 100644 index 00000000..bdf7e6d9 --- /dev/null +++ b/assets/images/asset_image_upload/side_left.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/images/asset_image_upload/side_right.svg b/assets/images/asset_image_upload/side_right.svg new file mode 100644 index 00000000..f8db13a1 --- /dev/null +++ b/assets/images/asset_image_upload/side_right.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/images/asset_image_upload/side_top.svg b/assets/images/asset_image_upload/side_top.svg new file mode 100644 index 00000000..52841a4e --- /dev/null +++ b/assets/images/asset_image_upload/side_top.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 84b68233..e0820b57 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -3,13 +3,13 @@ class URLs { static const String appReleaseBuildNumber = "44"; - 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/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/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 // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module diff --git a/lib/controllers/providers/api/devices_provider.dart b/lib/controllers/providers/api/devices_provider.dart index f933a5a9..d3e841ff 100644 --- a/lib/controllers/providers/api/devices_provider.dart +++ b/lib/controllers/providers/api/devices_provider.dart @@ -232,11 +232,21 @@ class AssetProvider extends ChangeNotifier { } } - Future uploadAssetImages(int assetId, List attachments) async { + Future uploadAssetImages(int assetId, TrainingImageData trainingImageData) async { late Response response; try { - response = - await ApiManager.instance.multiPart(URLs.saveAssetImages + "?AssetId=$assetId", body: {}, files: attachments.map((attachment) => MultipartFile.fromPath("Files", attachment.name!)).toList()); + List> 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 assetData = json.decode(response.body)["data"]; diff --git a/lib/models/device/asset.dart b/lib/models/device/asset.dart index 1e41dc31..91c86dc9 100644 --- a/lib/models/device/asset.dart +++ b/lib/models/device/asset.dart @@ -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 toJson() { @@ -381,6 +386,7 @@ class Asset { map['comment'] = comment; map['tagCode'] = tagCode; map['assetPhoto'] = assetPhoto; + map['hasTrainingImageData'] = hasTrainingImageData; return map; } } diff --git a/lib/models/device/asset_by_id_model.dart b/lib/models/device/asset_by_id_model.dart index b4ac22ef..ab177b23 100644 --- a/lib/models/device/asset_by_id_model.dart +++ b/lib/models/device/asset_by_id_model.dart @@ -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 { @@ -63,6 +64,8 @@ class AssetByIdModel { String? modelPhoto; String? nextPMDate; String? lastPMDate; + bool? hasTrainingImageData; + TrainingImageData? trainingImageData; AssetByIdModel( {this.multiAssets, @@ -125,9 +128,13 @@ class AssetByIdModel { this.assetPhoto, this.modelPhoto, this.nextPMDate, - this.lastPMDate}); + this.lastPMDate, + this.trainingImageData, + this.hasTrainingImageData}); AssetByIdModel.fromJson(Map json) { + print("assetModel!.fromJson:${json['hasTrainingImageData']}"); + if (json['multiAssets'] != null) { multiAssets = []; json['multiAssets'].forEach((v) { @@ -204,6 +211,8 @@ class AssetByIdModel { modelPhoto = json['modelPhoto']; nextPMDate = json['nextPMDate']; lastPMDate = json['lastPMDate']; + hasTrainingImageData = json['hasTrainingImageData']; + trainingImageData = json['trainingImageData'] != null ? TrainingImageData.fromJson(json['trainingImageData']) : null; } Map toJson() { @@ -301,6 +310,8 @@ class AssetByIdModel { data['modelPhoto'] = modelPhoto; data['nextPMDate'] = nextPMDate; data['lastPMDate'] = lastPMDate; + data['hasTrainingImageData'] = hasTrainingImageData; + data['trainingImageData'] = trainingImageData?.toJson(); return data; } } @@ -789,3 +800,37 @@ class DemoRequest { return data; } } + +class TrainingImageData { + GenericAttachmentModel? leftImage; + GenericAttachmentModel? rightImage; + GenericAttachmentModel? upImage; + GenericAttachmentModel? downImage; + List? additionalImages; + + TrainingImageData({this.leftImage, this.rightImage, this.upImage, this.downImage, this.additionalImages = const []}); + + TrainingImageData.fromJson(Map 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 toJson() { + final Map data = {}; + 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; + } +} diff --git a/lib/models/generic_attachment_model.dart b/lib/models/generic_attachment_model.dart index dde1d74d..6ba126e1 100644 --- a/lib/models/generic_attachment_model.dart +++ b/lib/models/generic_attachment_model.dart @@ -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 json) { + GenericAttachmentModel.fromJson(Map 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 toJson() { diff --git a/lib/models/new_models/work_order_detail_model.dart b/lib/models/new_models/work_order_detail_model.dart index 1936113a..984edca5 100644 --- a/lib/models/new_models/work_order_detail_model.dart +++ b/lib/models/new_models/work_order_detail_model.dart @@ -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 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, }; } diff --git a/lib/modules/cm_module/views/components/service_request_detail_view.dart b/lib/modules/cm_module/views/components/service_request_detail_view.dart index 8bc088b8..6ebd657c 100644 --- a/lib/modules/cm_module/views/components/service_request_detail_view.dart +++ b/lib/modules/cm_module/views/components/service_request_detail_view.dart @@ -206,6 +206,13 @@ class _ServiceRequestDetailViewState extends State { ], ), 8.height, + if (context.userProvider.isEngineer && !(workOrder.asset!.hasTrainingImageData ?? true)) ...[ + Text( + '*Images are required for the asset', + style: AppTextStyles.bodyText.copyWith(color: AppColor.red30), + ), + 4.height, + ], Text( '${context.translation.assetName}: ${workOrder.assetNdModel!.name?.cleanupWhitespace?.capitalizeFirstOfEach}', style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), diff --git a/lib/modules/tm_module/device_transfer/search_device_page.dart b/lib/modules/tm_module/device_transfer/search_device_page.dart index cc76e370..93a6f523 100644 --- a/lib/modules/tm_module/device_transfer/search_device_page.dart +++ b/lib/modules/tm_module/device_transfer/search_device_page.dart @@ -171,68 +171,67 @@ class _SearchDevicePageState extends State { ).expanded, if (widget.multiSelection && selectedAssets.isNotEmpty) FooterActionButton.footerContainer( - context: context, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: AppFilledButton( - label: "Clear", - buttonColor: AppColor.white60, - textColor: AppColor.black10, - loading: false, - onPressed: () async { - selectedAssets.clear(); - setState(() {}); - }, + context: context, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: AppFilledButton( + label: "Clear", + buttonColor: AppColor.white60, + textColor: AppColor.black10, + loading: false, + onPressed: () async { + selectedAssets.clear(); + setState(() {}); + }, + ), ), - ), - 12.width, - Expanded( - child: AppFilledButton( - label: context.translation.select, - buttonColor: AppColor.primary10, - loading: false, - onPressed: () async { - Navigator.of(context).pop(); - List assets = _searchableList.where((asset) => selectedAssets.contains(asset.id)).toList(); - Navigator.of(context).pop(assets); - }, + 12.width, + Expanded( + child: AppFilledButton( + label: context.translation.select, + buttonColor: AppColor.primary10, + loading: false, + onPressed: () async { + Navigator.of(context).pop(); + List assets = _searchableList.where((asset) => selectedAssets.contains(asset.id)).toList(); + Navigator.of(context).pop(assets); + }, + ), ), - ), - ], - ) - ), - // Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // Expanded( - // child: AppFilledButton( - // label: "Clear", - // buttonColor: AppColor.white60, - // textColor: AppColor.black10, - // loading: false, - // onPressed: () async { - // selectedAssets.clear(); - // setState(() {}); - // }, - // ), - // ), - // 12.width, - // Expanded( - // child: AppFilledButton( - // label: context.translation.select, - // buttonColor: AppColor.primary10, - // loading: false, - // onPressed: () async { - // Navigator.of(context).pop(); - // List assets = _searchableList.where((asset) => selectedAssets.contains(asset.id)).toList(); - // Navigator.of(context).pop(assets); - // }, - // ), - // ), - // ], - // ).toShadowContainer(context), + ], + )), + // Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Expanded( + // child: AppFilledButton( + // label: "Clear", + // buttonColor: AppColor.white60, + // textColor: AppColor.black10, + // loading: false, + // onPressed: () async { + // selectedAssets.clear(); + // setState(() {}); + // }, + // ), + // ), + // 12.width, + // Expanded( + // child: AppFilledButton( + // label: context.translation.select, + // buttonColor: AppColor.primary10, + // loading: false, + // onPressed: () async { + // Navigator.of(context).pop(); + // List assets = _searchableList.where((asset) => selectedAssets.contains(asset.id)).toList(); + // Navigator.of(context).pop(assets); + // }, + // ), + // ), + // ], + // ).toShadowContainer(context), ], ), ), diff --git a/lib/views/widgets/equipment/asset_detail_page.dart b/lib/views/widgets/equipment/asset_detail_page.dart index 151df55e..9c9c0d97 100644 --- a/lib/views/widgets/equipment/asset_detail_page.dart +++ b/lib/views/widgets/equipment/asset_detail_page.dart @@ -117,6 +117,13 @@ class _AssetDetailPageState extends State { if (assetModel?.commissioningStatus != null) 8.height, (assetModel?.modelDefinition?.assetName?.cleanupWhitespace.capitalizeFirstOfEach ?? "-").heading5(context), 8.height, + if (context.userProvider.isEngineer && !(assetModel!.hasTrainingImageData ?? true)) ...[ + Text( + '*Upload Images for this asset', + style: AppTextStyles.bodyText2.copyWith(color: AppColor.red30), + ), + 4.height, + ], Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -164,12 +171,12 @@ class _AssetDetailPageState extends State { ], ).toShadowContainer(context), ]).expanded, - if (context.userProvider.isEngineer) + if (context.userProvider.isEngineer && !(assetModel!.hasTrainingImageData ?? true)) FooterActionButton.footerContainer( context: context, child: AppFilledButton( buttonColor: AppColor.primary10, - label: "Upload Images (AI Training)", + label: "Upload Images", onPressed: _upload, ), ), @@ -183,20 +190,7 @@ class _AssetDetailPageState extends State { } void _upload() async { - bool reload = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AssetImageUploadPage( - assetId: assetId!, - attachments: assetModel?.assetAttachments - ?.map((attachment) => GenericAttachmentModel( - id: attachment.id, - name: attachment.attachmentName, - originalName: attachment.originalName, - )) - .toList() ?? - []))) ?? - false; + bool reload = await Navigator.push(context, MaterialPageRoute(builder: (context) => AssetImageUploadPage(assetId: assetId!))) ?? false; if (reload) { assetModel = null; setState(() {}); diff --git a/lib/views/widgets/equipment/asset_image_upload_page.dart b/lib/views/widgets/equipment/asset_image_upload_page.dart index cd0caae9..65d1efc9 100644 --- a/lib/views/widgets/equipment/asset_image_upload_page.dart +++ b/lib/views/widgets/equipment/asset_image_upload_page.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/api/devices_provider.dart'; @@ -5,7 +8,9 @@ import 'package:test_sa/extensions/context_extension.dart'; 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_by_id_model.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/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; @@ -15,9 +20,9 @@ import 'package:test_sa/views/widgets/images/multi_image_picker.dart'; class AssetImageUploadPage extends StatefulWidget { int assetId; - List attachments; + TrainingImageData? attachments; - AssetImageUploadPage({Key? key, required this.assetId, required this.attachments}) : super(key: key); + AssetImageUploadPage({Key? key, required this.assetId, this.attachments}) : super(key: key); @override _AssetImageUploadPageState createState() { @@ -26,12 +31,12 @@ class AssetImageUploadPage extends StatefulWidget { } class _AssetImageUploadPageState extends State { - List attachments = []; + late TrainingImageData trainingImageData; @override void initState() { super.initState(); - // attachments = widget.attachments; + trainingImageData = widget.attachments ?? TrainingImageData(additionalImages: []); } @override @@ -48,16 +53,68 @@ class _AssetImageUploadPageState extends State { ListView( padding: const EdgeInsets.all(16), children: [ + Text( + "Instructions", + style: AppTextStyles.heading5.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50), + ), + Text( + '* Please upload all images of the sides of the asset as mentioned below.\n* Images should be clear and high quality.\n* Avoid uploading blurry, low-light or low-resolution images.', + style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), + ), + GridView( + padding: const EdgeInsets.only(top: 16, bottom: 16), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: (MediaQuery.of(context).size.width < 550) ? 4 : 4, childAspectRatio: 1, crossAxisSpacing: 12, mainAxisSpacing: 12), + children: [ + gridCard( + icon: "asset_image_upload/side_right", + title: "Right Side", + attachment: trainingImageData.rightImage, + onUpdate: (value) { + trainingImageData.rightImage = value; + setState(() {}); + }, + ), + gridCard( + icon: "asset_image_upload/side_left", + title: "Left Side", + attachment: trainingImageData.leftImage, + onUpdate: (value) { + trainingImageData.leftImage = value; + setState(() {}); + }, + ), + gridCard( + icon: "asset_image_upload/side_top", + title: "Top Side", + attachment: trainingImageData.upImage, + onUpdate: (value) { + trainingImageData.upImage = value; + setState(() {}); + }, + ), + gridCard( + icon: "asset_image_upload/side_bottom", + title: "Bottom Side", + attachment: trainingImageData.downImage, + onUpdate: (value) { + trainingImageData.downImage = value; + setState(() {}); + }, + ), + ], + ), AttachmentPicker( - label: "Import", - attachment: attachments, + label: "Additional Images", + attachment: trainingImageData.additionalImages!, buttonColor: AppColor.black10, onlyImages: true, pickMultiple: true, - showAsListView: true, buttonIcon: 'image-plus'.toSvgAsset(color: context.isDark ? AppColor.primary10 : AppColor.neutral120), onChange: (_attachments) { - attachments = _attachments; + trainingImageData.additionalImages = _attachments; setState(() {}); }, ).toShadowContainer(context), @@ -67,18 +124,19 @@ class _AssetImageUploadPageState extends State { context: context, child: AppFilledButton( buttonColor: AppColor.primary10, - disableButton: attachments.length < 4, - label: "Upload Images", + disableButton: trainingImageData.leftImage == null || trainingImageData.rightImage == null || trainingImageData.upImage == null || trainingImageData.downImage == null, + label: "Upload", onPressed: () async { bool result = await showDialog( context: context, builder: (_) => AAlertDialog( - title: context.translation.confirm, - content: "For more enhancement, these images will be used to train AI model.\n\nAre you sure you are uploading clear and high quality images?"), + title: context.translation.confirm, + content: "I followed all the instructions and uploading the images in high quality, clear and not blurry.", + ), ); if (result) { Utils.showUploadingDialog(context); - result = await Provider.of(context, listen: false).uploadAssetImages(widget.assetId, attachments); + result = await Provider.of(context, listen: false).uploadAssetImages(widget.assetId, trainingImageData); Utils.hideLoading(context); if (result) { Navigator.pop(context, result); @@ -90,4 +148,112 @@ class _AssetImageUploadPageState extends State { ], )); } + + Widget gridCard({ + required String icon, + required String title, + required GenericAttachmentModel? attachment, + required Function(GenericAttachmentModel?) onUpdate, + }) { + return Stack( + alignment: AlignmentDirectional.topEnd, + children: [ + Positioned.fill( + child: AttachmentPicker( + label: "", + onlyImages: true, + checkQuality: true, + enabled: attachment == null, + attachment: attachment == null ? [] : [attachment], + buttonIcon: 'image-plus'.toSvgAsset(color: context.isDark ? AppColor.primary10 : AppColor.neutral120), + onChange: (_attachments) { + if (_attachments.isNotEmpty) { + onUpdate(_attachments.first); + } else { + onUpdate(null); + } + }, + child: imageAngleCard(icon, title, attachment), + ).toShadowContainer(context, padding: 0), + ), + if (attachment != null) + Positioned( + top: 4, + right: 4, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.clear), + iconSize: 18, + style: IconButton.styleFrom(elevation: 2, tapTargetSize: MaterialTapTargetSize.shrinkWrap, backgroundColor: AppColor.red30, foregroundColor: Colors.white), + visualDensity: VisualDensity.compact, + constraints: const BoxConstraints(), + onPressed: () { + onUpdate(null); + }, + )), + ], + ); + } + + Widget imageAngleCard(String icon, String title, GenericAttachmentModel? attachment) { + if (attachment != null) { + Widget imageWidget = _getImageWidget(attachment.name!); + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: imageWidget.onPress(() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => Scaffold( + appBar: const DefaultAppBar(), + body: SafeArea(child: InteractiveViewer(child: imageWidget).center), + ), + ), + ); + })); + } + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + icon.toSvgAsset(height: 50, width: 50), + Text( + title, + style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), + ), + ], + ); + } + + 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); + }, + ); + } } diff --git a/lib/views/widgets/equipment/asset_item_gridview.dart b/lib/views/widgets/equipment/asset_item_gridview.dart index cc463097..61e89ddc 100644 --- a/lib/views/widgets/equipment/asset_item_gridview.dart +++ b/lib/views/widgets/equipment/asset_item_gridview.dart @@ -12,7 +12,7 @@ class AssetItemGridView extends StatelessWidget { final Asset device; final Function(Asset) onPressed; - const AssetItemGridView({Key? key,required this.device,required this.onPressed}) : super(key: key); + const AssetItemGridView({Key? key, required this.device, required this.onPressed}) : super(key: key); @override Widget build(BuildContext context) { @@ -43,6 +43,13 @@ class AssetItemGridView extends StatelessWidget { overflow: TextOverflow.ellipsis, style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50), ), + if (context.userProvider.isEngineer && !(device.hasTrainingImageData ?? true)) ...[ + 2.height, + Text( + '*Upload Images for this asset', + style: AppTextStyles.tinyFont2.copyWith(color: AppColor.red30), + ), + ], Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -71,6 +78,6 @@ class AssetItemGridView extends StatelessWidget { ], ).expanded ], - ).onPress(() => onPressed(device)).toShadowContainer(context,padding: 12); + ).onPress(() => onPressed(device)).toShadowContainer(context, padding: 12); } } diff --git a/lib/views/widgets/equipment/asset_item_listview.dart b/lib/views/widgets/equipment/asset_item_listview.dart index f2aab423..2c8f96c1 100644 --- a/lib/views/widgets/equipment/asset_item_listview.dart +++ b/lib/views/widgets/equipment/asset_item_listview.dart @@ -41,7 +41,14 @@ class AssetItemListView extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ device.modelDefinition!.assetName!.cleanupWhitespace.capitalizeFirstOfEach.heading6(context), - 8.height, + if (context.userProvider.isEngineer && !(device.hasTrainingImageData ?? true)) ...[ + 2.height, + Text( + '*Upload Images for this asset', + style: AppTextStyles.tinyFont2.copyWith(color: AppColor.red30), + ), + ], + 6.height, "${context.translation.assetNumber} : ${device.assetNumber}".bodyText(context), "${context.translation.modelName} : ${device.modelDefinition!.modelName}".cleanupWhitespace.capitalizeFirstOfEach.bodyText(context), ], diff --git a/lib/views/widgets/equipment/single_device_picker.dart b/lib/views/widgets/equipment/single_device_picker.dart index bd709317..7bd79936 100644 --- a/lib/views/widgets/equipment/single_device_picker.dart +++ b/lib/views/widgets/equipment/single_device_picker.dart @@ -119,7 +119,7 @@ class _MyAssetsPageState extends State { }).expanded, 16.width, Icon( - Icons.grid_view_outlined, + showListView ? Icons.view_list_rounded : Icons.grid_view_rounded, color: context.isDark ? AppColor.neutral10 : AppColor.neutral20, ).onPress(() => setState(() { showListView = !showListView; diff --git a/lib/views/widgets/images/multi_image_picker.dart b/lib/views/widgets/images/multi_image_picker.dart index cead4b1a..b50b2b3c 100644 --- a/lib/views/widgets/images/multi_image_picker.dart +++ b/lib/views/widgets/images/multi_image_picker.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:image_blur_detection/image_blur_detection.dart'; import 'package:image_picker/image_picker.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; @@ -324,6 +325,8 @@ class AttachmentPicker extends StatefulWidget { final bool showAsListView; final bool pickMultiple; Lookup? documentType; + Widget? child; + bool checkQuality; AttachmentPicker( {Key? key, @@ -339,6 +342,8 @@ class AttachmentPicker extends StatefulWidget { this.showAsGrid = false, this.showAsListView = false, this.pickMultiple = false, + this.checkQuality = false, + this.child, this.buttonColor}) : super(key: key); @@ -349,72 +354,78 @@ class AttachmentPicker extends StatefulWidget { class _AttachmentPickerState extends State { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, - children: [ - AppDashedButton( - title: widget.label, - height: widget.buttonHeight, - buttonColor: widget.buttonColor, - icon: widget.buttonIcon, - onPressed: (widget.enabled == false) - ? () {} - : widget.showAsGrid - ? showFileSourceSheet - : onFilePicker), - if (widget.attachment.isNotEmpty) - widget.showAsListView - ? ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.only(top: 0), - itemBuilder: (cxt, index) { - File image = File(widget.attachment[index].name!); - return MultiFilesPickerItem( - file: image, - showAsListView: widget.showAsListView, - enabled: widget.enabled, - documentType: widget.attachment[index].documentTypeId?.name, - onRemoveTap: (image) { - if (!widget.enabled) { - return; - } - widget.attachment.removeAt(index); - if (widget.onChange != null) { - widget.onChange!(widget.attachment); - } - setState(() {}); - }, - ); - }, - separatorBuilder: (cxt, index) => 8.height, - itemCount: widget.attachment.length) - : Wrap( - spacing: 8.toScreenWidth, - children: List.generate( - widget.attachment.length, - (index) { - File image = File(widget.attachment[index].name!); - return MultiFilesPickerItem( - file: image, - enabled: widget.enabled, - onRemoveTap: (image) { - if (!widget.enabled) { - return; - } - widget.attachment.removeAt(index); - if (widget.onChange != null) { - widget.onChange!(widget.attachment); - } - setState(() {}); + return widget.child != null + ? widget.child!.onPress((widget.enabled == false) + ? () {} + : widget.showAsGrid + ? () => showFileSourceSheet() + : () => onFilePicker(widget.checkQuality)) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + AppDashedButton( + title: widget.label, + height: widget.buttonHeight, + buttonColor: widget.buttonColor, + icon: widget.buttonIcon, + onPressed: (widget.enabled == false) + ? () {} + : widget.showAsGrid + ? () => showFileSourceSheet() + : () => onFilePicker(widget.checkQuality)), + if (widget.attachment.isNotEmpty) + widget.showAsListView + ? ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 0), + itemBuilder: (cxt, index) { + File image = File(widget.attachment[index].name!); + return MultiFilesPickerItem( + file: image, + showAsListView: widget.showAsListView, + enabled: widget.enabled, + documentType: widget.attachment[index].documentTypeId?.name, + onRemoveTap: (image) { + if (!widget.enabled) { + return; + } + widget.attachment.removeAt(index); + if (widget.onChange != null) { + widget.onChange!(widget.attachment); + } + setState(() {}); + }, + ); }, - ); - }, - ), - ).paddingOnly(top: 0), - ], - ); + separatorBuilder: (cxt, index) => 8.height, + itemCount: widget.attachment.length) + : Wrap( + spacing: 8.toScreenWidth, + children: List.generate( + widget.attachment.length, + (index) { + File image = File(widget.attachment[index].name!); + return MultiFilesPickerItem( + file: image, + enabled: widget.enabled, + onRemoveTap: (image) { + if (!widget.enabled) { + return; + } + widget.attachment.removeAt(index); + if (widget.onChange != null) { + widget.onChange!(widget.attachment); + } + setState(() {}); + }, + ); + }, + ), + ).paddingOnly(top: 0), + ], + ); } fromFilePicker() async { @@ -507,7 +518,7 @@ class _AttachmentPickerState extends State { ); } - onFilePicker() async { + onFilePicker(bool checkQuality) async { ImageSource? source = await showModalBottomSheet( context: context, builder: (BuildContext context) { @@ -658,6 +669,19 @@ class _AttachmentPickerState extends State { if (pickedFile != null) { File fileImage = File(pickedFile.path); + + if (checkQuality) { + ImageQualityValidator imageQualityValidator = ImageQualityValidator(); + final result = await imageQualityValidator.validate(await fileImage.readAsBytes()); + + if (!result.isValid) { + String issues = "Your image have following problems:"; + result.issues.forEach((issue) => (issues += "\n- $issue")); + context.showInfoDialog(issues, title: "Bad Quality Found"); + return; + } + } + widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path, documentTypeId: widget.documentType)); if (widget.onChange != null) { widget.onChange!(widget.attachment); diff --git a/pubspec.yaml b/pubspec.yaml index aa34d07d..3b19daf1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ dependencies: flutter_timezone: ^3.0.1 device_calendar: ^4.3.3 pie_chart: ^5.4.0 + image_blur_detection: ^1.0.1 badges: ^3.1.1 # buttons_tabbar: ^1.1.2 @@ -136,6 +137,7 @@ flutter: - assets/ - assets/audio/ - assets/images/ + - assets/images/asset_image_upload/ - assets/lottie/ - assets/subtitles/ - assets/rives/