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/api_manager.dart b/lib/controllers/api_routes/api_manager.dart
index 722e894e..6142dbc5 100644
--- a/lib/controllers/api_routes/api_manager.dart
+++ b/lib/controllers/api_routes/api_manager.dart
@@ -19,9 +19,9 @@ import 'package:test_sa/new_views/pages/login_page.dart';
class _CachedResponse {
final http.Response response;
final DateTime timestamp;
-
+
_CachedResponse(this.response, this.timestamp);
-
+
bool isValid(Duration cacheDuration) {
return DateTime.now().difference(timestamp) < cacheDuration;
}
@@ -41,26 +41,26 @@ class ApiManager {
User? user;
AssetGroup? assetGroup;
-
+
// OPTIMIZATION: HTTP Connection Pooling - Reuse connections instead of creating new ones
// This improves performance by 100-200ms on subsequent API calls
static final http.Client _httpClient = http.Client();
-
+
// OPTIMIZATION: Response Caching - Cache lookup/static data to reduce network calls
// This reduces API calls by 60% for frequently accessed but rarely changing data
static final Map _cache = {};
static const Duration _defaultCacheDuration = Duration(hours: 1);
-
+
/// Clear all cached responses (call on logout or when needed)
void clearCache() {
_cache.clear();
}
-
+
/// Clear specific cache entry
void clearCacheEntry(String url) {
_cache.remove(_generateCacheKey(url));
}
-
+
/// Generate cache key from URL and user context
String _generateCacheKey(String url) {
// Include user and asset group in cache key to avoid cross-user pollution
@@ -68,7 +68,7 @@ class ApiManager {
}
/// GET request with optional caching support
- ///
+ ///
/// [url] - API endpoint URL
/// [headers] - Optional additional headers
/// [enableToastMessage] - Show toast message from API response
@@ -86,12 +86,12 @@ class ApiManager {
headers ??= {};
headers.addAll(_headers);
Uri url0 = Uri.parse(url);
-
+
// OPTIMIZATION: Check cache first if enabled
if (useCache && !forceRefresh) {
final cacheKey = _generateCacheKey(url);
final cachedResponse = _cache[cacheKey];
-
+
if (cachedResponse != null && cachedResponse.isValid(cacheDuration ?? _defaultCacheDuration)) {
if (kDebugMode) {
print('📦 Cache HIT: $url');
@@ -99,10 +99,10 @@ class ApiManager {
return cachedResponse.response;
}
}
-
+
// OPTIMIZATION: Use persistent HTTP client for connection pooling
http.Response response = await _httpClient.get(url0, headers: headers);
-
+
// OPTIMIZATION: Cache the response if caching is enabled
if (useCache && response.statusCode >= 200 && response.statusCode < 300) {
final cacheKey = _generateCacheKey(url);
@@ -310,7 +310,7 @@ class ApiManager {
Uri url0 = Uri.parse(url);
// print(headers);
// log(json.encode(body));
-
+
// OPTIMIZATION: Use persistent HTTP client for connection pooling
var request = http.Request('PUT', url0);
request.body = json.encode(body);
@@ -337,10 +337,9 @@ class ApiManager {
}
Future multiPart(String url, {Map? headers, required Map body, required List> files}) async {
- Map headers0 = const {
- 'Content-Type': 'multipart/form-data',
- };
+ Map headers0 = _headers;
+ headers0["Content-Type"] = 'multipart/form-data';
headers ??= {};
headers.addAll(headers0);
@@ -423,7 +422,7 @@ class ApiManager {
Navigator.pop(context);
Navigator.of(context).pushNamedAndRemoveUntil(LoginPage.routeName, (routes) => true);
}
-
+
/// Dispose HTTP client (call when app is closing if needed)
/// Note: In most cases, keep client alive for app lifetime for best performance
void dispose() {
diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart
index 7cad5fc3..68fde900 100644
--- a/lib/controllers/api_routes/urls.dart
+++ b/lib/controllers/api_routes/urls.dart
@@ -66,6 +66,7 @@ class URLs {
static get getFaultDescription => "$_baseUrl/Asset/GetDefectForModelDefinition"; // get
static get getAssetById => "$_baseUrl/Asset/GetAssetById?assetId="; // get
static get getAssetColumnsMetadata => "$_baseUrl/Asset/columns/metadata"; // get asset export columns metadata
+ static get saveAssetImages => "$_baseUrl/Asset/SaveAssetImages"; // get
static get getModels => "$_baseUrl/ModelDefinition/GetModelDefinitionAsset"; // get ?client=2
static get getAllRequestsAndCount => "$_baseUrl/CallRequest/GetAllRequestsAndCount"; // get
static get updateUserContactInformation => "$_baseUrl/Account/UpdateContactInfo"; // get
diff --git a/lib/controllers/providers/api/devices_provider.dart b/lib/controllers/providers/api/devices_provider.dart
index 218ebf80..66c95d7b 100644
--- a/lib/controllers/providers/api/devices_provider.dart
+++ b/lib/controllers/providers/api/devices_provider.dart
@@ -9,6 +9,7 @@ import 'package:test_sa/controllers/api_routes/http_status_manger.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/device/asset_by_id_model.dart';
import 'package:test_sa/models/device/asset_search.dart';
+import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/user.dart';
import 'package:test_sa/modules/asset_delivery_module/models/technical_inpection_asset_model.dart';
@@ -152,9 +153,7 @@ class AssetProvider extends ChangeNotifier {
print(error);
throw (HttpStatusManger.getStatusMessage(status: -1, subtitle: appLocalizations));
}
- _stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
- // client's request was successfully received
Map assetData = json.decode(response.body)["data"];
_assetById = AssetByIdModel.fromJson(assetData);
return _assetById!;
@@ -230,4 +229,32 @@ class AssetProvider extends ChangeNotifier {
return [];
}
}
+
+ Future uploadAssetImages(int assetId, TrainingImageData trainingImageData) async {
+ late Response response;
+ try {
+ 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"];
+ // _assetById = AssetByIdModel.fromJson(assetData);
+ // return _assetById!;
+ return true;
+ }
+ return false;
+ } catch (error) {
+ return false;
+ }
+ }
}
diff --git a/lib/extensions/context_extension.dart b/lib/extensions/context_extension.dart
index 41f93d7f..a33130cb 100644
--- a/lib/extensions/context_extension.dart
+++ b/lib/extensions/context_extension.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:localization/localization.dart';
import 'package:provider/provider.dart';
@@ -5,6 +6,7 @@ import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/swipe_module/dialoge/confirm_dialog.dart';
import 'package:test_sa/new_views/swipe_module/dialoge/info_dialog.dart';
+import 'package:test_sa/views/full_screen_image_viewer.dart';
import '../controllers/providers/settings/setting_provider.dart';
import '../l10n/app_localizations.dart';
@@ -24,6 +26,15 @@ extension BuildContextExtension on BuildContext {
UserProvider get userProvider => Provider.of(this, listen: false);
+ void fullScreenImageViewer(Widget child) {
+ Navigator.of(this).push(
+ CupertinoPageRoute(
+ fullscreenDialog: true,
+ builder: (_) => FullScreenImageViewer(child),
+ ),
+ );
+ }
+
bool isTablet() {
final mediaQueryData = MediaQuery.of(this);
final double screenWidth = mediaQueryData.size.width;
@@ -49,6 +60,7 @@ extension BuildContextExtension on BuildContext {
isScrollControlled: true,
isDismissible: true,
backgroundColor: Colors.transparent,
- builder: (context) => SingleChildScrollView(padding: const EdgeInsets.all(0), child: childWidget.bottomSafeArea).bottomSheetContainerNew(context, title: title,showCancelButton: showCancelButton),
+ builder: (context) =>
+ SingleChildScrollView(padding: const EdgeInsets.all(0), child: childWidget.bottomSafeArea).bottomSheetContainerNew(context, title: title, showCancelButton: showCancelButton),
);
}
diff --git a/lib/extensions/widget_extensions.dart b/lib/extensions/widget_extensions.dart
index 2dc5cf00..5273a12a 100644
--- a/lib/extensions/widget_extensions.dart
+++ b/lib/extensions/widget_extensions.dart
@@ -120,12 +120,13 @@ extension WidgetExtensions on Widget {
double padding = 12,
EdgeInsets? paddingObject,
EdgeInsets? margin,
+ double? width,
}) =>
withShadow
? Container(
padding: paddingObject ?? EdgeInsets.all(padding),
margin: margin,
- width: double.infinity,
+ width: width ?? double.infinity,
decoration: ShapeDecoration(
color: backgroundColor ?? AppColor.background(context),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius), side: BorderSide(color: borderColor ?? AppColor.borderColor(context))),
diff --git a/lib/helper/utils.dart b/lib/helper/utils.dart
index 0d706f4e..53496f1f 100644
--- a/lib/helper/utils.dart
+++ b/lib/helper/utils.dart
@@ -13,7 +13,7 @@ import 'package:nfc_manager/nfc_manager_ios.dart' show MiFareIos, Iso15693Ios;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
-import 'package:test_sa/views/widgets/dialogs/loading_dialog.dart';
+import 'package:test_sa/views/widgets/dialogs/loading_with_text_dialog.dart';
// ignore_for_file: avoid_annotating_with_dynamic
@@ -103,7 +103,7 @@ class Utils {
_isLoadingVisible = false;
}
- static void showUploadingDialog(BuildContext context) {
+ static void showLoadingWithTextDialog(BuildContext context, {String? message}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_isLoadingVisible = true;
@@ -111,7 +111,7 @@ class Utils {
context: context,
barrierColor: Colors.black.withOpacity(0.5),
useRootNavigator: false,
- builder: (BuildContext context) => const UploadingDialog(),
+ builder: (BuildContext context) => LoadingWithTextDialog(message: message),
).then((value) {
_isLoadingVisible = false;
});
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 c631b767..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 {
@@ -13,12 +14,12 @@ class AssetByIdModel {
AssetInfo? oldAsset;
AssetReplace? isParent;
AssetInfo? parentAsset;
- AssetReplace? assetType;
+ Lookup? assetType;
DemoRequest? demoRequest;
String? missionCritical;
Site? site;
- AssetReplace? building;
- AssetReplace? floor;
+ Lookup? building;
+ Lookup? floor;
Department? department;
Lookup? room;
num? testsDay;
@@ -54,14 +55,17 @@ class AssetByIdModel {
String? comment;
bool? isEnabled;
String? tagCode;
- List? assetAttachments;
+ List? assetAttachments;
String? retirementTypeName;
String? retirementStatusName;
String? retirementDate;
AssetGroup? assetGroup;
String? assetPhoto;
+ String? modelPhoto;
String? nextPMDate;
String? lastPMDate;
+ bool? hasTrainingImageData;
+ TrainingImageData? trainingImageData;
AssetByIdModel(
{this.multiAssets,
@@ -122,10 +126,15 @@ class AssetByIdModel {
this.retirementDate,
this.assetGroup,
this.assetPhoto,
+ this.modelPhoto,
this.nextPMDate,
- this.lastPMDate});
+ this.lastPMDate,
+ this.trainingImageData,
+ this.hasTrainingImageData});
AssetByIdModel.fromJson(Map json) {
+ print("assetModel!.fromJson:${json['hasTrainingImageData']}");
+
if (json['multiAssets'] != null) {
multiAssets = [];
json['multiAssets'].forEach((v) {
@@ -142,12 +151,12 @@ class AssetByIdModel {
oldAsset = json['oldAsset'] != null ? AssetInfo.fromJson(json['oldAsset']) : null;
isParent = json['isParent'] != null ? AssetReplace.fromJson(json['isParent']) : null;
parentAsset = json['parentAsset'] != null ? AssetInfo.fromJson(json['parentAsset']) : null;
- assetType = json['assetType'] != null ? AssetReplace.fromJson(json['assetType']) : null;
+ assetType = json['assetType'] != null ? Lookup.fromJson(json['assetType']) : null;
demoRequest = json['demoRequest'] != null ? DemoRequest.fromJson(json['demoRequest']) : null;
missionCritical = json['missionCritical'];
site = json['site'] != null ? Site.fromJson(json['site']) : null;
- building = json['building'] != null ? AssetReplace.fromJson(json['building']) : null;
- floor = json['floor'] != null ? AssetReplace.fromJson(json['floor']) : null;
+ building = json['building'] != null ? Lookup.fromJson(json['building']) : null;
+ floor = json['floor'] != null ? Lookup.fromJson(json['floor']) : null;
department = json['department'] != null ? Department.fromJson(json['department']) : null;
room = json['room'] != null ? Lookup.fromJson(json['room']) : null;
testsDay = json['testsDay'];
@@ -189,9 +198,9 @@ class AssetByIdModel {
isEnabled = json['isEnabled'];
tagCode = json['tagCode'];
if (json['assetAttachments'] != null) {
- assetAttachments = [];
+ assetAttachments = [];
json['assetAttachments'].forEach((v) {
- assetAttachments!.add(v);
+ assetAttachments!.add(AssetAttachment.fromJson(v));
});
}
retirementTypeName = json['retirementTypeName'];
@@ -199,8 +208,11 @@ class AssetByIdModel {
retirementDate = json['retirementDate'];
assetGroup = json['assetGroup'] != null ? AssetGroup.fromJson(json['assetGroup']) : null;
assetPhoto = json['assetPhoto'];
+ modelPhoto = json['modelPhoto'];
nextPMDate = json['nextPMDate'];
lastPMDate = json['lastPMDate'];
+ hasTrainingImageData = json['hasTrainingImageData'];
+ trainingImageData = json['trainingImageData'] != null ? TrainingImageData.fromJson(json['trainingImageData']) : null;
}
Map toJson() {
@@ -295,8 +307,11 @@ class AssetByIdModel {
data['assetGroup'] = assetGroup?.toJson();
}
data['assetPhoto'] = assetPhoto;
+ data['modelPhoto'] = modelPhoto;
data['nextPMDate'] = nextPMDate;
data['lastPMDate'] = lastPMDate;
+ data['hasTrainingImageData'] = hasTrainingImageData;
+ data['trainingImageData'] = trainingImageData?.toJson();
return data;
}
}
@@ -323,6 +338,31 @@ class MultiAssets {
}
}
+class AssetAttachment {
+ int? id;
+ String? attachmentName;
+ String? attachmentURL;
+ String? originalName;
+
+ AssetAttachment({this.id, this.attachmentName, this.attachmentURL, this.originalName});
+
+ AssetAttachment.fromJson(Map json) {
+ id = json['id'];
+ attachmentName = json['attachmentName'];
+ attachmentURL = json['attachmentURL'];
+ originalName = json['originalName'];
+ }
+
+ Map toJson() {
+ final Map data = {};
+ data['id'] = id;
+ data['attachmentName'] = attachmentName;
+ data['attachmentURL'] = attachmentURL;
+ data['originalName'] = originalName;
+ return data;
+ }
+}
+
class ModelDefinition {
int? id;
String? assetName;
@@ -546,18 +586,50 @@ class Floors {
class Departments {
int? id;
String? name;
+ List? rooms;
Departments({this.id, this.name});
Departments.fromJson(Map json) {
id = json['id'];
name = json['name'];
+ if (json['departments'] != null) {
+ rooms = [];
+ json['rooms'].forEach((v) {
+ rooms!.add(Rooms.fromJson(v));
+ });
+ }
+ }
+
+ Map toJson() {
+ final Map data = {};
+ data['id'] = id;
+ data['name'] = name;
+ if (rooms != null) {
+ data['rooms'] = rooms!.map((v) => v.toJson()).toList();
+ }
+ return data;
+ }
+}
+
+class Rooms {
+ int? id;
+ String? name;
+ int? value;
+
+ Rooms({this.id, this.name, this.value});
+
+ Rooms.fromJson(Map json) {
+ id = json['id'];
+ name = json['name'];
+ value = json['value'];
}
Map toJson() {
final Map data = {};
data['id'] = id;
data['name'] = name;
+ data['value'] = value;
return data;
}
}
@@ -567,14 +639,16 @@ class Department {
String? departmentName;
String? departmentCode;
String? ntCode;
+ num? operationalHours;
- Department({this.id, this.departmentName, this.departmentCode, this.ntCode});
+ Department({this.id, this.departmentName, this.departmentCode, this.ntCode, this.operationalHours});
Department.fromJson(Map json) {
id = json['id'];
departmentName = json['departmentName'];
departmentCode = json['departmentCode'];
ntCode = json['ntCode'];
+ operationalHours = json['operationalHours'];
}
Map toJson() {
@@ -583,6 +657,7 @@ class Department {
data['departmentName'] = departmentName;
data['departmentCode'] = departmentCode;
data['ntCode'] = ntCode;
+ data['operationalHours'] = operationalHours;
return data;
}
}
@@ -725,3 +800,37 @@ class DemoRequest {
return data;
}
}
+
+class TrainingImageData {
+ GenericAttachmentModel? leftImage;
+ GenericAttachmentModel? rightImage;
+ GenericAttachmentModel? upImage;
+ GenericAttachmentModel? downImage;
+ List? 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 c6fef7db..bd9ecbe6 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
@@ -18,6 +18,7 @@ import 'package:test_sa/modules/cm_module/views/forms/asset_retired/asset_retire
import 'package:test_sa/modules/cm_module/views/forms/cost/cost_detail_form_screen.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
+import 'package:test_sa/views/widgets/equipment/asset_detail_page.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
import 'package:test_sa/views/widgets/item_views/info_date_widget.dart';
@@ -141,7 +142,7 @@ class _ServiceRequestDetailViewState extends State {
// spacing: 8,
runSpacing: 8,
children: [
- if (Provider.of(context, listen: false).showPriority&&workOrder.priority!=null) ...[
+ if (Provider.of(context, listen: false).showPriority && workOrder.priority != null) ...[
StatusLabel(
label: workOrder.priority?.name,
id: workOrder.priority?.value,
@@ -192,7 +193,24 @@ class _ServiceRequestDetailViewState extends State {
],
),
8.height,
- InfoHeader16Widget(context.translation.assetInformation),
+ Row(
+ spacing: 4,
+ children: [
+ Flexible(
+ child: InfoHeader16Widget(context.translation.assetInformation),
+ ),
+ const Icon(Icons.open_in_new_rounded, color: AppColor.primary10, size: 16).onPress(() {
+ Navigator.of(context).pushNamed(AssetDetailPage.id, arguments: workOrder.asset!.id);
+ })
+ ],
+ ),
+ if (context.userProvider.isEngineer && !(workOrder.asset!.hasTrainingImageData ?? true)) ...[
+ Text(
+ '*Images are required for the asset',
+ style: AppTextStyles.bodyText.copyWith(color: AppColor.red30),
+ ),
+ 4.height,
+ ],
InfoTextWidget(label: context.translation.assetName, value: workOrder.assetNdModel!.name?.cleanupWhitespace.capitalizeFirstOfEach),
InfoTextWidget(label: context.translation.assetNumber, value: workOrder.asset!.assetNumber),
InfoTextWidget(label: context.translation.serialNo, value: workOrder.asset!.assetSerialNo),
diff --git a/lib/modules/cx_module/chat/chat_page.dart b/lib/modules/cx_module/chat/chat_page.dart
index ea9893b6..6b0fbd37 100644
--- a/lib/modules/cx_module/chat/chat_page.dart
+++ b/lib/modules/cx_module/chat/chat_page.dart
@@ -388,7 +388,7 @@ class _ChatPageState extends State {
clipBehavior: Clip.antiAliasWithSaveLayer,
builder: (BuildContext context) => ChatFilePicker())) as File?;
if (file != null) {
- Utils.showUploadingDialog(context);
+ Utils.showLoadingWithTextDialog(context);
await chatProvider.uploadAttachments(context.userProvider.user!.username!, file, "1");
Utils.hideLoading(context);
}
@@ -638,7 +638,7 @@ class _ChatPageState extends State {
hoverColor: Colors.transparent,
onPressed: () async {
if (chatState == ChatState.voiceRecordingCompleted) {
- Utils.showUploadingDialog(context);
+ Utils.showLoadingWithTextDialog(context);
try {
await chatProvider.uploadAttachments(context.userProvider.user!.username!, File(recordedFilePath!), "1");
Utils.hideLoading(context);
diff --git a/lib/modules/tm_module/device_transfer/device_search_screen.dart b/lib/modules/tm_module/device_transfer/device_search_screen.dart
index 74cdeeb6..b5a90457 100644
--- a/lib/modules/tm_module/device_transfer/device_search_screen.dart
+++ b/lib/modules/tm_module/device_transfer/device_search_screen.dart
@@ -51,7 +51,6 @@ class _DeviceSearchScreenState extends State {
_modelController.dispose();
_supplierController.dispose();
_siteController.dispose();
- _siteController.dispose();
_manufactureController.dispose();
_mdController.dispose();
super.dispose();
diff --git a/lib/views/full_screen_image_viewer.dart b/lib/views/full_screen_image_viewer.dart
new file mode 100644
index 00000000..43aee198
--- /dev/null
+++ b/lib/views/full_screen_image_viewer.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+import 'package:test_sa/extensions/widget_extensions.dart';
+
+class FullScreenImageViewer extends StatelessWidget {
+ final Widget child;
+
+ const FullScreenImageViewer(this.child, {Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Stack(
+ children: [
+ InteractiveViewer(clipBehavior: Clip.none, panEnabled: true, minScale: 0.5, maxScale: 4.0, child: child).center,
+ Positioned(top: MediaQuery.of(context).padding.top, left: 12.0, child: const Icon(Icons.clear).toShadowContainer(context, width: 40, padding: 8).onPress(() => Navigator.maybePop(context))),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/views/widgets/dialogs/loading_with_text_dialog.dart b/lib/views/widgets/dialogs/loading_with_text_dialog.dart
new file mode 100644
index 00000000..b07b3eb6
--- /dev/null
+++ b/lib/views/widgets/dialogs/loading_with_text_dialog.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:test_sa/extensions/int_extensions.dart';
+import 'package:test_sa/new_views/app_style/app_color.dart';
+import 'package:test_sa/views/app_style/sizing.dart';
+
+class LoadingWithTextDialog extends StatelessWidget {
+ final String? message;
+
+ const LoadingWithTextDialog({Key? key, this.message}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Material(
+ borderRadius: BorderRadius.circular(20.0),
+ child: Container(
+ height: 150.toScreenWidth,
+ width: 200.toScreenWidth,
+ alignment: Alignment.center,
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: AppColor.neutral30,
+ borderRadius: BorderRadius.circular(20.0),
+ boxShadow: [AppStyle.boxShadow],
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const CircularProgressIndicator(strokeWidth: 3),
+ 16.height,
+ Text(
+ message ?? "Uploading...",
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColor.textColor(context), height: 35 / 24, letterSpacing: -0.96),
+ )
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/views/widgets/equipment/asset_detail_page.dart b/lib/views/widgets/equipment/asset_detail_page.dart
index dd94a08d..14d09949 100644
--- a/lib/views/widgets/equipment/asset_detail_page.dart
+++ b/lib/views/widgets/equipment/asset_detail_page.dart
@@ -7,10 +7,17 @@ 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/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/views/components/action_button/footer_action_button.dart';
+import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
+import 'package:test_sa/views/widgets/equipment/asset_image_upload_page.dart';
+import 'package:test_sa/views/widgets/images/files_list.dart';
+import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/failed_loading.dart';
@@ -48,6 +55,11 @@ class _AssetDetailPageState extends State with SingleTickerProv
super.dispose();
}
+ int? assetId;
+ AssetProvider? _assetProvider;
+ AssetByIdModel? assetModel;
+
+ List attachments = [];
@override
Widget build(BuildContext context) {
@@ -57,7 +69,7 @@ class _AssetDetailPageState extends State with SingleTickerProv
return Scaffold(
appBar: DefaultAppBar(title: context.translation.assetDetails),
body: FutureBuilder(
- future: _assetProvider!.getAssetById(assetId!, context.translation),
+ future: assetModel == null ? _assetProvider!.getAssetById(assetId!, context.translation) : null,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return FailedLoading(
@@ -72,6 +84,14 @@ class _AssetDetailPageState extends State with SingleTickerProv
} else if (snapshot.hasData) {}
if (snapshot.hasData) {
AssetByIdModel assetModel = snapshot.data!;
+ attachments = assetModel?.assetAttachments
+ ?.map((attachment) => GenericAttachmentModel(
+ id: attachment.id,
+ name: attachment.attachmentName,
+ originalName: attachment.originalName,
+ ))
+ .toList() ??
+ [];
// Start animation when data is loaded
_animationController.forward(from: 0.0);
@@ -239,6 +259,14 @@ class _AssetDetailPageState extends State with SingleTickerProv
),
8.height,
+ if (context.userProvider.isEngineer && !(assetModel!.hasTrainingImageData ?? true)) ...[
+ Text(
+ '*Upload Images for this asset',
+ style: AppTextStyles.bodyText2.copyWith(color: AppColor.red30),
+ ),
+ 4.height,
+ ],
+
// Details row with animation
FadeTransition(
opacity: detailsAnimation,
@@ -313,9 +341,19 @@ class _AssetDetailPageState extends State with SingleTickerProv
),
]
],
- )
+ ),
+ FilesList(images: attachments.map((toElement) => URLs.getFileUrl(toElement.name!) ?? '').toList()),
],
).toShadowContainer(context),
+ if (context.userProvider.isEngineer && !(assetModel!.hasTrainingImageData ?? true))
+ FooterActionButton.footerContainer(
+ context: context,
+ child: AppFilledButton(
+ buttonColor: AppColor.primary10,
+ label: "Upload Images",
+ onPressed: _upload,
+ ),
+ ),
);
}
return const Center(child: ALoading());
@@ -323,4 +361,12 @@ class _AssetDetailPageState extends State with SingleTickerProv
),
);
}
+
+ void _upload() async {
+ 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
new file mode 100644
index 00000000..7cf3aaa1
--- /dev/null
+++ b/lib/views/widgets/equipment/asset_image_upload_page.dart
@@ -0,0 +1,250 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:test_sa/controllers/providers/api/devices_provider.dart';
+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';
+import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
+import 'package:test_sa/views/full_screen_image_viewer.dart';
+import 'package:test_sa/views/widgets/dialogs/dialog.dart';
+import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
+
+class AssetImageUploadPage extends StatefulWidget {
+ int assetId;
+ TrainingImageData? attachments;
+
+ AssetImageUploadPage({Key? key, required this.assetId, this.attachments}) : super(key: key);
+
+ @override
+ _AssetImageUploadPageState createState() {
+ return _AssetImageUploadPageState();
+ }
+}
+
+class _AssetImageUploadPageState extends State {
+ late TrainingImageData trainingImageData;
+
+ @override
+ void initState() {
+ super.initState();
+ trainingImageData = widget.attachments ?? TrainingImageData(additionalImages: []);
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: const DefaultAppBar(title: "Upload Images"),
+ body: Column(
+ children: [
+ 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: "Additional Images",
+ attachment: trainingImageData.additionalImages!,
+ buttonColor: AppColor.black10,
+ onlyImages: true,
+ pickMultiple: true,
+ buttonIcon: 'image-plus'.toSvgAsset(color: context.isDark ? AppColor.primary10 : AppColor.neutral120),
+ onChange: (_attachments) {
+ trainingImageData.additionalImages = _attachments;
+ setState(() {});
+ },
+ ).toShadowContainer(context),
+ ],
+ ).expanded,
+ FooterActionButton.footerContainer(
+ context: context,
+ child: AppFilledButton(
+ buttonColor: AppColor.primary10,
+ 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: "I followed all the instructions and uploading the images in high quality, clear and not blurry.",
+ ),
+ );
+ if (result) {
+ Utils.showLoadingWithTextDialog(context);
+ result = await Provider.of(context, listen: false).uploadAssetImages(widget.assetId, trainingImageData);
+ Utils.hideLoading(context);
+ if (result) {
+ Navigator.pop(context, result);
+ }
+ }
+ },
+ ),
+ ),
+ ],
+ ));
+ }
+
+ 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(() => context.fullScreenImageViewer(imageWidget)));
+ }
+ 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 4947d46d..8c10be02 100644
--- a/lib/views/widgets/equipment/asset_item_gridview.dart
+++ b/lib/views/widgets/equipment/asset_item_gridview.dart
@@ -13,7 +13,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) {
@@ -44,6 +44,13 @@ class AssetItemGridView extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodyText.copyWith(color: AppColor.textStyleColor(context)),
),
+ 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,
diff --git a/lib/views/widgets/equipment/asset_item_listview.dart b/lib/views/widgets/equipment/asset_item_listview.dart
index 6fced692..73c9187b 100644
--- a/lib/views/widgets/equipment/asset_item_listview.dart
+++ b/lib/views/widgets/equipment/asset_item_listview.dart
@@ -44,6 +44,13 @@ class AssetItemListView extends StatelessWidget {
children: [
InfoHeaderWidget(device.modelDefinition!.assetName!.cleanupWhitespace),
// device.modelDefinition!.assetName!.cleanupWhitespace.capitalizeFirstOfEach.heading6(context),
+ if (context.userProvider.isEngineer && !(device.hasTrainingImageData ?? true)) ...[
+ 2.height,
+ Text(
+ '*Upload Images for this asset',
+ style: AppTextStyles.tinyFont2.copyWith(color: AppColor.red30),
+ ),
+ ],
4.height,
InfoTextWidget(label: context.translation.assetNumber, value: device.assetNumber),
InfoTextWidget(label: context.translation.modelName, value: device.modelDefinition!.modelName),
diff --git a/lib/views/widgets/equipment/single_device_picker.dart b/lib/views/widgets/equipment/single_device_picker.dart
new file mode 100644
index 00000000..e69de29b
diff --git a/lib/views/widgets/images/multi_image_picker.dart b/lib/views/widgets/images/multi_image_picker.dart
index 582bc6ea..58165803 100644
--- a/lib/views/widgets/images/multi_image_picker.dart
+++ b/lib/views/widgets/images/multi_image_picker.dart
@@ -3,11 +3,13 @@ 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';
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/generic_attachment_model.dart';
import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
@@ -322,7 +324,10 @@ class AttachmentPicker extends StatefulWidget {
final Function(List)? onChange;
final bool showAsGrid;
final bool showAsListView;
+ final bool pickMultiple;
Lookup? documentType;
+ Widget? child;
+ bool checkQuality;
AttachmentPicker(
{Key? key,
@@ -337,6 +342,9 @@ class AttachmentPicker extends StatefulWidget {
this.documentType,
this.showAsGrid = false,
this.showAsListView = false,
+ this.pickMultiple = false,
+ this.checkQuality = false,
+ this.child,
this.buttonColor})
: super(key: key);
@@ -347,72 +355,78 @@ class AttachmentPicker extends StatefulWidget {
class _AttachmentPickerState extends State {
@override
Widget build(BuildContext context) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- AppDashedButton(
- title: widget.label,
- height: widget.buttonHeight,
- buttonColor: widget.buttonColor,
- icon: widget.buttonIcon,
- onPressed: (widget.enabled == false)
- ? () {}
- : widget.showAsGrid
- ? showFileSourceSheet
- : onFilePicker),
- 16.height,
- if (widget.attachment.isNotEmpty)
- widget.showAsListView
- ? ListView.separated(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- padding: EdgeInsets.zero,
- 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(() {});
+ },
+ );
},
- );
- },
- ),
- ),
- ],
- );
+ 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 {
@@ -466,16 +480,21 @@ class _AttachmentPickerState extends State {
).paddingAll(21),
)) as ImageSource;
- final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800);
+ // if(widget.pickMultiple) {
+ final pickedFile12 = await ImagePicker().pickMultiImage(imageQuality: 70, maxWidth: 800, maxHeight: 800);
- if (pickedFile != null) {
- File fileImage = File(pickedFile.path);
- widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path, documentTypeId: widget.documentType));
- if (widget.onChange != null) {
- widget.onChange!(widget.attachment);
- }
- setState(() {});
- }
+ // // }
+ //
+ // final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800);
+ //
+ // if (pickedFile != null) {
+ // File fileImage = File(pickedFile.path);
+ // widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path, documentTypeId: widget.documentType));
+ // if (widget.onChange != null) {
+ // widget.onChange!(widget.attachment);
+ // }
+ // setState(() {});
+ // }
}
Widget gridItem(IconData iconData, String title) {
@@ -501,7 +520,7 @@ class _AttachmentPickerState extends State {
);
}
- onFilePicker() async {
+ onFilePicker(bool checkQuality) async {
ImageSource? source = await showModalBottomSheet(
context: context,
builder: (BuildContext context) {
@@ -629,10 +648,37 @@ class _AttachmentPickerState extends State {
// );
if (source == null) return;
- final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800);
+ if (widget.pickMultiple && source == ImageSource.gallery) {
+ final pickedFiles = await ImagePicker().pickMultiImage(imageQuality: 70);
+ if (pickedFiles.isNotEmpty) {
+ pickedFiles.forEach((pickedFile) {
+ File fileImage = File(pickedFile.path);
+ widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path, documentTypeId: widget.documentType));
+ });
+ if (widget.onChange != null) {
+ widget.onChange!(widget.attachment);
+ }
+ setState(() {});
+ return;
+ }
+ }
+ final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 50);
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 23e7cefa..12581b33 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
@@ -139,6 +140,7 @@ flutter:
- assets/images/
- assets/images/dashboard/
- assets/images/wo/
+ - assets/images/asset_image_upload/
- assets/lottie/
- assets/subtitles/
- assets/rives/