Merge branch 'refs/heads/main_production_ai_enhancement' into ui_ux_rollout_merge

# Conflicts:
#	lib/controllers/api_routes/urls.dart
#	lib/modules/cm_module/views/components/service_request_detail_view.dart
#	lib/views/widgets/dialogs/loading_dialog.dart
#	lib/views/widgets/equipment/asset_detail_page.dart
#	lib/views/widgets/equipment/asset_item_gridview.dart
#	lib/views/widgets/equipment/asset_item_listview.dart
#	lib/views/widgets/equipment/single_device_picker.dart
#	pubspec.yaml
ui_ux_rollout_merge
Sikander Saleem 2 weeks ago
commit 8d45b02614

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

After

Width:  |  Height:  |  Size: 808 B

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

After

Width:  |  Height:  |  Size: 824 B

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

After

Width:  |  Height:  |  Size: 837 B

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

After

Width:  |  Height:  |  Size: 797 B

@ -337,10 +337,9 @@ class ApiManager {
} }
Future<http.Response> multiPart(String url, {Map<String, String>? headers, required Map<String, String> body, required List<Future<MultipartFile>> files}) async { Future<http.Response> multiPart(String url, {Map<String, String>? headers, required Map<String, String> body, required List<Future<MultipartFile>> files}) async {
Map<String, String> headers0 = const { Map<String, String> headers0 = _headers;
'Content-Type': 'multipart/form-data',
};
headers0["Content-Type"] = 'multipart/form-data';
headers ??= {}; headers ??= {};
headers.addAll(headers0); headers.addAll(headers0);

@ -66,6 +66,7 @@ class URLs {
static get getFaultDescription => "$_baseUrl/Asset/GetDefectForModelDefinition"; // get static get getFaultDescription => "$_baseUrl/Asset/GetDefectForModelDefinition"; // get
static get getAssetById => "$_baseUrl/Asset/GetAssetById?assetId="; // get static get getAssetById => "$_baseUrl/Asset/GetAssetById?assetId="; // get
static get getAssetColumnsMetadata => "$_baseUrl/Asset/columns/metadata"; // get asset export columns metadata 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 getModels => "$_baseUrl/ModelDefinition/GetModelDefinitionAsset"; // get ?client=2
static get getAllRequestsAndCount => "$_baseUrl/CallRequest/GetAllRequestsAndCount"; // get static get getAllRequestsAndCount => "$_baseUrl/CallRequest/GetAllRequestsAndCount"; // get
static get updateUserContactInformation => "$_baseUrl/Account/UpdateContactInfo"; // get static get updateUserContactInformation => "$_baseUrl/Account/UpdateContactInfo"; // get

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

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:localization/localization.dart'; import 'package:localization/localization.dart';
import 'package:provider/provider.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/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/confirm_dialog.dart';
import 'package:test_sa/new_views/swipe_module/dialoge/info_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 '../controllers/providers/settings/setting_provider.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
@ -24,6 +26,15 @@ extension BuildContextExtension on BuildContext {
UserProvider get userProvider => Provider.of<UserProvider>(this, listen: false); UserProvider get userProvider => Provider.of<UserProvider>(this, listen: false);
void fullScreenImageViewer(Widget child) {
Navigator.of(this).push(
CupertinoPageRoute(
fullscreenDialog: true,
builder: (_) => FullScreenImageViewer(child),
),
);
}
bool isTablet() { bool isTablet() {
final mediaQueryData = MediaQuery.of(this); final mediaQueryData = MediaQuery.of(this);
final double screenWidth = mediaQueryData.size.width; final double screenWidth = mediaQueryData.size.width;
@ -49,6 +60,7 @@ extension BuildContextExtension on BuildContext {
isScrollControlled: true, isScrollControlled: true,
isDismissible: true, isDismissible: true,
backgroundColor: Colors.transparent, 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),
); );
} }

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

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

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

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

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

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

@ -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/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/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_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/files_list.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker.dart'; import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
import 'package:test_sa/views/widgets/item_views/info_date_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_date_widget.dart';
@ -141,7 +142,7 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
// spacing: 8, // spacing: 8,
runSpacing: 8, runSpacing: 8,
children: [ children: [
if (Provider.of<SettingProvider>(context, listen: false).showPriority&&workOrder.priority!=null) ...[ if (Provider.of<SettingProvider>(context, listen: false).showPriority && workOrder.priority != null) ...[
StatusLabel( StatusLabel(
label: workOrder.priority?.name, label: workOrder.priority?.name,
id: workOrder.priority?.value, id: workOrder.priority?.value,
@ -192,7 +193,24 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
], ],
), ),
8.height, 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.assetName, value: workOrder.assetNdModel!.name?.cleanupWhitespace.capitalizeFirstOfEach),
InfoTextWidget(label: context.translation.assetNumber, value: workOrder.asset!.assetNumber), InfoTextWidget(label: context.translation.assetNumber, value: workOrder.asset!.assetNumber),
InfoTextWidget(label: context.translation.serialNo, value: workOrder.asset!.assetSerialNo), InfoTextWidget(label: context.translation.serialNo, value: workOrder.asset!.assetSerialNo),

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

@ -51,7 +51,6 @@ class _DeviceSearchScreenState extends State<DeviceSearchScreen> {
_modelController.dispose(); _modelController.dispose();
_supplierController.dispose(); _supplierController.dispose();
_siteController.dispose(); _siteController.dispose();
_siteController.dispose();
_manufactureController.dispose(); _manufactureController.dispose();
_mdController.dispose(); _mdController.dispose();
super.dispose(); super.dispose();

@ -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))),
],
),
);
}
}

@ -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),
)
],
),
),
),
);
}
}

@ -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/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_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/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/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_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_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/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/failed_loading.dart'; import 'package:test_sa/views/widgets/loaders/failed_loading.dart';
@ -48,6 +55,11 @@ class _AssetDetailPageState extends State<AssetDetailPage> with SingleTickerProv
super.dispose(); super.dispose();
} }
int? assetId;
AssetProvider? _assetProvider;
AssetByIdModel? assetModel;
List<GenericAttachmentModel> attachments = [];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -57,7 +69,7 @@ class _AssetDetailPageState extends State<AssetDetailPage> with SingleTickerProv
return Scaffold( return Scaffold(
appBar: DefaultAppBar(title: context.translation.assetDetails), appBar: DefaultAppBar(title: context.translation.assetDetails),
body: FutureBuilder<AssetByIdModel>( body: FutureBuilder<AssetByIdModel>(
future: _assetProvider!.getAssetById(assetId!, context.translation), future: assetModel == null ? _assetProvider!.getAssetById(assetId!, context.translation) : null,
builder: (BuildContext context, AsyncSnapshot<AssetByIdModel> snapshot) { builder: (BuildContext context, AsyncSnapshot<AssetByIdModel> snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return FailedLoading( return FailedLoading(
@ -72,6 +84,14 @@ class _AssetDetailPageState extends State<AssetDetailPage> with SingleTickerProv
} else if (snapshot.hasData) {} } else if (snapshot.hasData) {}
if (snapshot.hasData) { if (snapshot.hasData) {
AssetByIdModel assetModel = snapshot.data!; 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 // Start animation when data is loaded
_animationController.forward(from: 0.0); _animationController.forward(from: 0.0);
@ -239,6 +259,14 @@ class _AssetDetailPageState extends State<AssetDetailPage> with SingleTickerProv
), ),
8.height, 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 // Details row with animation
FadeTransition( FadeTransition(
opacity: detailsAnimation, opacity: detailsAnimation,
@ -313,9 +341,19 @@ class _AssetDetailPageState extends State<AssetDetailPage> with SingleTickerProv
), ),
] ]
], ],
) ),
FilesList(images: attachments.map((toElement) => URLs.getFileUrl(toElement.name!) ?? '').toList()),
], ],
).toShadowContainer(context), ).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()); return const Center(child: ALoading());
@ -323,4 +361,12 @@ class _AssetDetailPageState extends State<AssetDetailPage> with SingleTickerProv
), ),
); );
} }
void _upload() async {
bool reload = await Navigator.push(context, MaterialPageRoute(builder: (context) => AssetImageUploadPage(assetId: assetId!))) ?? false;
if (reload) {
assetModel = null;
setState(() {});
}
}
} }

@ -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<AssetImageUploadPage> {
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: <Widget>[
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<AssetProvider>(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);
},
);
}
}

@ -13,7 +13,7 @@ class AssetItemGridView extends StatelessWidget {
final Asset device; final Asset device;
final Function(Asset) onPressed; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -44,6 +44,13 @@ class AssetItemGridView extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodyText.copyWith(color: AppColor.textStyleColor(context)), 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( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

@ -44,6 +44,13 @@ class AssetItemListView extends StatelessWidget {
children: [ children: [
InfoHeaderWidget(device.modelDefinition!.assetName!.cleanupWhitespace), InfoHeaderWidget(device.modelDefinition!.assetName!.cleanupWhitespace),
// device.modelDefinition!.assetName!.cleanupWhitespace.capitalizeFirstOfEach.heading6(context), // 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, 4.height,
InfoTextWidget(label: context.translation.assetNumber, value: device.assetNumber), InfoTextWidget(label: context.translation.assetNumber, value: device.assetNumber),
InfoTextWidget(label: context.translation.modelName, value: device.modelDefinition!.modelName), InfoTextWidget(label: context.translation.modelName, value: device.modelDefinition!.modelName),

@ -3,11 +3,13 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_blur_detection/image_blur_detection.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_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/generic_attachment_model.dart';
import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
@ -322,7 +324,10 @@ class AttachmentPicker extends StatefulWidget {
final Function(List<GenericAttachmentModel>)? onChange; final Function(List<GenericAttachmentModel>)? onChange;
final bool showAsGrid; final bool showAsGrid;
final bool showAsListView; final bool showAsListView;
final bool pickMultiple;
Lookup? documentType; Lookup? documentType;
Widget? child;
bool checkQuality;
AttachmentPicker( AttachmentPicker(
{Key? key, {Key? key,
@ -337,6 +342,9 @@ class AttachmentPicker extends StatefulWidget {
this.documentType, this.documentType,
this.showAsGrid = false, this.showAsGrid = false,
this.showAsListView = false, this.showAsListView = false,
this.pickMultiple = false,
this.checkQuality = false,
this.child,
this.buttonColor}) this.buttonColor})
: super(key: key); : super(key: key);
@ -347,72 +355,78 @@ class AttachmentPicker extends StatefulWidget {
class _AttachmentPickerState extends State<AttachmentPicker> { class _AttachmentPickerState extends State<AttachmentPicker> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return widget.child != null
crossAxisAlignment: CrossAxisAlignment.start, ? widget.child!.onPress((widget.enabled == false)
children: [ ? () {}
AppDashedButton( : widget.showAsGrid
title: widget.label, ? () => showFileSourceSheet()
height: widget.buttonHeight, : () => onFilePicker(widget.checkQuality))
buttonColor: widget.buttonColor, : Column(
icon: widget.buttonIcon, crossAxisAlignment: CrossAxisAlignment.start,
onPressed: (widget.enabled == false) spacing: 12,
? () {} children: [
: widget.showAsGrid AppDashedButton(
? showFileSourceSheet title: widget.label,
: onFilePicker), height: widget.buttonHeight,
16.height, buttonColor: widget.buttonColor,
if (widget.attachment.isNotEmpty) icon: widget.buttonIcon,
widget.showAsListView onPressed: (widget.enabled == false)
? ListView.separated( ? () {}
shrinkWrap: true, : widget.showAsGrid
physics: const NeverScrollableScrollPhysics(), ? () => showFileSourceSheet()
padding: EdgeInsets.zero, : () => onFilePicker(widget.checkQuality)),
itemBuilder: (cxt, index) { if (widget.attachment.isNotEmpty)
File image = File(widget.attachment[index].name!); widget.showAsListView
return MultiFilesPickerItem( ? ListView.separated(
file: image, shrinkWrap: true,
showAsListView: widget.showAsListView, physics: const NeverScrollableScrollPhysics(),
enabled: widget.enabled, padding: const EdgeInsets.only(top: 0),
documentType: widget.attachment[index].documentTypeId?.name, itemBuilder: (cxt, index) {
onRemoveTap: (image) { File image = File(widget.attachment[index].name!);
if (!widget.enabled) { return MultiFilesPickerItem(
return; file: image,
} showAsListView: widget.showAsListView,
widget.attachment.removeAt(index); enabled: widget.enabled,
if (widget.onChange != null) { documentType: widget.attachment[index].documentTypeId?.name,
widget.onChange!(widget.attachment); onRemoveTap: (image) {
} if (!widget.enabled) {
setState(() {}); return;
}, }
); widget.attachment.removeAt(index);
}, if (widget.onChange != null) {
separatorBuilder: (cxt, index) => 8.height, widget.onChange!(widget.attachment);
itemCount: widget.attachment.length) }
: Wrap( setState(() {});
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(() {});
}, },
); 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 { fromFilePicker() async {
@ -466,16 +480,21 @@ class _AttachmentPickerState extends State<AttachmentPicker> {
).paddingAll(21), ).paddingAll(21),
)) as ImageSource; )) 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)); // final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800);
if (widget.onChange != null) { //
widget.onChange!(widget.attachment); // if (pickedFile != null) {
} // File fileImage = File(pickedFile.path);
setState(() {}); // 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) { Widget gridItem(IconData iconData, String title) {
@ -501,7 +520,7 @@ class _AttachmentPickerState extends State<AttachmentPicker> {
); );
} }
onFilePicker() async { onFilePicker(bool checkQuality) async {
ImageSource? source = await showModalBottomSheet<ImageSource>( ImageSource? source = await showModalBottomSheet<ImageSource>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
@ -629,10 +648,37 @@ class _AttachmentPickerState extends State<AttachmentPicker> {
// ); // );
if (source == null) return; 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) { if (pickedFile != null) {
File fileImage = File(pickedFile.path); 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)); widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path, documentTypeId: widget.documentType));
if (widget.onChange != null) { if (widget.onChange != null) {
widget.onChange!(widget.attachment); widget.onChange!(widget.attachment);

@ -88,6 +88,7 @@ dependencies:
flutter_timezone: ^3.0.1 flutter_timezone: ^3.0.1
device_calendar: ^4.3.3 device_calendar: ^4.3.3
pie_chart: ^5.4.0 pie_chart: ^5.4.0
image_blur_detection: ^1.0.1
badges: ^3.1.1 badges: ^3.1.1
# buttons_tabbar: ^1.1.2 # buttons_tabbar: ^1.1.2
@ -139,6 +140,7 @@ flutter:
- assets/images/ - assets/images/
- assets/images/dashboard/ - assets/images/dashboard/
- assets/images/wo/ - assets/images/wo/
- assets/images/asset_image_upload/
- assets/lottie/ - assets/lottie/
- assets/subtitles/ - assets/subtitles/
- assets/rives/ - assets/rives/

Loading…
Cancel
Save