From 37a9cd3cc973eeb0962c6c6801384bff3243ef88 Mon Sep 17 00:00:00 2001 From: zaid_daoud Date: Mon, 6 Nov 2023 14:23:22 +0300 Subject: [PATCH] service request comment done --- lib/controllers/api_routes/urls.dart | 3 + .../providers/api/comments_provider.dart | 112 ++++++++++++++++ lib/main.dart | 2 + lib/models/comment.dart | 75 +++++++++++ .../user/requests/comments_bottom_sheet.dart | 125 ++++++++++++++++++ .../requests/service_request_details.dart | 78 ++++++----- 6 files changed, 361 insertions(+), 34 deletions(-) create mode 100644 lib/controllers/providers/api/comments_provider.dart create mode 100644 lib/models/comment.dart create mode 100644 lib/views/pages/user/requests/comments_bottom_sheet.dart diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 14a19e32..ddc9dd25 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -112,4 +112,7 @@ class URLs { static get getPentryStatus => "$_baseUrl/Lookups/GetLookup?lookupEnum=401"; // get // contacts static get getPentryContacts => "$_baseUrl/handle/return/all/contacts"; // get + //comments + static get getComments => "$_baseUrl/CallRequest/GetHistoryComments"; // get + static get addComment => "$_baseUrl/CallRequest/AddHistoryComment"; // add } diff --git a/lib/controllers/providers/api/comments_provider.dart b/lib/controllers/providers/api/comments_provider.dart new file mode 100644 index 00000000..2c30be3a --- /dev/null +++ b/lib/controllers/providers/api/comments_provider.dart @@ -0,0 +1,112 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:http/http.dart'; +import 'package:test_sa/controllers/api_routes/api_manager.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/models/comment.dart'; + +import '../../../new_views/common_widgets/app_lazy_loading.dart'; + +class CommentsProvider extends ChangeNotifier { + // number of items call in each request + final pageItemNumber = 12; + + //reset provider data + void reset() { + comments = []; + nextPage = true; + stateCode = null; + } + + // state code of current request to defied error message + // like 400 customer request failed + // 500 service not available + int stateCode; + + // true if there is next page in product list and false if not + bool nextPage = true; + + // list of user requests + List comments = []; + + // when requests in-process _loading = true + // done _loading = true + // failed _loading = false + bool isLoading; + + /// return -2 if request in progress + /// return -1 if error happen when sending request + /// return state code if request complete may be 200, 404 or 403 + /// for more details check http state manager + /// lib\controllers\http_status_manger\http_status_manger.dart + Future getComments({@required String callId}) async { + if (isLoading == true) return -2; + isLoading = true; + notifyListeners(); + Response response; + try { + response = await ApiManager.instance.get(URLs.getComments + "?callRequestId=$callId"); + + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + List requestsListJson = json.decode(response.body)["data"]; + List commentsPage = requestsListJson.map((request) => Comment.fromJson(request)).toList(); + comments ??= []; + comments.addAll(commentsPage); + if (commentsPage.length == pageItemNumber) { + nextPage = true; + } else { + nextPage = false; + } + } + isLoading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + print(error); + isLoading = false; + stateCode = -1; + notifyListeners(); + return -1; + } + } + + /// return -2 if request in progress + /// return -1 if error happen when sending request + /// return state code if request complete may be 200, 404 or 403 + /// for more details check http state manager + /// lib\controllers\http_status_manger\http_status_manger.dart + Future addComment(BuildContext context, {@required Comment comment}) async { + if (isLoading == true) return -2; + isLoading = true; + Response response; + try { + comment.id = 0; + showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); + response = await ApiManager.instance.post(URLs.addComment, body: comment.toJson()); + + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + reset(); //visit.status = pentry.ppmVisitStatus; + notifyListeners(); + Fluttertoast.showToast(msg: context.translation.successfulRequestMessage); + Navigator.of(context).pop(); + } else { + Fluttertoast.showToast(msg: "${context.translation.failedToCompleteRequest} ${jsonDecode(response.body)["message"]}"); + } + isLoading = false; + notifyListeners(); + Navigator.of(context).pop(); + return response.statusCode; + } catch (error) { + print(error); + isLoading = false; + stateCode = -1; + notifyListeners(); + return -1; + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 7d312ff8..2eaafa3c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:localization/localization.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/api/asset_transfer_provider.dart'; +import 'package:test_sa/controllers/providers/api/comments_provider.dart'; import 'package:test_sa/controllers/providers/api/departments_provider.dart'; import 'package:test_sa/controllers/providers/api/devices_provider.dart'; import 'package:test_sa/controllers/providers/api/gas_refill_provider.dart'; @@ -185,6 +186,7 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => PPMVisitStatusProvider()), ChangeNotifierProvider(create: (_) => PentryTaskStatusProvider()), ChangeNotifierProvider(create: (_) => PPMDeviceStatusProvider()), + ChangeNotifierProvider(create: (_) => CommentsProvider()), ], child: GestureDetector( onTap: () { diff --git a/lib/models/comment.dart b/lib/models/comment.dart new file mode 100644 index 00000000..f0bebfd2 --- /dev/null +++ b/lib/models/comment.dart @@ -0,0 +1,75 @@ +class Comment { + Comment({ + this.id, + this.callRequestId, + this.createdOn, + this.createdBy, + this.comment, + }); + + Comment.fromJson(dynamic json) { + id = json['id']; + callRequestId = json['callRequestId']; + createdOn = json['createdOn']; + createdBy = json['createdBy'] != null ? CreatedBy.fromJson(json['createdBy']) : null; + comment = json['comment']; + } + num id; + num callRequestId; + String createdOn; + CreatedBy createdBy; + String comment; + Comment copyWith({ + num id, + num callRequestId, + String createdOn, + CreatedBy createdBy, + String comment, + }) => + Comment( + id: id ?? this.id, + callRequestId: callRequestId ?? this.callRequestId, + createdOn: createdOn ?? this.createdOn, + createdBy: createdBy ?? this.createdBy, + comment: comment ?? this.comment, + ); + Map toJson() { + final map = {}; + map['id'] = id; + map['callRequestId'] = callRequestId; + map['createdOn'] = createdOn; + if (createdBy != null) { + map['createdBy'] = createdBy.toJson(); + } + map['comment'] = comment; + return map; + } +} + +class CreatedBy { + CreatedBy({ + this.userId, + this.userName, + }); + + CreatedBy.fromJson(dynamic json) { + userId = json['userId']; + userName = json['userName']; + } + String userId; + String userName; + CreatedBy copyWith({ + String userId, + String userName, + }) => + CreatedBy( + userId: userId ?? this.userId, + userName: userName ?? this.userName, + ); + Map toJson() { + final map = {}; + map['userId'] = userId; + map['userName'] = userName; + return map; + } +} diff --git a/lib/views/pages/user/requests/comments_bottom_sheet.dart b/lib/views/pages/user/requests/comments_bottom_sheet.dart new file mode 100644 index 00000000..c330cee3 --- /dev/null +++ b/lib/views/pages/user/requests/comments_bottom_sheet.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/controllers/providers/api/comments_provider.dart'; +import 'package:test_sa/controllers/providers/api/user_provider.dart'; +import 'package:test_sa/controllers/validator/validator.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/models/comment.dart'; +import 'package:test_sa/models/enums/user_types.dart'; +import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; +import 'package:test_sa/views/widgets/loaders/lazy_loading.dart'; +import 'package:test_sa/views/widgets/loaders/no_item_found.dart'; + +import '../../../../new_views/app_style/app_color.dart'; +import '../../../widgets/loaders/loading_manager.dart'; + +class CommentsBottomSheet extends StatefulWidget { + final String requestId; + const CommentsBottomSheet({Key key, @required this.requestId}) : super(key: key); + + @override + State createState() => _CommentsBottomSheetState(); +} + +class _CommentsBottomSheetState extends State { + final GlobalKey _formKey = GlobalKey(); + String text; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final commentsProvider = Provider.of(context, listen: false); + final userProvider = Provider.of(context, listen: false); + return Container( + height: MediaQuery.of(context).size.height * 0.55, + clipBehavior: Clip.antiAlias, + margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: const BorderRadius.only(topRight: Radius.circular(20), topLeft: Radius.circular(20)), + ), + padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight), + child: Form( + key: _formKey, + child: LoadingManager( + isLoading: commentsProvider.isLoading, + isFailedLoading: commentsProvider.comments == null, + stateCode: commentsProvider.stateCode, + onRefresh: () async { + commentsProvider.reset(); + await commentsProvider.getComments(callId: widget.requestId); + }, + child: Column( + children: [ + Container( + width: 40.toScreenWidth, + height: 5.toScreenHeight, + decoration: BoxDecoration(color: AppColor.neutral40, borderRadius: BorderRadius.circular(30)), + ), + Align( + alignment: AlignmentDirectional.centerStart, + child: context.translation.comments.heading3(context).custom(fontWeight: FontWeight.w600).paddingOnly(top: 16, bottom: 16), + ), + commentsProvider.comments.isEmpty + ? NoItemFound(message: context.translation.noServiceRequestFound).expanded + : LazyLoading( + nextPage: commentsProvider.nextPage, + onLazyLoad: () async => await commentsProvider.getComments(callId: widget.requestId), + child: ListView.separated( + itemCount: commentsProvider.comments.length, + separatorBuilder: (cxt, index) => 8.height, + itemBuilder: (context, itemIndex) { + final model = commentsProvider.comments[itemIndex]; + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + (model?.createdBy?.userName ?? "Nurse").heading6(context), + 8.height, + (model?.comment ?? "").bodyText(context), + 8.height, + Align( + alignment: AlignmentDirectional.bottomEnd, + child: DateTime.tryParse(model.createdOn).toIso8601String().toServiceRequestDetailsFormat.tinyFont(context), + ), + ], + ).paddingAll(16), + ); + }, + ), + ).expanded, + if (userProvider.user.type == UsersTypes.normal_user) 16.height, + if (userProvider.user.type == UsersTypes.normal_user) + AppTextFormField( + labelText: "Type any comment", + backgroundColor: AppColor.neutral30, + alignLabelWithHint: true, + validator: (value) => Validator.hasValue(value) ? null : context.translation.requiredField, + textInputType: TextInputType.multiline, + suffixIcon: "comment_send".toSvgAsset().paddingOnly(end: 16).onPress(() { + if (_formKey.currentState.validate()) { + _formKey.currentState.save(); + final comment = Comment(id: 0, callRequestId: num.tryParse(widget.requestId ?? ""), comment: text); + commentsProvider.addComment(context, comment: comment); + } + }), + onSaved: (value) { + text = value; + }, + ), + 16.height, + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/pages/user/requests/service_request_details.dart b/lib/views/pages/user/requests/service_request_details.dart index 0ce5b97c..83349e5e 100644 --- a/lib/views/pages/user/requests/service_request_details.dart +++ b/lib/views/pages/user/requests/service_request_details.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; +import 'package:test_sa/controllers/providers/api/comments_provider.dart'; import 'package:test_sa/controllers/providers/api/service_requests_provider.dart'; import 'package:test_sa/controllers/providers/api/user_provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; @@ -15,6 +16,7 @@ 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/app_style/colors.dart'; import 'package:test_sa/views/app_style/sizing.dart'; +import 'package:test_sa/views/pages/user/requests/comments_bottom_sheet.dart'; import 'package:test_sa/views/pages/user/requests/update_service_request_page.dart'; import 'package:test_sa/views/pages/user/requests/work_order/work_orders_list_page.dart'; import 'package:test_sa/views/widgets/images/files_list.dart'; @@ -24,12 +26,23 @@ import 'package:test_sa/views/widgets/sound/sound_player.dart'; import 'first_action_bottom_sheet.dart'; -class ServiceRequestDetailsPage extends StatelessWidget { +class ServiceRequestDetailsPage extends StatefulWidget { static const String id = "/call-details"; ServiceRequest serviceRequest; ServiceRequestDetailsPage({Key key, this.serviceRequest}) : super(key: key); + @override + State createState() => _ServiceRequestDetailsPageState(); +} + +class _ServiceRequestDetailsPageState extends State { + @override + void didChangeDependencies() { + Provider.of(context, listen: false).reset(); + super.didChangeDependencies(); + } + @override Widget build(BuildContext context) { UserProvider _userProvider = Provider.of(context); @@ -61,31 +74,31 @@ class ServiceRequestDetailsPage extends StatelessWidget { Row( children: [ StatusLabel( - label: serviceRequest.priority.name, - id: serviceRequest.priority.id, - textColor: AColors.getPriorityStatusTextColor(serviceRequest.priority.id), - backgroundColor: AColors.getPriorityStatusColor(serviceRequest.priority.id)), + label: widget.serviceRequest.priority.name, + id: widget.serviceRequest.priority.id, + textColor: AColors.getPriorityStatusTextColor(widget.serviceRequest.priority.id), + backgroundColor: AColors.getPriorityStatusColor(widget.serviceRequest.priority.id)), 8.width, StatusLabel( - label: serviceRequest.statusLabel, - textColor: AColors.getRequestStatusTextColor(serviceRequest.statusValue), - backgroundColor: AColors.getRequestStatusColor(serviceRequest.statusValue)), + label: widget.serviceRequest.statusLabel, + textColor: AColors.getRequestStatusTextColor(widget.serviceRequest.statusValue), + backgroundColor: AColors.getRequestStatusColor(widget.serviceRequest.statusValue)), 1.width.expanded, ], ), 8.height, - Text(serviceRequest.deviceEnName, style: AppTextStyles.heading5.copyWith(color: const Color(0xFF3B3D4A))), + Text(widget.serviceRequest.deviceEnName, style: AppTextStyles.heading5.copyWith(color: const Color(0xFF3B3D4A))), 8.height, Text( - '${context.translation.assetNumber}: ${serviceRequest.device.assetNumber}', + '${context.translation.assetNumber}: ${widget.serviceRequest.device.assetNumber}', style: AppTextStyles.bodyText.copyWith(color: Color(0xFF757575)), ), Text( - 'Request Type: ${serviceRequest.type.name}', + 'Request Type: ${widget.serviceRequest.type.name}', style: AppTextStyles.bodyText.copyWith(color: Color(0xFF757575)), ), Text( - 'Request No: ${serviceRequest.requestCode}', + 'Request No: ${widget.serviceRequest.requestCode}', style: AppTextStyles.bodyText.copyWith(color: Color(0xFF757575)), ), ], @@ -95,10 +108,10 @@ class ServiceRequestDetailsPage extends StatelessWidget { children: [ if (_userProvider.user.type == UsersTypes.normal_user) "edit".toSvgAsset(width: 48).onPress(() { - Navigator.push(context, MaterialPageRoute(builder: (context) => UpdateServiceRequestPage(serviceRequest: serviceRequest))); + Navigator.push(context, MaterialPageRoute(builder: (context) => UpdateServiceRequestPage(serviceRequest: widget.serviceRequest))); }), if (_userProvider.user.type == UsersTypes.engineer) 16.height, - Text(serviceRequest.date.toServiceRequestCardFormat, textAlign: TextAlign.end, style: AppTextStyles.tinyFont.copyWith(color: const Color(0xFF3B3D4A))), + Text(widget.serviceRequest.date.toServiceRequestCardFormat, textAlign: TextAlign.end, style: AppTextStyles.tinyFont.copyWith(color: const Color(0xFF3B3D4A))), ], ) ], @@ -107,31 +120,31 @@ class ServiceRequestDetailsPage extends StatelessWidget { const Divider(color: Color(0xFFEAF1F4), height: 1, thickness: 1), 8.height, Text( - 'Manufacture: ${serviceRequest.device.modelDefinition.manufacturerName}', + 'Manufacture: ${widget.serviceRequest.device.modelDefinition.manufacturerName}', style: AppTextStyles.bodyText.copyWith(color: Color(0xFF757575)), ), Text( - 'Model: ${serviceRequest.device.modelDefinition.modelName}', + 'Model: ${widget.serviceRequest.device.modelDefinition.modelName}', style: AppTextStyles.bodyText.copyWith(color: Color(0xFF757575)), ), 8.height, - if ((serviceRequest.callComments ?? "").isNotEmpty) ...[ + if ((widget.serviceRequest.callComments ?? "").isNotEmpty) ...[ const Divider(color: Color(0xFFEAF1F4), height: 1, thickness: 1), 8.height, Text( - serviceRequest.callComments, + widget.serviceRequest.callComments, style: AppTextStyles.bodyText.copyWith(color: Color(0xFF757575)), ), ], - if (serviceRequest.devicePhotos.isNotEmpty) ...[ + if (widget.serviceRequest.devicePhotos.isNotEmpty) ...[ 8.height, const Divider(color: Color(0xFFEAF1F4), height: 1, thickness: 1), - FilesList(images: serviceRequest.devicePhotos), + FilesList(images: widget.serviceRequest.devicePhotos), ], - if (serviceRequest.audio?.isNotEmpty ?? false) ...[ + if (widget.serviceRequest.audio?.isNotEmpty ?? false) ...[ const Divider(color: Color(0xFFEAF1F4), height: 1, thickness: 1), 16.height, - ASoundPlayer(audio: serviceRequest.audio), + ASoundPlayer(audio: widget.serviceRequest.audio), ], ], ).paddingAll(16), @@ -186,16 +199,13 @@ class ServiceRequestDetailsPage extends StatelessWidget { ], ).paddingOnly(bottom: 16, start: 16, end: 16)) .onPress(() async { - // todo 'sikander' add comment bottom sheet - // await showModalBottomSheet( - // context: context, - // useSafeArea: true, - // isScrollControlled: true, - // backgroundColor: Colors.transparent, - // builder: (context) => FirstActionBottomSheet( - // onPressed: () {}, - // ), - // ); + await showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => CommentsBottomSheet(requestId: widget.serviceRequest.id), + ); }), ], ), @@ -207,12 +217,12 @@ class ServiceRequestDetailsPage extends StatelessWidget { backgroundColor: const Color(0xfff8f9fb), body: SafeArea( child: FutureBuilder( - future: _serviceRequestsProvider.getServiceRequestObjectById(requestId: serviceRequest.id), + future: _serviceRequestsProvider.getServiceRequestObjectById(requestId: widget.serviceRequest.id), builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const ALoading(); } else if (snap.hasData) { - serviceRequest = snap.data; + final serviceRequest = snap.data; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [