invoice/lib/ui/client/client_pdf.dart

517 lines
18 KiB
Dart

// Dart imports:
import 'dart:async';
import 'dart:convert';
// Flutter imports:
import 'package:built_collection/built_collection.dart';
import 'package:flutter/material.dart';
// Package imports:
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_styled_toast/flutter_styled_toast.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/main_app.dart';
import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart';
import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart';
import 'package:invoiceninja_flutter/ui/app/forms/date_picker.dart';
import 'package:invoiceninja_flutter/ui/app/multiselect.dart';
import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart';
import 'package:invoiceninja_flutter/utils/files.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:printing/printing.dart';
// Project imports:
import 'package:invoiceninja_flutter/data/models/dashboard_model.dart';
import 'package:invoiceninja_flutter/data/web_client.dart';
import 'package:invoiceninja_flutter/redux/app/app_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/app_text_button.dart';
import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart';
import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart';
import 'package:invoiceninja_flutter/ui/client/client_pdf_vm.dart';
import 'package:invoiceninja_flutter/utils/dates.dart';
import 'package:invoiceninja_flutter/utils/dialogs.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/utils/platforms.dart';
class ClientPdfView extends StatefulWidget {
const ClientPdfView({
Key? key,
required this.viewModel,
this.showAppBar = true,
}) : super(key: key);
final ClientPdfVM viewModel;
final bool showAppBar;
@override
_ClientPdfViewState createState() => _ClientPdfViewState();
}
class _ClientPdfViewState extends State<ClientPdfView> {
bool _isLoading = false;
http.Response? _response;
//int _pageCount = 1;
//int _currentPage = 1;
DateRange _dateRange = DateRange.thisQuarter;
String? _startDate =
convertDateTimeToSqlDate(DateTime.now().subtract(Duration(days: 365)));
String? _endDate = convertDateTimeToSqlDate();
String _status = kStatementStatusAll;
//String? _designId;
@override
void didChangeDependencies() {
super.didChangeDependencies();
loadPDF();
}
void loadPDF({bool sendEmail = false}) {
if (_isLoading) {
return;
}
final localization = AppLocalization.of(context);
setState(() {
_isLoading = true;
});
_loadPDF(sendEmail: sendEmail).then((response) {
setState(() {
if (sendEmail) {
if (response!.statusCode >= 200) {
showToast(localization!.emailedStatement);
}
} else {
_response = response;
}
_isLoading = false;
});
}).catchError((Object error) {
setState(() {
_isLoading = false;
});
showDialog<void>(
context: navigatorKey.currentContext!,
builder: (BuildContext context) {
return ErrorDialog(error);
});
});
}
Future<Response?> _loadPDF(
{bool sendEmail = false, String designId = ''}) async {
final client = widget.viewModel.client!;
http.Response? response;
final store = StoreProvider.of<AppState>(context);
final state = store.state;
final webClient = WebClient();
String url = '${state.credentials.url}/client_statement';
if (sendEmail) {
url += '?send_email=true';
}
if (designId.isNotEmpty) {
url += '&design_id=$designId';
}
String? startDate = '';
String? endDate = '';
startDate = calculateStartDate(
company: state.company,
dateRange: _dateRange,
customStartDate: _startDate,
customEndDate: _endDate);
endDate = calculateEndDate(
company: state.company,
dateRange: _dateRange,
customStartDate: _startDate,
customEndDate: _endDate);
if (_dateRange != DateRange.custom) {
_startDate = startDate;
_endDate = endDate;
}
final includes = state.prefState.statementIncludes;
final data = json.encode({
'client_id': client.id,
'start_date': startDate,
'end_date': endDate,
'show_payments_table': includes.contains(kStatementIncludePayments),
'show_credits_table': includes.contains(kStatementIncludeCredits),
'show_aging_table': includes.contains(kStatementIncludeAging),
'status': _status,
});
response = await webClient.post(
url,
state.credentials.token,
data: data,
rawResponse: true,
);
return response;
}
@override
Widget build(BuildContext context) {
final store = StoreProvider.of<AppState>(context);
final state = store.state;
final localization = AppLocalization.of(context)!;
final client = widget.viewModel.client!;
/*
final designPicker = Expanded(
child: IgnorePointer(
ignoring: _isLoading,
child: DesignPicker(
initialValue: _designId,
onSelected: (design) {
setState(() {
_designId = design?.id;
loadPDF();
});
},
label: localization.design,
showBlank: true,
entityType: EntityType.client,
),
),
);
*/
final datePicker = Expanded(
child: AppDropdownButton<DateRange>(
labelText: localization.dateRange,
blankValue: null,
//showBlank: true,
value: _dateRange,
onChanged: (dynamic value) {
setState(() {
_dateRange = value;
});
if (value != DateRange.custom) {
loadPDF();
}
},
items: DateRange.values
.where((value) => value != DateRange.allTime)
.map((dateRange) => DropdownMenuItem<DateRange>(
child: Text(localization.lookup(dateRange.toString())),
value: dateRange,
))
.toList(),
),
);
final statusPicker = Expanded(
child: AppDropdownButton<String>(
labelText: localization.status,
blankValue: null,
value: _status,
onChanged: (dynamic value) {
setState(() {
_status = value;
});
loadPDF();
},
items: [
kStatementStatusAll,
kStatementStatusPaid,
kStatementStatusUnpaid,
]
.map((value) => DropdownMenuItem<String>(
child: Text(localization.lookup(value)),
value: value,
))
.toList()),
);
final sectionPicker = Expanded(
child: DropDownMultiSelect(
onChanged: (List<dynamic> selected) {
//_selectedOptions = selected;
store.dispatch(UpdateUserPreferences(
statementIncludes: BuiltList<String>(selected)));
loadPDF();
},
selectedValues: state.prefState.statementIncludes.toList(),
menuItembuilder: (dynamic option) => Text(
localization.lookup(option),
style: TextStyle(fontSize: 14),
),
isDense: true,
options: <String>[
kStatementIncludePayments,
kStatementIncludeCredits,
kStatementIncludeAging,
],
whenEmpty: '',
height: 50,
));
/*
final pageSelector = _pageCount == 1
? <Widget>[]
: [
IconButton(
icon: Icon(Icons.navigate_before),
onPressed: _currentPage > 1
? () {
setState(() {
_currentPage++;
});
}
: null,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(localization.pdfPageInfo
.replaceFirst(':current', '$_currentPage')
.replaceFirst(':total', '$_pageCount')),
),
IconButton(
icon: Icon(Icons.navigate_next),
onPressed: _currentPage < _pageCount
? () {
setState(() {
_currentPage++;
});
}
: null,
),
];
*/
return Scaffold(
backgroundColor: Colors.grey.shade300,
appBar: widget.showAppBar
? AppBar(
centerTitle: false,
automaticallyImplyLeading: isMobile(context),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
EntityPresenter().initialize(client, context).title()!,
),
),
],
),
actions: <Widget>[
AppTextButton(
isInHeader: true,
label: localization.download,
onPressed: _response == null
? null
: () async {
final fileName = localization.statement +
'_' +
(client.number) +
'.pdf';
saveDownloadedFile(_response!.bodyBytes, fileName);
},
),
AppTextButton(
isInHeader: true,
label: localization.email,
onPressed: _response == null
? null
: () async {
if (!client.hasEmailAddress) {
showMessageDialog(
message: localization.clientEmailNotSet,
secondaryActions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
editEntity(
entity: state.clientState
.get(client.id));
},
child: Text(localization.editClient
.toUpperCase()))
]);
return;
}
confirmCallback(
message: localization.sendEmail,
context: context,
callback: (_) => loadPDF(sendEmail: true));
},
),
TextButton(
onPressed: () {
if (!state.isProPlan) {
showMessageDialog(
message: localization.upgradeToPaidPlanToSchedule,
secondaryActions: [
TextButton(
onPressed: () {
store.dispatch(ViewSettings(
section: kSettingsAccountManagement));
Navigator.of(context).pop();
},
child:
Text(localization.upgrade.toUpperCase())),
]);
return;
}
final includes = state.prefState.statementIncludes;
createEntity(
entity: ScheduleEntity(
ScheduleEntity.TEMPLATE_EMAIL_STATEMENT)
.rebuild((b) => b
..parameters.clients.add(client.id)
..parameters.showAgingTable =
includes.contains(localization.aging)
..parameters.showPaymentsTable =
includes.contains(localization.payments)
..parameters.showCreditsTable =
includes.contains(localization.credits)
..parameters.status = _status
..parameters.dateRange = _dateRange.snakeCase));
},
child: Text(localization.schedule,
style: TextStyle(color: state.headerTextColor))),
if (isDesktop(context))
TextButton(
child: Text(localization.close,
style: TextStyle(color: state.headerTextColor)),
onPressed: () {
viewEntity(entity: client);
},
),
],
)
: null,
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Material(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
isDesktop(context)
? Row(
children: [
datePicker,
SizedBox(width: 16),
statusPicker,
SizedBox(width: 16),
/*
if (hasDesignTemplatesForEntityType(
state.designState.map, EntityType.client)) ...[
designPicker,
SizedBox(width: 16),
],
*/
sectionPicker,
],
)
: Column(
children: [
Row(
children: [
datePicker,
SizedBox(width: 16),
statusPicker,
],
),
Row(
children: [
/*
if (hasDesignTemplatesForEntityType(
state.designState.map,
EntityType.client)) ...[
designPicker,
SizedBox(width: 16),
],
*/
sectionPicker,
],
),
],
),
if (_dateRange == DateRange.custom) ...[
SizedBox(height: 8),
Wrap(
alignment: WrapAlignment.start,
children: [
Container(
width: 180,
child: DatePicker(
labelText: localization.startDate,
onSelected: (value, _) {
setState(() {
_startDate = value;
});
},
selectedDate: _startDate,
),
),
SizedBox(width: 16),
Container(
width: 180,
child: DatePicker(
labelText: localization.endDate,
onSelected: (value, _) {
setState(() {
_endDate = value;
});
},
selectedDate: _endDate,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AppButton(
label: localization.loadPdf,
onPressed: () => loadPDF(),
),
)
],
),
],
],
),
),
),
Expanded(
child: _isLoading || _response == null
? LoadingIndicator()
: PdfPreview(
build: (format) => _response!.bodyBytes,
canChangeOrientation: false,
canChangePageFormat: false,
canDebug: false,
maxPageWidth: 800,
pdfFileName:
localization.statement + '_' + client.number + '.pdf',
),
),
],
),
);
}
}