invoice/lib/ui/reports/reports_screen.dart

1902 lines
65 KiB
Dart

// Flutter imports:
import 'package:collection/collection.dart' show IterableNullableExtension;
import 'package:flutter/material.dart' hide DataRow, DataCell, DataColumn;
import 'package:flutter/material.dart' as mt;
// Package imports:
import 'package:flutter_redux/flutter_redux.dart';
// Project imports:
import 'package:invoiceninja_flutter/.env.dart';
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/dashboard_model.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/main_app.dart';
import 'package:invoiceninja_flutter/redux/app/app_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart';
import 'package:invoiceninja_flutter/redux/reports/reports_actions.dart';
import 'package:invoiceninja_flutter/redux/reports/reports_state.dart';
import 'package:invoiceninja_flutter/redux/ui/pref_state.dart';
import 'package:invoiceninja_flutter/ui/app/actions_menu_button.dart';
import 'package:invoiceninja_flutter/ui/app/app_border.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/app_text_button.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart';
import 'package:invoiceninja_flutter/ui/app/dialogs/multiselect_dialog.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart';
import 'package:invoiceninja_flutter/ui/app/forms/date_picker.dart';
import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart';
import 'package:invoiceninja_flutter/ui/app/help_text.dart';
import 'package:invoiceninja_flutter/ui/app/history_drawer_vm.dart';
import 'package:invoiceninja_flutter/ui/app/menu_drawer_vm.dart';
import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart';
import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart';
import 'package:invoiceninja_flutter/ui/app/tables/app_data_table.dart';
import 'package:invoiceninja_flutter/ui/app/tables/app_data_table_source.dart';
import 'package:invoiceninja_flutter/ui/app/tables/app_paginated_data_table.dart';
import 'package:invoiceninja_flutter/ui/app/upgrade_dialog.dart';
import 'package:invoiceninja_flutter/ui/reports/report_charts.dart';
import 'package:invoiceninja_flutter/ui/reports/reports_screen_vm.dart';
import 'package:invoiceninja_flutter/utils/colors.dart';
import 'package:invoiceninja_flutter/utils/dates.dart';
import 'package:invoiceninja_flutter/utils/dialogs.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/utils/platforms.dart';
import 'package:invoiceninja_flutter/utils/strings.dart';
import 'package:url_launcher/url_launcher.dart';
class ReportsScreen extends StatelessWidget {
const ReportsScreen({
Key? key,
required this.viewModel,
}) : super(key: key);
static const String route = '/reports';
final ReportsScreenVM viewModel;
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context)!;
final store = StoreProvider.of<AppState>(context);
final state = viewModel.state;
final reportsState = viewModel.reportState;
final reportResult = viewModel.reportResult!;
Widget leading = SizedBox();
final hideReports = state.isHosted && !state.isProPlan && !state.isTrial;
if (isMobile(context) || state.prefState.isMenuFloated) {
leading = Builder(
builder: (context) => InkWell(
child: IconButton(
tooltip: localization.menuSidebar,
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer();
},
),
),
);
}
final hasCustomDate = reportsState.filters.keys.where((column) {
final filter = reportsState.filters[column];
return (getReportColumnType(column, context) ==
ReportColumnType.dateTime ||
getReportColumnType(column, context) == ReportColumnType.date) &&
filter == DateRange.custom.toString();
}).isNotEmpty;
final reports = [
kReportClient,
kReportClientContact,
if (state.company.isModuleEnabled(EntityType.invoice)) ...[
kReportInvoice,
kReportInvoiceItem,
kReportPayment,
if (state.company.hasTaxes) ...[
kReportInvoiceTax,
kReportPaymentTax,
],
if (state.company.isModuleEnabled(EntityType.recurringInvoice))
kReportRecurringInvoice,
],
if (state.company.isModuleEnabled(EntityType.quote)) ...[
kReportQuote,
kReportQuoteItem,
],
if (state.company.isModuleEnabled(EntityType.credit)) ...[
kReportCredit,
kReportCreditItem,
],
kReportDocument,
if (state.company.isModuleEnabled(EntityType.expense)) ...[
kReportExpense,
if (state.company.isModuleEnabled(EntityType.recurringExpense))
kReportRecurringExpense,
],
kReportProduct,
kReportProfitAndLoss,
kReportTask,
if (state.company.isModuleEnabled(EntityType.vendor)) ...[
kReportVendor,
if (state.company.isModuleEnabled(EntityType.purchaseOrder))
kReportPurchaseOrder,
kReportPurchaseOrderItem,
],
if (state.company.isModuleEnabled(EntityType.transaction))
kReportTransaction,
]..sort((a, b) => a.compareTo(b));
final reportChildren = [
AppDropdownButton<String>(
labelText: localization.report,
value: reportsState.report,
onChanged: (dynamic value) =>
viewModel.onSettingsChanged(report: value),
items: reports
.map((report) => DropdownMenuItem(
value: report,
child: Text(localization.lookup(report)!),
))
.toList(),
),
AppDropdownButton<String>(
labelText: localization.group,
value: reportsState.group,
blankValue: '',
showBlank: true,
onChanged: (dynamic value) {
viewModel.onSettingsChanged(group: value, selectedGroup: '');
},
items: reportResult.columns
.where((column) =>
getReportColumnType(column, context) != ReportColumnType.number)
.map((column) {
final columnTitle = state.company.getCustomFieldLabel(column);
return DropdownMenuItem(
child: Text(columnTitle.isEmpty
? localization.lookup(column)!
: columnTitle),
value: column,
);
}).toList(),
),
if (getReportColumnType(reportsState.group, context) ==
ReportColumnType.dateTime ||
getReportColumnType(reportsState.group, context) ==
ReportColumnType.date)
AppDropdownButton<String>(
labelText: localization.subgroup,
value: reportsState.subgroup,
onChanged: (dynamic value) {
viewModel.onSettingsChanged(subgroup: value);
},
items: [
DropdownMenuItem(
child: Text(localization.day),
value: kReportGroupDay,
),
DropdownMenuItem(
child: Text(localization.week),
value: kReportGroupWeek,
),
DropdownMenuItem(
child: Text(localization.month),
value: kReportGroupMonth,
),
DropdownMenuItem(
child: Text(localization.year),
value: kReportGroupYear,
),
],
),
];
final reportState = viewModel.reportState;
final filterColumns = reportState.filters.keys.where((column) =>
[
ReportColumnType.date,
ReportColumnType.dateTime,
].contains(getReportColumnType(column, context)) &&
(reportState.filters[column] ?? '').isNotEmpty);
final dateColumns = reportResult.columns.where((column) => [
ReportColumnType.date,
ReportColumnType.dateTime,
].contains(getReportColumnType(column, context)));
final dateField = filterColumns.isNotEmpty ? filterColumns.first : null;
final dateRange = (reportState.filters[dateField] ?? '').isNotEmpty
? DateRange.valueOf(reportState.filters[dateField]!)
: null;
final dateChildren = [
if (dateColumns.length > 1)
AppDropdownButton<String>(
labelText: localization.date,
value: dateField,
showBlank: true,
onChanged: (dynamic value) {
viewModel.onReportFiltersChanged(
context,
reportState.filters.rebuild((b) => b
..addAll(
(value ?? '').isEmpty
? {
if (filterColumns.isNotEmpty)
filterColumns.first: ''
}
: {
value: reportState.filters.containsKey(value) &&
(reportState.filters[value] ?? '')
.isNotEmpty
? reportState.filters[value]!
: DateRange.thisQuarter.toString(),
if (filterColumns.isNotEmpty &&
filterColumns.first != value)
filterColumns.first: ''
},
)),
);
},
items: dateColumns
.map((column) => DropdownMenuItem<String>(
value: column,
child: Text(
localization.lookup(column)!,
),
))
.toList()),
AppDropdownButton<DateRange>(
labelText: localization.range,
showBlank: true,
blankValue: null,
value: dateRange,
onChanged: dateColumns.isEmpty
? null
: (dynamic value) {
viewModel.onReportFiltersChanged(
context,
reportState.filters.rebuild((b) => b
..addAll({
dateField ?? dateColumns.first:
value == null ? '' : '$value'
})));
},
items: DateRange.values
.where((value) => value != DateRange.allTime)
.map((dateRange) => DropdownMenuItem<DateRange>(
child: Text(localization.lookup(dateRange.toString())!),
value: dateRange,
))
.toList(),
),
if (hasCustomDate) ...[
DatePicker(
labelText: localization.startDate,
selectedDate: reportsState.customStartDate,
onSelected: (date, _) =>
viewModel.onSettingsChanged(customStartDate: date),
),
DatePicker(
labelText: localization.endDate,
selectedDate: reportsState.customEndDate,
onSelected: (date, _) =>
viewModel.onSettingsChanged(customEndDate: date),
),
]
];
final cappedEntities = <BaseEntity?>[...reportResult.entities ?? []];
final firstEntity = cappedEntities.isNotEmpty ? cappedEntities.first : null;
if (cappedEntities.length > kMaxEntitiesPerBulkAction) {
cappedEntities.removeRange(
kMaxEntitiesPerBulkAction, cappedEntities.length);
}
final chartChildren = [
AppDropdownButton<String>(
enabled: reportsState.group.isNotEmpty,
labelText: localization.chart,
value: reportsState.group.isNotEmpty ? reportsState.chart : null,
blankValue: '',
showBlank: true,
onChanged: (dynamic value) {
viewModel.onSettingsChanged(chart: value);
},
items: reportResult.columns
.where((column) => [
ReportColumnType.number,
ReportColumnType.age,
ReportColumnType.duration,
].contains(getReportColumnType(column, context)))
.map((column) => DropdownMenuItem(
child: Text(localization.lookup(column)!),
value: column,
))
.toList(),
),
];
return WillPopScope(
onWillPop: () async {
store.dispatch(ViewDashboard());
return false;
},
child: Scaffold(
drawer: isMobile(context) || state.prefState.isMenuFloated
? MenuDrawerBuilder()
: null,
endDrawer: isMobile(context) || state.prefState.isHistoryFloated
? HistoryDrawerBuilder()
: null,
appBar: AppBar(
centerTitle: false,
automaticallyImplyLeading: false,
leadingWidth: isMobile(context) ? kMinInteractiveDimension : 0,
leading: leading,
title: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(child: Text(localization.reports)),
if (state.isSaving)
SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(),
),
],
),
actions: hideReports
? []
: [
if (isDesktop(context)) ...[
Builder(builder: (BuildContext context) {
return AppTextButton(
label: localization.columns,
isInHeader: true,
onPressed: () {
multiselectDialog(
// Using the navigatorKey to prevent using the appBarTheme
context: navigatorKey.currentContext!,
onSelected: (selected) {
viewModel.onReportColumnsChanged(
context, selected);
},
options: reportResult.allColumns,
selected: reportResult.columns.toList(),
defaultSelected: reportResult.defaultColumns,
);
},
);
}),
AppTextButton(
label: localization.export,
isInHeader: true,
onPressed: () {
viewModel.onExportPressed(context);
},
),
],
Padding(
padding: const EdgeInsets.only(right: 8),
child: ActionMenuButton(
entityActions: firstEntity == null
? null
: firstEntity.getActions(
userCompany: state.userCompany,
multiselect: true),
entity: firstEntity,
onSelected: (context, action) {
final entities = action.applyMaxLimit
? cappedEntities
: reportResult.entities!;
confirmCallback(
context: context,
message: localization.lookup(action.toString())! +
'' +
(entities.length == 1
? '1 ${localization.lookup(firstEntity!.entityType.toString())}'
: '${entities.length} ${localization.lookup(firstEntity!.entityType!.plural)}'),
callback: (_) {
handleEntitiesActions(entities, action);
});
}),
),
if (isMobile(context) || !state.prefState.isHistoryVisible)
Builder(
builder: (context) => IconButton(
icon: Icon(Icons.history),
padding: const EdgeInsets.only(left: 4, right: 20),
tooltip: state.prefState.enableTooltips
? localization.history
: null,
onPressed: () {
if (isMobile(context) ||
state.prefState.isHistoryFloated) {
Scaffold.of(context).openEndDrawer();
} else {
store.dispatch(UpdateUserPreferences(
sidebar: AppSidebar.history));
}
},
),
),
],
),
body: hideReports
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
HelpText(localization.upgradeToViewReports),
AppButton(
label: localization.upgrade.toUpperCase(),
onPressed: () {
if (supportsInAppPurchase() &&
state.account.canMakeIAP) {
showDialog<void>(
context: context,
builder: (context) => UpgradeDialog(),
);
} else {
launchUrl(
Uri.parse(state.userCompany.ninjaPortalUrl));
}
})
],
),
)
: ScrollableListView(
primary: true,
key: ValueKey(
'${viewModel.state.company.id}_${viewModel.state.isSaving}_${reportsState.report}_${reportsState.group}'),
children: <Widget>[
isMobile(context)
? FormCard(
children: [
...reportChildren,
...dateChildren,
...chartChildren,
],
)
: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Flexible(
child: FormCard(
children: reportChildren,
padding: const EdgeInsets.only(
top: kMobileDialogPadding,
right: kMobileDialogPadding / 2,
left: kMobileDialogPadding),
),
),
Flexible(
child: FormCard(
children: dateChildren,
padding: const EdgeInsets.only(
top: kMobileDialogPadding,
right: kMobileDialogPadding / 2,
left: kMobileDialogPadding / 2),
),
),
Flexible(
child: FormCard(
children: chartChildren,
padding: const EdgeInsets.only(
top: kMobileDialogPadding,
right: kMobileDialogPadding,
left: kMobileDialogPadding / 2),
),
)
],
),
if (isMobile(context))
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Builder(builder: (BuildContext context) {
return Expanded(
child: AppButton(
label: localization.columns,
onPressed: () {
multiselectDialog(
context: context,
onSelected: (selected) {
viewModel.onReportColumnsChanged(
context, selected);
},
options: reportResult.allColumns,
selected: reportResult.columns.toList(),
defaultSelected:
reportResult.defaultColumns,
);
},
),
);
}),
SizedBox(width: kGutterWidth),
Expanded(
child: AppButton(
label: localization.export,
onPressed: () {
viewModel.onExportPressed(context);
},
),
),
],
),
),
ReportDataTable(
key: ValueKey(
'${viewModel.state.isSaving}_${reportsState.group}_${reportsState.selectedGroup}'),
viewModel: viewModel,
)
],
),
),
);
}
}
class ReportDataTable extends StatefulWidget {
const ReportDataTable({Key? key, required this.viewModel}) : super(key: key);
final ReportsScreenVM viewModel;
@override
_ReportDataTableState createState() => _ReportDataTableState();
}
class _ReportDataTableState extends State<ReportDataTable> {
final Map<String, Map<String, TextEditingController>>
_textEditingControllers = {};
final Map<String, Map<String, FocusNode>> _textEditingFocusNodes = {};
late ReportDataTableSource dataTableSource;
@override
void initState() {
super.initState();
final viewModel = widget.viewModel;
dataTableSource = ReportDataTableSource(
viewModel: viewModel,
context: context,
textEditingControllers: _textEditingControllers,
textEditingFocusNodes: _textEditingFocusNodes,
onFilterChanged: (column, value) {
final reportState = widget.viewModel.reportState;
viewModel.onReportFiltersChanged(context,
reportState.filters.rebuild((b) => b..addAll({column: value})));
});
}
@override
void didUpdateWidget(ReportDataTable oldWidget) {
super.didUpdateWidget(oldWidget);
final viewModel = widget.viewModel;
dataTableSource.viewModel = viewModel;
/*
dataTableSource.editingId = viewModel.state.productUIState.editing.id;
dataTableSource.entityList = viewModel.productList;
dataTableSource.entityMap = viewModel.productMap;
*/
// ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
dataTableSource.notifyListeners();
}
@override
void didChangeDependencies() {
final viewModel = widget.viewModel;
final reportState = viewModel.reportState;
final reportResult = viewModel.reportResult!;
for (var column in reportResult.columns) {
if (_textEditingControllers[reportState.report] == null) {
_textEditingControllers[reportState.report] = {};
_textEditingFocusNodes[reportState.report] = {};
}
if (!_textEditingControllers[reportState.report]!.containsKey(column)) {
final textEditingController = TextEditingController();
// TODO figure out how to remove this listener in dispose
textEditingController.addListener(() {
_onChanged(column, textEditingController.text);
});
if (reportState.filters.containsKey(column)) {
textEditingController.text = reportState.filters[column]!;
}
_textEditingControllers[reportState.report]![column] =
textEditingController;
_textEditingFocusNodes[reportState.report]![column] = FocusNode();
}
}
super.didChangeDependencies();
}
void _onChanged(String column, String value) {
widget.viewModel.onReportFiltersChanged(
context,
widget.viewModel.reportState.filters
.rebuild((b) => b..addAll({column: value})));
}
@override
void dispose() {
_textEditingControllers.keys.forEach((i) {
_textEditingControllers[i]!.keys.forEach((j) {
_textEditingControllers[i]![j]!.dispose();
_textEditingFocusNodes[i]![j]!.dispose();
});
});
super.dispose();
}
@override
Widget build(BuildContext context) {
final state = widget.viewModel.state;
final viewModel = widget.viewModel;
final reportResult = viewModel.reportResult!;
final reportState = viewModel.reportState;
final settings = state.userCompany.settings;
final reportSettings =
settings.reportSettings.containsKey(reportState.report)
? settings.reportSettings[reportState.report]!
: ReportSettingsEntity();
final sortedColumns = reportResult.sortedColumns(reportState);
return Column(
children: <Widget>[
if (reportState.chart.isNotEmpty)
ClipRect(
child: ReportCharts(
viewModel: widget.viewModel,
),
),
if (reportResult.showTotals)
FormCard(
child: isMobile(context)
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: TotalsDataTable(
viewModel: viewModel,
reportResult: reportResult,
reportSettings: reportSettings,
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TotalsDataTable(
viewModel: viewModel,
reportResult: reportResult,
reportSettings: reportSettings,
),
],
),
),
SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: AppPaginatedDataTable(
//showFirstLastButtons: true,
subtractOneFromCount: true,
header: SizedBox(),
sortColumnIndex: sortedColumns.contains(reportSettings.sortColumn)
? sortedColumns.indexOf(reportSettings.sortColumn)
: null,
sortAscending: reportSettings.sortAscending,
columns: reportResult.tableColumns(
context,
(index, ascending) => widget.viewModel
.onReportSorted(sortedColumns[index], ascending)),
source: dataTableSource,
),
)
],
);
}
}
class TotalsDataTable extends StatelessWidget {
const TotalsDataTable({
required this.reportSettings,
required this.reportResult,
required this.viewModel,
});
final ReportsScreenVM viewModel;
final ReportSettingsEntity reportSettings;
final ReportResult reportResult;
@override
Widget build(BuildContext context) {
return mt.DataTable(
sortColumnIndex:
reportResult.columns.length > reportSettings.sortTotalsIndex
? reportSettings.sortTotalsIndex
: null,
sortAscending: reportSettings.sortTotalsAscending,
columns: reportResult.totalColumns(
context,
(index, ascending) =>
viewModel.onReportTotalsSorted(index, ascending)),
rows: reportResult.totalRows(context),
);
}
}
enum ReportColumnType {
string,
dateTime,
date,
number,
bool,
age,
duration,
}
bool canTotalColumn(String? column) {
if (['notification_threshold', 'age'].contains(column)) {
return false;
}
if (column!.contains('_rate')) {
return false;
}
return true;
}
ReportColumnType getReportColumnType(String? column, BuildContext context) {
column = toSnakeCase(column);
ReportColumnType convertCustomFieldType(String type) {
if (type == kFieldTypeDate) {
return ReportColumnType.date;
} else if (type == kFieldTypeSwitch) {
return ReportColumnType.bool;
} else {
return ReportColumnType.string;
}
}
final store = StoreProvider.of<AppState>(context);
final company = store.state.userCompany.company;
if (column.startsWith('surcharge')) {
return ReportColumnType.number;
} else if (column == 'duration') {
return ReportColumnType.duration;
} else if (company.hasCustomField(column)) {
return convertCustomFieldType(company.getCustomFieldType(column));
} else if (EntityPresenter.isFieldNumeric(column)) {
return ReportColumnType.number;
} else if (['updated_at', 'created_at', 'start_time', 'end_time']
.contains(column)) {
return ReportColumnType.dateTime;
} else if ((column.contains('_date') && column != 'paid_to_date') ||
['date', 'valid_until'].contains(column)) {
return ReportColumnType.date;
} else if (column == 'age') {
return ReportColumnType.age;
} else if (column.startsWith('is_')) {
return ReportColumnType.bool;
} else {
return ReportColumnType.string;
}
}
class ReportDataTableSource extends AppDataTableSource {
ReportDataTableSource({
required this.context,
required this.textEditingControllers,
required this.textEditingFocusNodes,
required this.onFilterChanged,
required this.viewModel,
});
ReportsScreenVM viewModel;
final BuildContext context;
final Map<String, Map<String, TextEditingController>> textEditingControllers;
final Map<String, Map<String, FocusNode>> textEditingFocusNodes;
final Function(String, String) onFilterChanged;
@override
int get selectedRowCount => 0;
@override
bool get isRowCountApproximate => false;
@override
int get rowCount {
final reportState = viewModel.reportState;
if (reportState.group.isEmpty || reportState.isGroupByFiltered) {
return viewModel.reportResult!.data.length + 1;
} else {
return viewModel.groupTotals.totals == null
? 1
: viewModel.groupTotals.totals!.length + 1;
}
}
@override
DataRow getRow(int index) {
final reportResult = viewModel.reportResult;
if (index == 0) {
return reportResult!.tableFilters(
context,
textEditingControllers[viewModel.reportState.report],
textEditingFocusNodes[viewModel.reportState.report],
(column, value) => onFilterChanged(column, value));
} else {
return reportResult!.tableRow(context, viewModel, index);
}
}
}
class ReportResult {
ReportResult({
required this.columns,
required this.allColumns,
required this.defaultColumns,
required this.data,
this.entities,
this.showTotals = true,
});
final List<String> columns;
final List<String> allColumns;
final List<String> defaultColumns;
final List<List<ReportElement>> data;
final List<BaseEntity>? entities;
final bool showTotals;
static bool? matchField({
required String column,
required dynamic value,
required UserCompanyEntity userCompany,
required ReportsUIState reportsUIState,
AppLocalization? localization,
}) {
if (reportsUIState.filters.containsKey(column)) {
final filter = reportsUIState.filters[column]!;
if (filter.isNotEmpty) {
if (column == 'age') {
final min = kAgeGroups[filter];
if (filter == kAgeGroupPaid) {
return value == -1;
} else if (filter == kAgeGroup120) {
return value > min;
} else {
final max = min! + 30;
if (value < min || value >= max) {
return false;
}
}
} else if (value.runtimeType == int || value.runtimeType == double) {
if (!ReportResult.matchAmount(filter: filter, amount: value)) {
return false;
}
} else if (value.runtimeType == bool ||
filter == 'true' ||
filter == 'false') {
// Support custom fields
String boolFilter = filter;
if (filter.toLowerCase() == 'yes') {
boolFilter = 'true';
} else if (filter.toLowerCase() == 'no') {
boolFilter = 'false';
}
if (value.runtimeType == String) {
if (value.toLowerCase() == 'yes') {
value = 'true';
} else if (value.toLowerCase() == 'no') {
value = 'false';
}
}
if (boolFilter != '$value') {
return false;
}
} else if (value.runtimeType == EntityType) {
return filter.toLowerCase() == '$value'.toLowerCase();
} else if (isValidDate(value)) {
if (!ReportResult.matchDateTime(
filter: filter,
value: value,
reportsUIState: reportsUIState,
userCompany: userCompany)) {
return false;
}
} else if (!ReportResult.matchString(filter: filter, value: value)) {
return false;
}
}
}
return true;
}
static bool matchString({
required String filter,
String? value,
}) {
filter = filter.trim();
if (filter.isEmpty) {
return true;
}
value = (value ?? '').toLowerCase();
filter = filter.toLowerCase();
if (filter == 'null' && value.isEmpty) {
return true;
}
if (value.contains(filter)) {
return true;
}
final localization = AppLocalization.of(navigatorKey.currentContext!)!;
if (localization.lookup(value)!.toLowerCase().contains(filter)) {
return true;
}
return false;
}
static bool matchAmount({required String filter, required num amount}) {
final String range = filter.replaceAll(',', '-') + '-';
final List<String> parts = range.split('-');
final min = parseDouble(parts[0])!;
final max = parts.length > 1 ? parseDouble(parts[1]) : 0;
if (amount < min || (max! > 0 && amount > max)) {
return false;
}
return true;
}
static bool matchDateTime({
required String filter,
required String value,
required UserCompanyEntity userCompany,
required ReportsUIState reportsUIState,
}) {
DateRange dateRange = DateRange.last30Days;
try {
dateRange = DateRange.valueOf(filter);
} catch (exception) {
//
}
final startDate = calculateStartDate(
dateRange: dateRange,
company: userCompany.company,
customStartDate: reportsUIState.customStartDate,
customEndDate: reportsUIState.customEndDate,
);
final endDate = calculateEndDate(
dateRange: dateRange,
company: userCompany.company,
customStartDate: reportsUIState.customStartDate,
customEndDate: reportsUIState.customEndDate,
);
final customStartDate = reportsUIState.customStartDate;
final customEndDate = reportsUIState.customEndDate;
value = value.split('T').first;
if (dateRange == DateRange.custom) {
if (customStartDate.isNotEmpty && customEndDate.isNotEmpty) {
if (!(startDate!.compareTo(value) <= 0 &&
endDate!.compareTo(value) >= 0)) {
return false;
}
} else if (customStartDate.isNotEmpty) {
if (!(startDate!.compareTo(value) <= 0)) {
return false;
}
} else if (customEndDate.isNotEmpty) {
if (!(endDate!.compareTo(value) >= 0)) {
return false;
}
}
} else {
if (!(startDate!.compareTo(value) <= 0 &&
endDate!.compareTo(value) >= 0)) {
return false;
}
}
return true;
}
List<String> sortedColumns(ReportsUIState reportState) {
final data = columns.toList();
final group = reportState.group;
if (group.isNotEmpty) {
data.remove(group);
data.insert(0, group);
}
return data;
}
List<DataColumn> tableColumns(
BuildContext context, Function(int, bool) onSortCallback) {
final localization = AppLocalization.of(context);
final store = StoreProvider.of<AppState>(context);
final company = store.state.company;
final reportState = store.state.uiState.reportsUIState;
return [
for (String? column in sortedColumns(reportState))
DataColumn(
label: Container(
constraints: BoxConstraints(minWidth: kTableColumnWidthMin),
child: Row(
children: [
Text(
(company.getCustomFieldLabel(column!).isNotEmpty
? company.getCustomFieldLabel(column)
: localization!.lookup(column))! +
' ',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (column == reportState.group)
IconButton(
onPressed: () {
store.dispatch(UpdateReportSettings(
report: reportState.report,
group: '',
));
},
icon: Icon(Icons.clear, color: Colors.grey))
],
),
),
numeric:
getReportColumnType(column, context) == ReportColumnType.number,
onSort: onSortCallback,
)
];
}
DataRow tableFilters(
BuildContext context,
Map<String, TextEditingController>? textEditingControllers,
Map<String, FocusNode>? textEditingFocusNodes,
Function(String, String) onFilterChanged) {
final localization = AppLocalization.of(context);
final theme = Theme.of(context);
final store = StoreProvider.of<AppState>(context);
final reportState = store.state.uiState.reportsUIState;
return DataRow(cells: [
for (String column in sortedColumns(reportState))
if (textEditingControllers == null ||
!textEditingControllers.containsKey(column))
DataCell(Text(''))
else if (getReportColumnType(column, context) == ReportColumnType.bool)
DataCell(AppDropdownButton<bool>(
labelText: null,
showBlank: true,
blankValue: null,
value: textEditingControllers[column]!.text == 'true'
? true
: textEditingControllers[column]!.text == 'false'
? false
: null,
onChanged: (dynamic value) {
if (value == null) {
textEditingControllers[column]!.text = '';
onFilterChanged(column, '');
} else {
textEditingControllers[column]!.text = value.toString();
onFilterChanged(column, value.toString());
}
},
items: [
DropdownMenuItem(
child: Text(AppLocalization.of(context)!.yes),
value: true,
),
DropdownMenuItem(
child: Text(AppLocalization.of(context)!.no),
value: false,
),
],
))
else if (getReportColumnType(column, context) == ReportColumnType.age)
DataCell(AppDropdownButton<String>(
value: (textEditingControllers[column]!.text).isNotEmpty &&
textEditingControllers[column]!.text != 'null'
? textEditingControllers[column]!.text
: null,
showBlank: true,
onChanged: (dynamic value) {
textEditingControllers[column]!.text = value;
onFilterChanged(column, value);
},
items: kAgeGroups.keys
.map((ageGroup) => DropdownMenuItem(
child: Text(localization!.lookup(ageGroup)!),
value: ageGroup,
))
.toList(),
))
else if ([
ReportColumnType.number,
ReportColumnType.duration,
].contains(getReportColumnType(column, context)))
DataCell(TextFormField(
controller: textEditingControllers[column],
keyboardType:
TextInputType.numberWithOptions(decimal: true, signed: true),
decoration: InputDecoration(
suffixIcon: (textEditingControllers[column]?.text ?? '').isEmpty
? null
: IconButton(
icon: Icon(
Icons.clear,
color: Colors.grey,
),
onPressed: () {
textEditingControllers[column]!.text = '';
onFilterChanged(column, '');
},
)),
))
else if ([
ReportColumnType.date,
ReportColumnType.dateTime,
].contains(getReportColumnType(column, context)))
DataCell(AppDropdownButton<DateRange>(
labelText: null,
showBlank: true,
blankValue: null,
value: (reportState.filters[column] ?? '').isNotEmpty
? DateRange.valueOf(reportState.filters[column]!)
: null,
onChanged: (dynamic value) {
if (value == null) {
textEditingControllers[column]!.text = '';
onFilterChanged(column, '');
} else {
textEditingControllers[column]!.text = value.toString();
onFilterChanged(column, value.toString());
}
},
items: DateRange.values
.where((value) => value != DateRange.allTime)
.map((dateRange) => DropdownMenuItem<DateRange>(
child: Text(localization!.lookup(dateRange.toString())!),
value: dateRange,
))
.toList(),
))
// TODO remove DEMO_MODE check
else if (Config.DEMO_MODE)
DataCell(TextFormField(
controller: textEditingControllers[column],
decoration: InputDecoration(
suffixIcon: (textEditingControllers[column]?.text ?? '').isEmpty
? null
: IconButton(
icon: Icon(
Icons.clear,
color: Colors.grey,
),
onPressed: () {
textEditingControllers[column]!.text = '';
onFilterChanged(column, '');
},
)),
))
else
DataCell(
RawAutocomplete<String>(
textEditingController: textEditingControllers[column],
focusNode: textEditingFocusNodes![column],
optionsBuilder: (TextEditingValue textEditingValue) {
final filter = textEditingValue.text.toLowerCase();
final index = columns.indexOf(column);
final options = data
.where((row) =>
row[index]
.renderText(context, column)!
.toLowerCase()
.contains(filter) &&
row[index]
.renderText(context, column)!
.trim()
.isNotEmpty)
.map((row) => row[index].renderText(context, column))
.whereType<String>()
.toSet()
.toList();
return options;
},
onSelected: (value) {
final textEditingController = textEditingControllers[column]!;
textEditingController.text = value;
onFilterChanged(column, value);
textEditingFocusNodes[column]!.requestFocus();
WidgetsBinding.instance.addPostFrameCallback((duration) {
textEditingController.selection = TextSelection.fromPosition(
TextPosition(offset: textEditingController.text.length));
});
},
fieldViewBuilder: (BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted) {
return DecoratedFormField(
keyboardType: TextInputType.text,
decoration: textEditingController.text.isEmpty
? null
: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
color: Colors.grey,
),
onPressed: () {
textEditingControllers[column]!.text = '';
onFilterChanged(column, '');
textEditingFocusNodes[column]!.unfocus();
},
)),
controller: textEditingController,
focusNode: focusNode,
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
);
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options) {
final highlightedIndex =
AutocompleteHighlightedOption.of(context);
return Theme(
data: theme,
child: Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4,
child: AppBorder(
child: Container(
color: Theme.of(context).cardColor,
width: 250,
constraints: BoxConstraints(maxHeight: 270),
child: ScrollableListViewBuilder(
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
return Container(
color: highlightedIndex == index
? convertHexStringToColor(
store.state.prefState.enableDarkMode
? kDefaultDarkSelectedColor
: kDefaultLightSelectedColor)
: Theme.of(context).cardColor,
child: ListTile(
title: Text(options.elementAt(index),
style: Theme.of(context)
.textTheme
.titleMedium),
onTap: () => onSelected(
options.elementAt(index),
),
),
);
},
),
),
),
),
),
);
},
),
),
]);
}
DataRow tableRow(BuildContext context, ReportsScreenVM viewModel, int index) {
final store = StoreProvider.of<AppState>(context);
final state = store.state;
final reportState = state.uiState.reportsUIState;
final groupBy = reportState.group;
final sorted = sortedColumns(reportState);
if (groupBy.isEmpty || reportState.isGroupByFiltered) {
final row = data[index - 1];
final cells = <DataCell>[];
for (var j = 0; j < row.length; j++) {
final index = columns.indexOf(sorted[j]);
final cell = row[index];
final column = sorted[j];
cells.add(
DataCell(
ConstrainedBox(
constraints: BoxConstraints(maxWidth: kTableColumnWidthMax),
child: cell.renderWidget(context, column),
),
onTap: () {
viewEntityById(
entityId: cell.entityId, entityType: cell.entityType);
},
),
);
}
return DataRow(cells: cells);
} else {
final groupTotals = viewModel.groupTotals;
final group = groupTotals.rows![index - 1];
final values = viewModel.groupTotals.totals![group];
final cells = <DataCell>[];
final localization = AppLocalization.of(context);
for (var column in sortedColumns(reportState)) {
String? value = '';
final columnType = getReportColumnType(column, context);
if (column == groupBy) {
if (group!.isEmpty) {
value = localization!.blank;
} else if (columnType == ReportColumnType.dateTime ||
columnType == ReportColumnType.date) {
value = formatDate(group, context);
} else if (columnType == ReportColumnType.age ||
EntityPresenter.isFieldLocalized(column)) {
value = localization!.lookup(group);
} else {
value = group == 'null' ? localization!.blank : group;
}
value = value! + ' (' + values!['count']!.floor().toString() + ')';
} else if (columnType == ReportColumnType.number) {
final currencyId = values!['${column}_currency_id'];
value = formatNumber(values[column], context,
formatNumberType: column.toLowerCase().contains('quantity')
? FormatNumberType.double
: FormatNumberType.money,
currencyId:
currencyId == null ? null : currencyId.round().toString());
} else if (columnType == ReportColumnType.duration) {
value = formatDuration(Duration(seconds: values![column]!.toInt()));
}
cells.add(DataCell(Text(value!), onTap: () {
if (group!.isEmpty) {
return;
}
if (column == groupBy) {
String filter = group;
String customStartDate = '';
String customEndDate = '';
if (getReportColumnType(column, context) ==
ReportColumnType.dateTime ||
getReportColumnType(column, context) == ReportColumnType.date) {
filter = DateRange.custom.toString();
final date = DateTime.tryParse(group);
customStartDate = group;
if (reportState.subgroup == kReportGroupDay) {
customEndDate = convertDateTimeToSqlDate(date);
} else if (reportState.subgroup == kReportGroupMonth) {
customEndDate =
convertDateTimeToSqlDate(addDays(addMonths(date!, 1), -1));
} else if (reportState.subgroup == kReportGroupWeek) {
customEndDate = convertDateTimeToSqlDate(addDays(date!, 6));
} else {
customEndDate =
convertDateTimeToSqlDate(addDays(addYears(date!, 1), -1));
}
} else if (getReportColumnType(column, context) ==
ReportColumnType.bool) {
filter = filter == localization!.yes
? 'true'
: filter == localization.no
? 'false'
: '';
}
store.dispatch(
UpdateReportSettings(
report: reportState.report,
selectedGroup: filter,
customStartDate: customStartDate,
customEndDate: customEndDate,
filters: reportState.filters
.rebuild((b) => b..addAll({column: filter})),
),
);
}
}));
}
return DataRow(cells: cells);
}
}
List<mt.DataColumn> totalColumns(
BuildContext context, Function(int, bool) onSortCallback) {
final store = StoreProvider.of<AppState>(context);
final company = store.state.company;
final localization = AppLocalization.of(context)!;
final sortedColumns = columns
.where((column) => canTotalColumn(column))
.toList()
..sort((String? str1, String? str2) => str1!.compareTo(str2!));
//for (String column in sortedColumns)
// print('## $column => ${getReportColumnType(column, context)}');
final totalColumns = [
mt.DataColumn(
label: Text(localization.currency),
onSort: onSortCallback,
),
mt.DataColumn(
label: Text(localization.count),
onSort: onSortCallback,
),
for (String? column in sortedColumns)
if ([
ReportColumnType.number,
ReportColumnType.age,
ReportColumnType.duration,
].contains(getReportColumnType(column, context)))
mt.DataColumn(
label: Text(
company.getCustomFieldLabel(column!).isEmpty
? localization.lookup(column)!
: company.getCustomFieldLabel(column),
overflow: TextOverflow.ellipsis,
),
numeric: true,
onSort: onSortCallback,
)
];
//print('## Total Columns: ${totalColumns.length}');
return totalColumns;
}
List<mt.DataRow> totalRows(BuildContext context) {
final rows = <mt.DataRow>[];
final store = StoreProvider.of<AppState>(context);
final state = store.state;
final reportState = state.uiState.reportsUIState;
final settings = state.userCompany.settings;
final reportSettings =
settings.reportSettings.containsKey(reportState.report)
? settings.reportSettings[reportState.report]!
: ReportSettingsEntity();
final Map<String, Map<String?, double>> totals = {};
final allColumns = <String?>[];
for (var i = 0; i < data.length; i++) {
final row = data[i];
bool countedRow = false;
for (var j = 0; j < row.length; j++) {
final cell = row[j];
final column = columns[j];
final canTotal = (cell is ReportIntValue ||
cell is ReportNumberValue ||
cell is ReportDurationValue ||
cell is ReportAgeValue) &&
canTotalColumn(column);
String currencyId = '';
if (canTotal) {
if (!allColumns.contains(column)) {
allColumns.add(column);
}
if (cell is ReportNumberValue) {
currencyId = cell.currencyId ?? '';
} else if (cell is ReportAgeValue) {
currencyId = cell.currencyId ?? '';
} else if (cell is ReportDurationValue) {
currencyId = cell.currencyId ?? '';
}
// if the specific cell doesn't have a currency
// set then find it in the row
if (currencyId.isEmpty) {
for (var k = 0; k < row.length; k++) {
final cell = row[k];
if (cell is ReportNumberValue) {
currencyId = cell.currencyId ?? '';
} else if (cell is ReportAgeValue) {
currencyId = cell.currencyId ?? '';
} else if (cell is ReportDurationValue) {
currencyId = cell.currencyId ?? '';
}
}
}
if (!totals.containsKey(currencyId)) {
totals[currencyId] = {'count': 0};
}
if (!countedRow) {
totals[currencyId]!['count'] = totals[currencyId]!['count']! + 1;
countedRow = true;
}
if (!totals[currencyId]!.containsKey(column)) {
totals[currencyId]![column] = 0;
}
totals[currencyId]![column] =
totals[currencyId]![column]! + cell.doubleValue!;
}
}
}
for (var currencyId in totals.keys) {
for (var column in allColumns) {
if (!totals[currencyId]!.containsKey(column)) {
totals[currencyId]![column] = 0;
}
}
}
final keys = totals.keys.whereNotNull().toList();
keys.sort((rowA, rowB) {
dynamic valueA;
dynamic valueB;
if (reportSettings.sortTotalsIndex == 0) {
final currencyMap = state.staticState.currencyMap;
valueA = currencyMap[rowA]?.listDisplayName;
valueB = currencyMap[rowB]?.listDisplayName;
} else if (reportSettings.sortTotalsIndex == 1) {
valueA = totals[rowA]!['count'];
valueB = totals[rowB]!['count'];
} else {
final List<String?> fields = totals[rowA]!.keys.toList()
..remove('count')
..sort((String? str1, String? str2) => str1!.compareTo(str2!));
final sortColumn = fields[reportSettings.sortTotalsIndex - 2];
valueA = totals[rowA]![sortColumn];
valueB = totals[rowB]![sortColumn];
}
if (valueA == null || valueB == null) {
return 0;
}
return reportSettings.sortTotalsAscending
? valueA.compareTo(valueB)
: valueB.compareTo(valueA);
});
List<String?> allFields = [];
keys.forEach((currencyId) {
final values = totals[currencyId]!;
allFields.addAll(values.keys);
});
allFields = allFields.toSet().toList()
..sort((String? str1, String? str2) => str1!.compareTo(str2!));
keys.forEach((currencyId) {
final values = totals[currencyId]!;
final cells = <mt.DataCell>[
mt.DataCell(Text(
store.state.staticState.currencyMap[currencyId]?.listDisplayName ??
'')),
mt.DataCell(Text(values['count']!.toInt().toString())),
];
allFields.forEach((field) {
final amount = values[field];
if (field != 'count') {
String? value;
if (field == 'age') {
value = formatNumber(amount! / values['count']!, context,
formatNumberType: FormatNumberType.double);
} else if (field == 'duration') {
value = formatDuration(Duration(seconds: amount!.toInt()));
} else {
value = formatNumber(amount, context,
currencyId: currencyId,
formatNumberType: EntityPresenter.isFieldAmount(field)
? FormatNumberType.double
: FormatNumberType.money);
}
cells.add(mt.DataCell(Text(value!)));
}
});
//print('## Total Rows: ${cells.length}');
rows.add(mt.DataRow(cells: cells));
});
return rows;
}
}
abstract class ReportElement {
ReportElement({this.entityType, this.entityId});
final EntityType? entityType;
final String? entityId;
double? get doubleValue => 0;
String? get stringValue => '';
Widget renderWidget(BuildContext context, String? column) {
throw 'Error: need to override renderWidget()';
}
String? renderText(BuildContext context, String? column) {
throw 'Error: need to override sortString()';
}
}
class ReportStringValue extends ReportElement {
ReportStringValue({
required this.value,
required EntityType? entityType,
required String entityId,
}) : super(entityType: entityType, entityId: entityId);
final String? value;
@override
String? get stringValue => value;
@override
Widget renderWidget(BuildContext context, String? column) {
return Text(
renderText(context, column)!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
@override
String? renderText(BuildContext context, String? column) {
if (getReportColumnType(column, context) == ReportColumnType.dateTime ||
getReportColumnType(column, context) == ReportColumnType.date) {
return formatDate(value, context,
showTime: getReportColumnType(column, context) ==
ReportColumnType.dateTime);
} else if (EntityPresenter.isFieldLocalized(column)) {
return AppLocalization.of(context)!.lookup(value);
} else {
return value ?? '';
}
}
}
class ReportEntityTypeValue extends ReportElement {
ReportEntityTypeValue({
required this.value,
required EntityType? entityType,
required String? entityId,
}) : super(entityType: entityType, entityId: entityId);
final EntityType? value;
@override
String get stringValue => '$value';
@override
Widget renderWidget(BuildContext context, String? column) {
return Text(renderText(context, column)!);
}
@override
String? renderText(BuildContext context, String? column) {
return AppLocalization.of(context)!.lookup('$value');
}
}
class ReportAgeValue extends ReportElement {
ReportAgeValue({
required this.value,
required EntityType? entityType,
required String entityId,
required this.currencyId,
}) : super(entityType: entityType, entityId: entityId);
final int? value;
final String? currencyId;
@override
String get stringValue => '$value';
@override
double get doubleValue => value == -1 ? 0 : value!.toDouble();
@override
Widget renderWidget(BuildContext context, String? column) {
return Text(renderText(context, column));
}
@override
String renderText(BuildContext context, String? column) {
return value == -1 ? AppLocalization.of(context)!.paid : '$value';
}
}
class ReportDurationValue extends ReportElement {
ReportDurationValue({
required this.value,
required EntityType? entityType,
required String entityId,
required this.currencyId,
}) : super(entityType: entityType, entityId: entityId);
final int? value;
final String? currencyId;
@override
String get stringValue => '$value';
@override
double get doubleValue => value!.toDouble();
@override
Widget renderWidget(BuildContext context, String? column) {
return Text(renderText(context, column));
}
@override
String renderText(BuildContext context, String? column) {
return formatDuration(Duration(seconds: value!));
}
}
class ReportIntValue extends ReportElement {
ReportIntValue({
this.value,
EntityType? entityType,
String? entityId,
}) : super(entityType: entityType, entityId: entityId);
final int? value;
@override
String get stringValue => '$value';
@override
double get doubleValue => value!.toDouble();
@override
Widget renderWidget(BuildContext context, String? column) {
return Text(renderText(context, column)!);
}
@override
String? renderText(BuildContext context, String? column) {
return formatNumber(value!.toDouble(), context,
formatNumberType: FormatNumberType.int);
}
}
class ReportNumberValue extends ReportElement {
ReportNumberValue({
required this.value,
required EntityType? entityType,
required String entityId,
required this.currencyId,
required this.exchangeRate,
this.formatNumberType = FormatNumberType.money,
}) : super(entityType: entityType, entityId: entityId);
final double? value;
final FormatNumberType? formatNumberType;
final String? currencyId;
final double? exchangeRate;
@override
double? get doubleValue => value;
@override
String get stringValue => '$value';
@override
Widget renderWidget(BuildContext context, String? column) {
return Text(renderText(context, column)!);
}
@override
String? renderText(BuildContext context, String? column) {
if (currencyId == null ||
column!.endsWith('_rate') ||
column.endsWith('_rate1') ||
column.endsWith('_rate2') ||
column.endsWith('_rate3')) {
return formatNumber(value, context,
formatNumberType: FormatNumberType.double);
}
return formatNumber(value, context,
currencyId: currencyId,
formatNumberType: formatNumberType ?? FormatNumberType.money);
}
}
class ReportBoolValue extends ReportElement {
ReportBoolValue({
required this.value,
required EntityType? entityType,
required String entityId,
}) : super(entityType: entityType, entityId: entityId);
final bool? value;
@override
String get stringValue => '$value';
@override
Widget renderWidget(BuildContext context, String? column) {
final localization = AppLocalization.of(context);
return SizedBox(
width: 80,
child: Text(
value == true ? localization!.yes : localization!.no,
textAlign: TextAlign.center,
),
);
}
@override
String renderText(BuildContext context, String? column) {
final localization = AppLocalization.of(context);
return value == true ? localization!.yes : localization!.no;
}
}
int? sortReportTableRows(dynamic rowA, dynamic rowB,
ReportSettingsEntity reportSettings, List<String?> columns) {
if (reportSettings.sortColumn.isEmpty) {
return 0;
}
final index = columns.indexOf(reportSettings.sortColumn);
if (index == -1) {
return 0;
} else if (rowA.length <= index || rowB.length <= index) {
return 0;
}
final dynamic valueA = rowA[index].value;
final dynamic valueB = rowB[index].value;
if (valueA is bool) {
if (reportSettings.sortAscending) {
return valueA ? 1 : -1;
} else {
return valueA ? -1 : 1;
}
}
if (reportSettings.sortAscending) {
return valueA.compareTo(valueB);
} else {
return valueB.compareTo(valueA);
}
}