loan improvements

design_3.0_loan_module
Sikander Saleem 2 days ago
parent 35e494863f
commit 67da263e0d

@ -393,4 +393,5 @@ class URLs {
static get getLoanByWorkOrderId => '$_baseUrl/Loan/GetLoanByWorkOrderId'; static get getLoanByWorkOrderId => '$_baseUrl/Loan/GetLoanByWorkOrderId';
static get addLoan => '$_baseUrl/Loan/AddLoan'; static get addLoan => '$_baseUrl/Loan/AddLoan';
static get loanWorkflowAction => '$_baseUrl/Loan/LoanWorkflowAction';
} }

@ -1,12 +1,13 @@
import 'dart:io'; import 'dart:io';
class GenericAttachmentModel { class GenericAttachmentModel {
GenericAttachmentModel({this.id, this.name,this.originalName,this.createdBy}); GenericAttachmentModel({this.id, this.name, this.originalName, this.createdBy});
int? id; int? id;
String? name; String? name;
String ?createdBy; String? createdBy;
String? originalName; String? originalName;
GenericAttachmentModel.fromJson(Map<String, dynamic> json) { GenericAttachmentModel.fromJson(Map<String, dynamic> json) {
print('created by here is ${json['createdBy']}'); print('created by here is ${json['createdBy']}');
id = json['id']; id = json['id'];

@ -30,7 +30,7 @@ class LoanFormModel {
LoanFormModel({ LoanFormModel({
this.docName, this.docName,
this.isNewVendor=false, this.isNewVendor = false,
this.docNumber, this.docNumber,
this.docEmail, this.docEmail,
this.itemDescription, this.itemDescription,
@ -118,7 +118,9 @@ class LoanAttachments {
num? loanId; num? loanId;
String? attachmentName; String? attachmentName;
String? attachmentDescription; String? attachmentDescription;
LoanAttachments({this.id, this.loanAttachmentTypeId, this.attachmentName, this.loanId, this.attachmentDescription}); LoanAttachments({this.id, this.loanAttachmentTypeId, this.attachmentName, this.loanId, this.attachmentDescription});
LoanAttachments.fromJson(dynamic json) { LoanAttachments.fromJson(dynamic json) {
id = json['id']; id = json['id'];
loanAttachmentTypeId = json['loanAttachmentTypeId']; loanAttachmentTypeId = json['loanAttachmentTypeId'];

@ -13,6 +13,9 @@ class LoanInstallationPullOutFormModel {
DateTime? startTime; DateTime? startTime;
DateTime? endTime; DateTime? endTime;
double? totalHours; double? totalHours;
int? loanId;
String? userId;
int? loanStatusId;
LoanInstallationPullOutFormModel({ LoanInstallationPullOutFormModel({
this.loanAttachment, this.loanAttachment,
@ -23,18 +26,71 @@ class LoanInstallationPullOutFormModel {
this.startTime, this.startTime,
this.endTime, this.endTime,
this.totalHours, this.totalHours,
this.loanId,
this.loanStatusId,
this.userId,
}); });
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
//Need to check payload parm they need //Need to check payload parm they need
return { return {
'snNo': snNo, // 'snNo': snNo,
'InstallationDate': date?.toIso8601String(), 'serialNumber': snNo,
'installationDate': date?.toIso8601String(),
'pulloutDate': date?.toIso8601String(),
"loanAttachments": loanAttachment != null ? loanAttachment!.map((v) => v.toJson()).toList() : [], "loanAttachments": loanAttachment != null ? loanAttachment!.map((v) => v.toJson()).toList() : [],
"installationSignature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null,
"signature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null, "signature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null,
"pulloutSignature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null,
'startTime': startTime?.toIso8601String(), 'startTime': startTime?.toIso8601String(),
'endTime': endTime?.toIso8601String(), 'endTime': endTime?.toIso8601String(),
'totalHours': totalHours, 'totalHours': totalHours,
'loanId': loanId,
'loanStatusId': loanStatusId,
'userId': userId,
'isFromMobile': true,
};
}
Map<String, dynamic> toInstallationJson() {
//Need to check payload parm they need
return {
// 'snNo': snNo,
'serialNumber': snNo,
'installationDate': date?.toIso8601String(),
// 'pulloutDate': date?.toIso8601String(),
"loanAttachments": loanAttachment != null ? loanAttachment!.map((v) => v.toJson()).toList() : [],
"installationSignature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null,
// "signature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null,
// "pulloutSignature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null,
'startTime': startTime?.toIso8601String(),
'endTime': endTime?.toIso8601String(),
'totalHours': totalHours,
'loanId': loanId,
'loanStatusId': loanStatusId,
'userId': userId,
'isFromMobile': true,
};
}
Map<String, dynamic> toPulloutJson() {
//Need to check payload parm they need
return {
// 'snNo': snNo,
'serialNumber': snNo,
// 'installationDate': date?.toIso8601String(),
'pulloutDate': date?.toIso8601String(),
"loanAttachments": loanAttachment != null ? loanAttachment!.map((v) => v.toJson()).toList() : [],
// "installationSignature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null,
// "signature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null,
"pulloutSignature": signature != null ? "${DateTime.now().toIso8601String()}.png|${base64Encode(signature!)}" : null,
'startTime': startTime?.toIso8601String(),
'endTime': endTime?.toIso8601String(),
'totalHours': totalHours,
'loanId': loanId,
'loanStatusId': loanStatusId,
'userId': userId,
'isFromMobile': true,
}; };
} }
} }

@ -55,6 +55,8 @@ class LoanRequestModel {
int? pulloutWOItemId; int? pulloutWOItemId;
int? id; int? id;
String? installationDate; String? installationDate;
String? startTime;
String? endTime;
String? pulloutDate; String? pulloutDate;
String? createdBy; String? createdBy;
String? createdDate; String? createdDate;
@ -114,6 +116,8 @@ class LoanRequestModel {
this.id, this.id,
this.createdBy, this.createdBy,
this.installationDate, this.installationDate,
this.startTime,
this.endTime,
this.pulloutDate, this.pulloutDate,
this.createdDate, this.createdDate,
this.modifiedBy, this.modifiedBy,
@ -177,6 +181,8 @@ class LoanRequestModel {
pulloutWOItemId = json['pulloutWOItemId']; pulloutWOItemId = json['pulloutWOItemId'];
id = json['id']; id = json['id'];
installationDate = json['installationDate']; installationDate = json['installationDate'];
startTime = json['startTime'];
endTime = json['endTime'];
pulloutDate = json['pulloutDate']; pulloutDate = json['pulloutDate'];
createdBy = json['createdBy']; createdBy = json['createdBy'];
createdDate = json['createdDate']; createdDate = json['createdDate'];
@ -239,6 +245,8 @@ class LoanRequestModel {
data['pulloutWOItemId'] = this.pulloutWOItemId; data['pulloutWOItemId'] = this.pulloutWOItemId;
data['id'] = this.id; data['id'] = this.id;
data['installationDate'] = this.installationDate; data['installationDate'] = this.installationDate;
data['startTime'] = this.startTime;
data['endTime'] = this.endTime;
data['pulloutDate'] = this.pulloutDate; data['pulloutDate'] = this.pulloutDate;
data['createdBy'] = this.createdBy; data['createdBy'] = this.createdBy;
data['createdDate'] = this.createdDate; data['createdDate'] = this.createdDate;

@ -0,0 +1,223 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:provider/provider.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/helper/utils.dart';
import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/timer_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/modules/loan_module/models/loan_form_model.dart';
import 'package:test_sa/modules/loan_module/models/loan_installation_pullout_form_model.dart';
import 'package:test_sa/modules/loan_module/models/loan_request_model.dart';
import 'package:test_sa/modules/loan_module/provider/loan_provider.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_text_form_field.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/date_and_time/date_picker.dart';
import 'package:test_sa/views/widgets/e_signature/e_signature.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
import 'package:test_sa/views/widgets/timer/app_timer.dart';
class InstallationFormView extends StatefulWidget {
LoanRequestModel? loanData;
bool isInstallation;
InstallationFormView({Key? key, this.loanData, this.isInstallation = true}) : super(key: key);
@override
_InstallationFormViewState createState() {
return _InstallationFormViewState();
}
}
class _InstallationFormViewState extends State<InstallationFormView> {
final _formKey = GlobalKey<FormState>();
List<GenericAttachmentModel> _attachments = [];
final LoanInstallationPullOutFormModel _formData = LoanInstallationPullOutFormModel();
@override
void initState() {
super.initState();
_formData.loanId = widget.loanData?.id;
}
@override
void dispose() {
super.dispose();
}
void updateTimer({TimerModel? timer}) {
if (timer?.startAt != null && timer?.endAt != null) {
final start = timer!.startAt!;
final end = timer.endAt!;
final difference = end.difference(start);
final totalHours = difference.inSeconds / 3600.0;
_formData.startTime = start;
_formData.endTime = end;
_formData.totalHours = totalHours;
}
if (timer == null) {
_formData.startTime = null;
_formData.endTime = null;
_formData.totalHours = null;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: DefaultAppBar(title: 'Installation Report'.addTranslation),
body: Form(
key: _formKey,
child: Column(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.isInstallation) ...[
AppTextFormField(
initialValue: "",
labelText: "Updates SN",
validator: (value) {
if ((value ?? "").isEmpty) return "Mandatory";
return null;
},
style: Theme.of(context).textTheme.titleMedium,
backgroundColor: AppColor.fieldBgColor(context),
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)),
showShadow: false,
textInputType: TextInputType.text,
textInputAction: TextInputAction.next,
onChange: (value) {
_formData.snNo = value;
},
),
8.height,
],
ADatePicker(
label: widget.isInstallation ? "Installation Date".addTranslation : "Validation Date".addTranslation,
hideShadow: true,
backgroundColor: AppColor.fieldBgColor(context),
to: DateTime.now().add(const Duration(days: 365)),
formatDateWithTime: true,
date: _formData.date,
onDatePicker: (DateTime date) async {
final time = await showTimePicker(context: context, initialTime: TimeOfDay.now());
if (time == null) return;
setState(() {
_formData.date = DateTime(date.year, date.month, date.day, time.hour, time.minute);
});
},
),
8.height,
AppTimer(
label: context.translation.workingHours,
timer: _formData.timerModel,
// pickerFromDate: DateTime.tryParse(widget.createdDate ?? ''),
pickerFromDate: DateTime(DateTime.now().year, DateTime.now().month, 1),
pickerTimer: _formData.timerModel,
showTimer: false,
onPick: (timer) {
updateTimer(timer: timer);
},
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(10),
),
timerProgress: (isRunning) {},
onChange: (timer) async {
updateTimer(timer: timer);
return true;
},
),
ESignature(
title: 'End-user signature'.addTranslation,
backgroundColor: AppColor.fieldBgColor(context),
showShadow: false,
oldSignature: '',
newSignature: _formData.signature,
onChange: (Uint8List signature) {
if (signature.isEmpty) return;
setState(() {
_formData.signature = signature;
});
},
),
24.height,
"Attachments".bodyText(context).custom(color: AppColor.black10),
8.height,
AttachmentPicker(
label: context.translation.attachments,
attachment: _attachments,
buttonColor: AppColor.primary10,
onlyImages: false,
showAsListView: true,
buttonIcon: 'attachment_icon'.toSvgAsset(color: AppColor.primary10),
onChange: (attachments) {
_attachments = attachments;
setState(() {});
},
),
],
).toShadowContainer(context, borderRadius: 20),
).expanded,
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
buttonColor: AppColor.primary10,
label: context.translation.submitRequest,
onPressed: _submitForm,
),
),
],
),
),
);
}
Future<void> _submitForm() async {
if (!_formKey.currentState!.validate()) return;
if (_formData.date == null) {
"Please select installation date".showToast;
return;
}
if (_formData.startTime == null || _formData.endTime == null) {
"Please select start and end time".showToast;
return;
}
if (_formData.signature == null || _formData.signature!.isEmpty) {
"Signature is required".showToast;
return;
}
_formData.loanAttachment = [];
for (var item in _attachments) {
String fileName = CMRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? '';
_formData.loanAttachment!.add(LoanAttachments(id: 0, attachmentName: fileName, loanId: widget.loanData?.id));
}
Utils.showLoading(context);
_formData.loanStatusId = widget.loanData?.loanStatusValue;
_formData.userId = context.userProvider.user?.userID;
LoanProvider incidentProvider = Provider.of<LoanProvider>(context, listen: false);
bool isSuccess = await incidentProvider.loanWorkflowAction(_formData.toInstallationJson());
Utils.hideLoading(context);
if (isSuccess) {
Navigator.pop(context, true);
}
}
}

@ -1,16 +1,24 @@
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.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/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/generic_attachment_model.dart'; import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/timer_model.dart'; import 'package:test_sa/models/timer_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/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/loan_module/models/loan_form_model.dart';
import 'package:test_sa/modules/loan_module/models/loan_installation_pullout_form_model.dart'; import 'package:test_sa/modules/loan_module/models/loan_installation_pullout_form_model.dart';
import 'package:test_sa/modules/loan_module/models/loan_request_model.dart';
import 'package:test_sa/modules/loan_module/provider/loan_provider.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/new_views/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
@ -21,10 +29,10 @@ import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
import 'package:test_sa/views/widgets/timer/app_timer.dart'; import 'package:test_sa/views/widgets/timer/app_timer.dart';
class InstallationPullOutFormView extends StatefulWidget { class InstallationPullOutFormView extends StatefulWidget {
final bool isPullout;
final String? createdDate; final String? createdDate;
final LoanRequestModel? loanData;
const InstallationPullOutFormView({super.key, required this.isPullout, this.createdDate}); const InstallationPullOutFormView({super.key, this.createdDate, this.loanData});
@override @override
State<InstallationPullOutFormView> createState() => _InstallationPullOutFormViewState(); State<InstallationPullOutFormView> createState() => _InstallationPullOutFormViewState();
@ -34,7 +42,16 @@ class _InstallationPullOutFormViewState extends State<InstallationPullOutFormVie
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _snController = TextEditingController(); final _snController = TextEditingController();
List<GenericAttachmentModel> _attachments = []; List<GenericAttachmentModel> _attachments = [];
final _formData = LoanInstallationPullOutFormModel(); final LoanInstallationPullOutFormModel _formData = LoanInstallationPullOutFormModel();
@override
void initState() {
// TODO: implement initState
super.initState();
_formData.loanId = widget.loanData?.id;
_formData.loanStatusId = widget.loanData?.loanStatusValue;
_formData.userId = context.userProvider.user?.userID;
}
@override @override
void dispose() { void dispose() {
@ -66,23 +83,6 @@ class _InstallationPullOutFormViewState extends State<InstallationPullOutFormVie
}); });
} }
void _submitForm() {
if (!_formKey.currentState!.validate()) return;
//Validation if required .
// if (_formData.date == null) {
// context.showSnackBar('Please select installation date');
// return;
// }
//
// if (_formData.signature == null || _formData.signature!.isEmpty) {
// context.showSnackBar('Signature is required');
// return;
// }
_formData.snNo = _snController.text;
}
void updateTimer({TimerModel? timer}) { void updateTimer({TimerModel? timer}) {
if (timer?.startAt != null && timer?.endAt != null) { if (timer?.startAt != null && timer?.endAt != null) {
final start = timer!.startAt!; final start = timer!.startAt!;
@ -98,90 +98,132 @@ class _InstallationPullOutFormViewState extends State<InstallationPullOutFormVie
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: widget.isPullout ? null : DefaultAppBar(title: 'Installation Report'.addTranslation), // appBar: DefaultAppBar(title: 'Pullout Report'.addTranslation),
body: SafeArea( body: Form(
child: Form( key: _formKey,
key: _formKey, child: Column(
child: Column( children: [
children: [ SingleChildScrollView(
SingleChildScrollView( padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ ADatePicker(
if (!widget.isPullout) ...[ label: "Validation Date".addTranslation,
AppTextFormField( hideShadow: true,
controller: _snController, backgroundColor: AppColor.fieldBgColor(context),
labelText: context.translation.snNo, to: DateTime.now().add(const Duration(days: 365)),
backgroundColor: AppColor.fieldBgColor(context), formatDateWithTime: true,
textAlign: TextAlign.center, date: _formData.date,
labelStyle: AppTextStyles.textFieldLabelStyle, onDatePicker: (DateTime date) async {
hintStyle: AppTextStyles.textFieldLabelStyle, final time = await showTimePicker(context: context, initialTime: TimeOfDay.now());
textInputType: TextInputType.text, if (time == null) return;
textInputAction: TextInputAction.next, setState(() {
showShadow: false, _formData.date = DateTime(date.year, date.month, date.day, time.hour, time.minute);
validator: (v) => (v == null || v.isEmpty) ? 'Required' : null, });
), },
8.height, ),
], // ADatePicker(
ADatePicker( // label: !widget.isPullout ? "Installation Date".addTranslation : "Validation Date".addTranslation,
label: !widget.isPullout ? "Installation Date".addTranslation : "Validation Date".addTranslation, // hideShadow: true,
hideShadow: true, // backgroundColor: AppColor.fieldBgColor(context),
backgroundColor: AppColor.fieldBgColor(context), // to: DateTime.now().add(const Duration(days: 365)),
to: DateTime.now().add(const Duration(days: 365)), // formatDateWithTime: true,
formatDateWithTime: true, // onDatePicker: _pickInstallationDate,
onDatePicker: _pickInstallationDate, // ),
), 8.height,
8.height, AppTimer(
AppTimer( label: context.translation.workingHours,
label: context.translation.workingHours, timer: _formData.timerModel,
timer: _formData.timerModel, // pickerFromDate: DateTime.tryParse(widget.createdDate ?? ''),
// pickerFromDate: DateTime.tryParse(widget.createdDate ?? ''), pickerFromDate: DateTime(DateTime.now().year, DateTime.now().month, 1),
pickerFromDate: DateTime(DateTime.now().year, DateTime.now().month, 1), pickerTimer: _formData.timerModel,
pickerTimer: _formData.timerModel, showTimer: false,
showTimer: false, onPick: (timer) {
onPick: (timer) { updateTimer(timer: timer);
updateTimer(timer: timer); },
}, width: double.infinity,
width: double.infinity, decoration: BoxDecoration(
decoration: BoxDecoration( color: AppColor.fieldBgColor(context),
color: AppColor.fieldBgColor(context), borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10),
),
timerProgress: (isRunning) {},
onChange: (timer) async {
updateTimer(timer: timer);
return true;
},
),
ESignature(
title: 'End-user signature'.addTranslation,
backgroundColor: AppColor.fieldBgColor(context),
showShadow: false,
oldSignature: '',
newSignature: _formData.signature,
onChange: _onSignatureChanged,
),
24.height,
AttachmentPicker(
label: context.translation.attachments,
attachment: _attachments,
), ),
], timerProgress: (isRunning) {},
).toShadowContainer(context, borderRadius: 20), onChange: (timer) async {
).expanded, updateTimer(timer: timer);
FooterActionButton.footerContainer( return true;
context: context, },
child: AppFilledButton( ),
buttonColor: AppColor.primary10, ESignature(
label: context.translation.submitRequest, title: 'End-user signature'.addTranslation,
onPressed: _submitForm, backgroundColor: AppColor.fieldBgColor(context),
), showShadow: false,
oldSignature: '',
newSignature: _formData.signature,
onChange: _onSignatureChanged,
),
24.height,
"Attachments".bodyText(context).custom(color: AppColor.black10),
8.height,
AttachmentPicker(
label: context.translation.attachments,
attachment: _attachments,
buttonColor: AppColor.primary10,
onlyImages: false,
showAsListView: true,
buttonIcon: 'attachment_icon'.toSvgAsset(color: AppColor.primary10),
onChange: (attachments) {
_attachments = attachments;
setState(() {});
},
),
// AttachmentPicker(
// label: context.translation.attachments,
// attachment: _attachments,
// ),
],
).toShadowContainer(context, borderRadius: 20),
).expanded,
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
buttonColor: AppColor.primary10,
label: context.translation.submitRequest,
onPressed: _submitForm,
), ),
], ),
), ],
), ),
), ),
); );
} }
Future<void> _submitForm() async {
if (!_formKey.currentState!.validate()) return;
if (_formData.date == null) {
"Please select Validation date".showToast;
return;
}
if (_formData.startTime == null || _formData.endTime == null) {
"Please select start and end time".showToast;
return;
}
if (_formData.signature == null || _formData.signature!.isEmpty) {
"Signature is required".showToast;
return;
}
_formData.loanAttachment = [];
for (var item in _attachments) {
String fileName = CMRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? '';
_formData.loanAttachment!.add(LoanAttachments(id: 0, attachmentName: fileName, loanId: widget.loanData?.id));
}
Utils.showLoading(context);
LoanProvider incidentProvider = Provider.of<LoanProvider>(context, listen: false);
bool isSuccess = await incidentProvider.loanWorkflowAction(_formData.toPulloutJson());
Utils.hideLoading(context);
if (isSuccess) {
Navigator.pop(context, true);
}
}
} }

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:test_sa/controllers/api_routes/urls.dart'; import 'package:test_sa/controllers/api_routes/urls.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/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/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/modules/loan_module/models/loan_request_model.dart'; import 'package:test_sa/modules/loan_module/models/loan_request_model.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/views/widgets/images/files_list.dart'; import 'package:test_sa/views/widgets/images/files_list.dart';
@ -16,72 +18,76 @@ class InstallationDetailsView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String workingTime = "-";
if (loanData.startTime != null && loanData.endTime != null) {
try {
final timeFormat = DateFormat('h:mm a'); // Matches "3:00 PM" format
final now = DateTime.now();
final start = timeFormat.parse(loanData.startTime!);
var end = timeFormat.parse(loanData.endTime!);
// Set the date to today for both times
var startDateTime = DateTime(now.year, now.month, now.day, start.hour, start.minute);
var endDateTime = DateTime(now.year, now.month, now.day, end.hour, end.minute);
// If end time is before start time, shift crossed midnight
if (endDateTime.isBefore(startDateTime)) {
endDateTime = endDateTime.add(const Duration(days: 1));
}
double totalWorkingTime = endDateTime.difference(startDateTime).inSeconds / 3600.0;
} catch (ex) {}
}
// if (loanData.startTime != null && loanData.endTime != null) {
// final start = DateTime.parse(loanData.startTime!);
// var end = DateTime.parse(loanData.endTime!);
//
// // If end time is before start time, it means the shift crossed midnight
// if (end.isBefore(start)) {
// end = end.add(const Duration(days: 1));
// }
//
// double totalWorkingTime = end.difference(start).inSeconds / 3600.0;
// workingTime = CMRequestUtils.formatTotalWorkingHours(totalWorkingTime);
// }
// if (loanData.startTime != null && loanData.endTime != null) {
// final start = DateTime.parse(loanData.startTime!);
// final end = DateTime.parse(loanData.endTime!);
// double totalWorkingTime = end.difference(start).inSeconds / 3600.0;
// workingTime = CMRequestUtils.formatTotalWorkingHours(totalWorkingTime);
// }
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
mainAxisAlignment: MainAxisAlignment.spaceBetween, "Installation Details".addTranslation,
children: [ style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
detailContent(
context: context,
heading: 'Updates SN:',
label: loanData.installationDate??'-'
),
detailContent(
context: context,
heading: 'Installation Date',
label: loanData.installationDate??'-'
),
],
),
12.height,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
detailContent(
context: context,
heading: 'Start Time',
label: loanData.installationDate??'-'
),
detailContent(
context: context,
heading: 'End Time',
label: loanData.installationDate??'-'
),
],
),
12.height,
detailContent(
context: context,
heading: 'Total Time',
label: loanData.installationDate??'-'
), ),
4.height,
'Updates SN: ${loanData.assetSerialNumber ?? "-"}'.bodyText(context),
'Installation Date: ${loanData.installationDate?.toAssetDetailsFormat ?? "-"}'.bodyText(context),
'Start Time: ${loanData.startTime ?? "-"}'.bodyText(context),
'End Time: ${loanData.endTime ?? "-"}'.bodyText(context),
'Total Working Time: $workingTime'.bodyText(context),
if (loanData.loanAttachments!.isNotEmpty) ...[ if (loanData.loanAttachments!.isNotEmpty) ...[
12.height, 12.height,
Text( Text(
"Uploads Phase 2 docs".addTranslation, "Uploads Phase 2 docs".addTranslation,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50), style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
), ),
8.height, 8.height,
FilesList(images: loanData.loanAttachments?.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []), FilesList(showAsListView: true, images: loanData.loanAttachments?.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []),
], ],
], ],
).toShadowContainer(context, borderRadius: 20), ).toShadowContainer(context, borderRadius: 20),
); );
} }
Widget detailContent({required BuildContext context, required String label, required String heading}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
heading.bodyText(context).custom(color: AppColor.neutral120),
6.height,
//TODO Need to check which value need to show here
label.bodyText(context).custom(color: AppColor.neutral120),
],
);
}
} }

@ -8,6 +8,7 @@ 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/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/loan_module/models/loan_attachment_model.dart'; import 'package:test_sa/modules/loan_module/models/loan_attachment_model.dart';
import 'package:test_sa/modules/loan_module/pages/installation_form_view.dart';
import 'package:test_sa/modules/loan_module/pages/pullout_detail_page.dart'; import 'package:test_sa/modules/loan_module/pages/pullout_detail_page.dart';
import 'package:test_sa/modules/loan_module/provider/loan_provider.dart'; import 'package:test_sa/modules/loan_module/provider/loan_provider.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
@ -20,101 +21,118 @@ import 'package:test_sa/views/widgets/requests/request_status.dart';
import '../models/loan_request_model.dart'; import '../models/loan_request_model.dart';
import 'installation_pullout_form_view.dart'; import 'installation_pullout_form_view.dart';
class LoanEquipmentDetailPage extends StatelessWidget { class LoanEquipmentDetailPage extends StatefulWidget {
static const String id = "/loan-equipment-detail-page"; static const String id = "/loan-equipment-detail-page";
final int loanId; final int loanId;
LoanEquipmentDetailPage({Key? key, required this.loanId}) : super(key: key); LoanEquipmentDetailPage({Key? key, required this.loanId}) : super(key: key);
@override
_LoanEquipmentDetailPageState createState() {
return _LoanEquipmentDetailPageState();
}
}
class _LoanEquipmentDetailPageState extends State<LoanEquipmentDetailPage> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const DefaultAppBar(title: "Request Details"), appBar: const DefaultAppBar(title: "Request Details"),
body: SafeArea( body: FutureBuilder<LoanRequestModel?>(
child: FutureBuilder<LoanRequestModel?>( future: Provider.of<LoanProvider>(context, listen: false).getLoanById(widget.loanId),
future: Provider.of<LoanProvider>(context, listen: false).getLoanById(loanId), builder: (BuildContext context, AsyncSnapshot<LoanRequestModel?> snapshot) {
builder: (BuildContext context, AsyncSnapshot<LoanRequestModel?> snapshot) { if (snapshot.connectionState == ConnectionState.waiting) return const CircularProgressIndicator(color: AppColor.primary10).center;
if (snapshot.connectionState == ConnectionState.waiting) return const CircularProgressIndicator(color: AppColor.primary10).center; if (snapshot.data == null) return const NoDataFound().center;
if (snapshot.data == null) return const NoDataFound().center;
List<LoanAttachmentModel> allAttachments = snapshot.data!.loanAttachments!;
List<LoanAttachmentModel> allAttachments = snapshot.data!.loanAttachments!;
return Column(
return Column( children: [
children: [ ListView(
ListView( padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16), children: [
children: [ Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Row(
Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ StatusLabel(
StatusLabel( label: snapshot.data!.loanStatusName!,
label: snapshot.data!.loanStatusName!, textColor: AppColor.getRequestStatusTextColorByName(context, snapshot.data!.loanStatusName!),
textColor: AppColor.getRequestStatusTextColorByName(context, snapshot.data!.loanStatusName!), backgroundColor: AppColor.getRequestStatusColorByName(context, snapshot.data!.loanStatusName!),
backgroundColor: AppColor.getRequestStatusColorByName(context, snapshot.data!.loanStatusName!), ),
), 1.width.expanded,
1.width.expanded,
Text(
snapshot.data!.createdDate?.toServiceRequestCardFormat ?? "-",
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral50),
),
],
),
12.height,
...requesterDetails(context, snapshot.data!),
12.height,
...requestDetails(context, snapshot.data!),
12.height,
...assetDetails(context, snapshot.data!),
12.height,
...installationDetails(context, snapshot.data!),
12.height,
...doctorDetails(context, snapshot.data!),
12.height,
...vendorDetails(context, snapshot.data!),
if (allAttachments.isNotEmpty) ...[
const Divider().defaultStyle(context),
Text( Text(
"Attachments".addTranslation, snapshot.data!.createdDate?.toServiceRequestCardFormat ?? "-",
style: AppTextStyles.heading4.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50), textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral50),
), ),
FilesList(images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []),
], ],
),
12.height,
...requesterDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...requestDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...assetDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...installationDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...doctorDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...vendorDetails(context, snapshot.data!),
if (allAttachments.isNotEmpty) ...[
const Divider().defaultStyle(context),
Text(
"Attachments".addTranslation,
style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
),
FilesList(images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []),
], ],
).toShadowContainer(context, padding: 12), ],
], ).toShadowContainer(context, padding: 12),
).expanded, ],
// if (context.userProvider.isEngineer && (snapshot.data?.loanStatusValue == 5 || snapshot.data?.loanStatusValue == 8)) ).expanded,
// FooterActionButton.footerContainer( // @todo ask backend to add loanTypeValue, so we can show or hide the buttons
// context: context, if (snapshot.data?.loanTypeName == "Standard" && context.userProvider.isEngineer && (snapshot.data?.loanStatusValue == 5 || snapshot.data?.loanStatusValue == 8))
// child: AppFilledButton( FooterActionButton.footerContainer(
// onPressed: () async { context: context,
// if (snapshot.data?.loanStatusValue == 5) { child: AppFilledButton(
// Navigator.push( onPressed: () async {
// context, if (snapshot.data?.loanStatusValue == 5) {
// MaterialPageRoute( bool isSuccess = await Navigator.push(context, MaterialPageRoute(builder: (context) => InstallationFormView(loanData: snapshot.data))) ?? false;
// builder: (context) => PullOutDetailsPage( if (isSuccess) {
// loanData: snapshot.data, setState(() {});
// ))); }
// return; return;
// } }
// Navigator.push(context, MaterialPageRoute(builder: (context) => const InstallationPullOutFormView(isPullout: false))); bool isSuccess = await Navigator.push(context, MaterialPageRoute(builder: (context) => PullOutDetailsPage(loanData: snapshot.data))) ?? false;
// }, if (isSuccess) {
// label: snapshot.data?.loanStatusValue == 5 ? 'Installation Report' : 'PullOut Report', setState(() {});
// ), }
// ) },
], label: snapshot.data?.loanStatusValue == 5 ? 'Installation Report' : 'PullOut Report',
); ),
}), )
)); ],
);
}));
} }
List<Widget> requestDetails(BuildContext context, LoanRequestModel loanData) { List<Widget> requestDetails(BuildContext context, LoanRequestModel loanData) {
return [ return [
Text("Request Details", style: AppTextStyles.heading4.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)), Text("Request Details", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'Loan Type: ${loanData.loanTypeName ?? "-"}'.bodyText(context), 'Loan Type: ${loanData.loanTypeName ?? "-"}'.bodyText(context),
'Loan No: ${loanData.reqCode ?? "-"}'.bodyText(context), 'Loan No: ${loanData.reqCode ?? "-"}'.bodyText(context),
'Loan Period: ${loanData.loanPeriodName ?? "-"}'.bodyText(context), 'Loan Period: ${loanData.loanPeriodName ?? "-"}'.bodyText(context),
@ -127,7 +145,7 @@ class LoanEquipmentDetailPage extends StatelessWidget {
List<Widget> vendorDetails(BuildContext context, LoanRequestModel loanData) { List<Widget> vendorDetails(BuildContext context, LoanRequestModel loanData) {
return [ return [
Text("Vendor Information", style: AppTextStyles.heading4.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)), Text("Vendor Information", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'Name: ${loanData.vendorName ?? "-"}'.bodyText(context), 'Name: ${loanData.vendorName ?? "-"}'.bodyText(context),
'Representative Name: ${loanData.vendorRepName ?? "-"}'.bodyText(context), 'Representative Name: ${loanData.vendorRepName ?? "-"}'.bodyText(context),
'Contact: ${loanData.vendorContact ?? "-"}'.bodyText(context), 'Contact: ${loanData.vendorContact ?? "-"}'.bodyText(context),
@ -137,7 +155,7 @@ class LoanEquipmentDetailPage extends StatelessWidget {
List<Widget> assetDetails(BuildContext context, LoanRequestModel loanData) { List<Widget> assetDetails(BuildContext context, LoanRequestModel loanData) {
return [ return [
Text("Asset Information", style: AppTextStyles.heading4.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)), Text("Asset Information", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'${context.translation.assetName}: ${loanData.assetName ?? "-"}'.bodyText(context), '${context.translation.assetName}: ${loanData.assetName ?? "-"}'.bodyText(context),
'${context.translation.assetNo}: ${loanData.assetNumber ?? "-"}'.bodyText(context), '${context.translation.assetNo}: ${loanData.assetNumber ?? "-"}'.bodyText(context),
'${context.translation.serialNumber}: ${loanData.assetSerialNumber ?? "-"}'.bodyText(context), '${context.translation.serialNumber}: ${loanData.assetSerialNumber ?? "-"}'.bodyText(context),
@ -148,7 +166,7 @@ class LoanEquipmentDetailPage extends StatelessWidget {
List<Widget> installationDetails(BuildContext context, LoanRequestModel loanData) { List<Widget> installationDetails(BuildContext context, LoanRequestModel loanData) {
return [ return [
Text("Installation & Pullout Information", style: AppTextStyles.heading4.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)), Text("Installation & Pullout Information", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'Installation EDD: ${loanData.installationEDD?.toServiceRequestDetailsFormat ?? "-"}'.bodyText(context), 'Installation EDD: ${loanData.installationEDD?.toServiceRequestDetailsFormat ?? "-"}'.bodyText(context),
'Installation Date: ${loanData.installationEDD?.toAssetDetailsFormat ?? "-"}'.bodyText(context), 'Installation Date: ${loanData.installationEDD?.toAssetDetailsFormat ?? "-"}'.bodyText(context),
'Pullout EDD: ${loanData.pulloutEDD?.toServiceRequestDetailsFormat ?? "-"}'.bodyText(context), 'Pullout EDD: ${loanData.pulloutEDD?.toServiceRequestDetailsFormat ?? "-"}'.bodyText(context),
@ -158,7 +176,7 @@ class LoanEquipmentDetailPage extends StatelessWidget {
List<Widget> doctorDetails(BuildContext context, LoanRequestModel loanData) { List<Widget> doctorDetails(BuildContext context, LoanRequestModel loanData) {
return [ return [
Text("Doctor Information", style: AppTextStyles.heading4.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)), Text("Doctor Information", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'Name: ${loanData.doctorName ?? "-"}'.bodyText(context), 'Name: ${loanData.doctorName ?? "-"}'.bodyText(context),
'Contact: ${loanData.doctorContact ?? "-"}'.bodyText(context), 'Contact: ${loanData.doctorContact ?? "-"}'.bodyText(context),
'Email: ${loanData.doctorEmail ?? "-"}'.bodyText(context), 'Email: ${loanData.doctorEmail ?? "-"}'.bodyText(context),
@ -167,7 +185,7 @@ class LoanEquipmentDetailPage extends StatelessWidget {
List<Widget> requesterDetails(BuildContext context, LoanRequestModel loanData) { List<Widget> requesterDetails(BuildContext context, LoanRequestModel loanData) {
return [ return [
Text("Requester Information", style: AppTextStyles.heading4.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)), Text("Requester Information", style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
'Name: ${loanData.requestorUserIdForDisplay ?? "-"}'.bodyText(context), 'Name: ${loanData.requestorUserIdForDisplay ?? "-"}'.bodyText(context),
'Email: ${loanData.requestorUserEmail ?? "-"}'.bodyText(context), 'Email: ${loanData.requestorUserEmail ?? "-"}'.bodyText(context),
'Contact: ${loanData.requestorUserMobileNumber ?? "-"}'.bodyText(context), 'Contact: ${loanData.requestorUserMobileNumber ?? "-"}'.bodyText(context),

@ -10,29 +10,17 @@ import 'package:test_sa/modules/loan_module/pages/intallation_details_view.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/default_app_bar.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
class PullOutDetailsPage extends StatefulWidget { class PullOutDetailsPage extends StatelessWidget {
final LoanRequestModel? loanData; final LoanRequestModel? loanData;
PullOutDetailsPage({Key? key, required this.loanData}) : super(key: key); PullOutDetailsPage({Key? key, required this.loanData}) : super(key: key);
@override
_PullOutDetailsPageState createState() {
return _PullOutDetailsPageState();
}
}
class _PullOutDetailsPageState extends State<PullOutDetailsPage> {
@override
void dispose() {
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: 'Loan Request'.addTranslation, title: 'Pullout Report'.addTranslation,
), ),
body: DefaultTabController( body: DefaultTabController(
length: 2, length: 2,
@ -61,16 +49,15 @@ class _PullOutDetailsPageState extends State<PullOutDetailsPage> {
], ],
), ),
), ),
12.height, 0.height,
TabBarView( TabBarView(
children: [ children: [
Align( Column(
alignment: Alignment.topCenter, children: [
child: InstallationDetailsView( InstallationDetailsView(loanData: loanData!),
loanData: widget.loanData!, ],
),
), ),
const InstallationPullOutFormView(isPullout: true), InstallationPullOutFormView(loanData: loanData!),
], ],
).expanded, ).expanded,
], ],

@ -48,4 +48,16 @@ class LoanProvider extends ChangeNotifier {
} }
return loanData; return loanData;
} }
Future<bool> loanWorkflowAction(Map<String, dynamic> body) async {
try {
Response response = await ApiManager.instance.post(URLs.loanWorkflowAction, body: body, showToast: true);
if (response.statusCode >= 200 && response.statusCode < 300) {
return true;
}
return false;
} catch (error) {
return false;
}
}
} }

Loading…
Cancel
Save