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/