// import 'dart:html' as html;
import 'dart:convert';
import 'dart:io';
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/shared.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import 'package:rive/rive.dart';
class ExportData extends StatefulWidget {
final Map> groupedBusinesses;
const ExportData({super.key, required this.groupedBusinesses});
@override
State createState() => _ExportDataState();
}
class _ExportDataState extends State {
String documentType = 'Business';
late Future refreshBusinessDataFuture;
bool _isPreviousData = false;
late Map> overviewBusinesses;
Set jobTypeFilters = {};
String searchQuery = '';
Set selectedDataTypesJob = {};
Set selectedDataTypesBusiness = {};
Future _setFilters(Set filters) async {
setState(() {
jobTypeFilters = filters;
});
_updateOverviewBusinesses();
}
Future _updateOverviewBusinesses() async {
var refreshedData =
fetchBusinessDataOverview(typeFilters: jobTypeFilters.toList());
await refreshedData;
setState(() {
refreshBusinessDataFuture = refreshedData;
});
}
Future _setSearch(String search) async {
setState(() {
searchQuery = search;
});
_updateOverviewBusinesses();
}
Map> _filterBySearch(
Map> businesses) {
Map> filteredBusinesses = businesses;
for (JobType jobType in businesses.keys) {
filteredBusinesses[jobType]!.removeWhere((tmpBusiness) => !tmpBusiness
.name
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.contains(searchQuery
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.trim()));
}
filteredBusinesses.removeWhere((key, value) => value.isEmpty);
return filteredBusinesses;
}
@override
void initState() {
super.initState();
refreshBusinessDataFuture = fetchBusinessDataOverview();
selectedBusinesses = {};
}
void _setStateCallbackReset() {
setState(() {
selectedDataTypesBusiness = {};
selectedDataTypesJob = {};
documentType = 'Business';
});
}
void _setStateCallbackApply(String docType, Set dataFiltersJob,
Set dataFiltersBusiness) {
setState(() {
selectedDataTypesBusiness = dataFiltersBusiness;
selectedDataTypesJob = dataFiltersJob;
documentType = docType;
});
}
@override
Widget build(BuildContext context) {
bool widescreen = MediaQuery.sizeOf(context).width >= 1000;
return Scaffold(
floatingActionButton: _FAB(
groupedBusinesses: widget.groupedBusinesses,
documentType: documentType,
selectedDataTypesBusiness: selectedDataTypesBusiness,
selectedDataTypesJob: selectedDataTypesJob,
),
body: CustomScrollView(
slivers: [
SliverAppBar(
forceMaterialTransparency: false,
title: const Text('Export Data'),
toolbarHeight: 70,
pinned: true,
centerTitle: true,
expandedHeight: 120,
backgroundColor: Theme.of(context).colorScheme.surface,
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
Set dataFiltersBusinessTmp =
Set.from(
selectedDataTypesBusiness);
Set dataFiltersJobTmp =
Set.from(selectedDataTypesJob);
String docTypeTmp = documentType;
return StatefulBuilder(builder: (context, setState) {
void segmentedCallback(String docType) {
setState(() {
docTypeTmp = docType;
});
}
void chipsCallback(
{Set? selectedDataTypesJob,
Set?
selectedDataTypesBusiness}) {
if (selectedDataTypesJob != null) {
dataFiltersJobTmp = selectedDataTypesJob;
}
if (selectedDataTypesBusiness != null) {
dataFiltersBusinessTmp =
selectedDataTypesBusiness;
}
}
return AlertDialog(
// DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree
backgroundColor:
Theme.of(context).colorScheme.surface,
title: const Text('Export Settings'),
content: SizedBox(
width: 450,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Document Type:'),
_SegmentedButton(
callback: segmentedCallback,
docType: docTypeTmp,
),
const Text(
'Data Columns you would like to show on the datasheet:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: _FilterDataTypeChips(
docTypeTmp,
dataFiltersJobTmp,
dataFiltersBusinessTmp,
chipsCallback),
),
],
),
),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
_setStateCallbackReset();
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Apply'),
onPressed: () {
_setStateCallbackApply(
docTypeTmp,
dataFiltersJobTmp,
dataFiltersBusinessTmp);
Navigator.of(context).pop();
}),
],
);
});
});
},
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(0),
child: SizedBox(
height: 70,
width: 1000,
child: Padding(
padding: const EdgeInsets.all(10),
child: BusinessSearchBar(
filters: jobTypeFilters,
setFiltersCallback: _setFilters,
setSearchCallback: _setSearch),
),
),
),
),
FutureBuilder(
future: refreshBusinessDataFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType == String) {
_isPreviousData = false;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child: Text(snapshot.data,
textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
_updateOverviewBusinesses();
},
),
),
]),
));
}
overviewBusinesses = snapshot.data;
_isPreviousData = true;
return BusinessDisplayPanel(
groupedBusinesses: _filterBySearch(overviewBusinesses),
widescreen: widescreen,
selectable: true);
} else if (snapshot.hasError) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
));
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
if (_isPreviousData) {
return BusinessDisplayPanel(
groupedBusinesses: _filterBySearch(overviewBusinesses),
widescreen: widescreen,
selectable: true);
} else {
return SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child: RiveAnimation.asset(
'assets/mdev_triangle_loading.riv'),
),
));
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
}),
const SliverToBoxAdapter(
child: SizedBox(
height: 100,
),
),
],
),
);
}
}
class _SegmentedButton extends StatefulWidget {
final void Function(String) callback;
final String docType;
const _SegmentedButton({required this.callback, required this.docType});
@override
State<_SegmentedButton> createState() => _SegmentedButtonState();
}
class _SegmentedButtonState extends State<_SegmentedButton> {
Set _selected = {};
void updateSelected(Set newSelection) {
setState(() {
_selected = newSelection;
});
widget.callback(newSelection.first);
}
@override
void initState() {
super.initState();
_selected = {widget.docType};
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: SegmentedButton(
segments: const >[
ButtonSegment(
value: 'Business',
label: Text('Businesses'),
icon: Icon(Icons.business)),
ButtonSegment(
value: 'Job Listing',
label: Text('Job Listings'),
icon: Icon(Icons.work))
],
selected: _selected,
onSelectionChanged: updateSelected,
style: SegmentedButton.styleFrom(
side: BorderSide(color: Theme.of(context).colorScheme.secondary),
)),
);
}
}
class _FAB extends StatefulWidget {
final String documentType;
final Map> groupedBusinesses;
final Set selectedDataTypesJob;
final Set selectedDataTypesBusiness;
const _FAB(
{required this.groupedBusinesses,
required this.documentType,
required this.selectedDataTypesJob,
required this.selectedDataTypesBusiness});
@override
State<_FAB> createState() => _FABState();
}
class _FABState extends State<_FAB> {
List allBusinesses = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
for (JobType type in widget.groupedBusinesses.keys) {
allBusinesses.addAll(widget.groupedBusinesses[type]!);
}
}
@override
Widget build(BuildContext context) {
return FloatingActionButton(
child: _isLoading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save_alt),
onPressed: () async {
setState(() {
_isLoading = true;
});
Set generateBusinesses = {};
if (selectedBusinesses.isEmpty) {
generateBusinesses = Set.from(allBusinesses);
} else {
generateBusinesses = selectedBusinesses;
}
await _generatePDF(context, widget.documentType, generateBusinesses,
widget.selectedDataTypesBusiness, widget.selectedDataTypesJob);
setState(() {
_isLoading = false;
});
});
}
}
class _FilterDataTypeChips extends StatefulWidget {
final String documentType;
final Set selectedDataTypesJob;
final Set selectedDataTypesBusiness;
final void Function(
{Set? selectedDataTypesJob,
Set? selectedDataTypesBusiness}) updateCallback;
const _FilterDataTypeChips(this.documentType, this.selectedDataTypesJob,
this.selectedDataTypesBusiness, this.updateCallback);
@override
State<_FilterDataTypeChips> createState() => _FilterDataTypeChipsState();
}
class _FilterDataTypeChipsState extends State<_FilterDataTypeChips> {
List filterDataTypeChips() {
List chips = [];
if (widget.documentType == 'Business') {
for (var type in DataTypeBusiness.values) {
chips.add(Padding(
padding: const EdgeInsets.only(
left: 3.0, right: 3.0, bottom: 3.0, top: 3.0),
child: FilterChip(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: Theme.of(context).colorScheme.secondary)),
label: Text(dataTypeFriendlyBusiness[type]!),
showCheckmark: false,
selected: widget.selectedDataTypesBusiness.contains(type),
onSelected: (bool selected) {
setState(() {
if (selected) {
widget.selectedDataTypesBusiness.add(type);
} else {
widget.selectedDataTypesBusiness.remove(type);
}
});
widget.updateCallback(
selectedDataTypesBusiness:
widget.selectedDataTypesBusiness);
}),
));
}
} else if (widget.documentType == 'Job Listing') {
for (var type in DataTypeJob.values) {
chips.add(Padding(
padding: const EdgeInsets.only(
left: 3.0, right: 3.0, bottom: 3.0, top: 3.0),
child: FilterChip(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: Theme.of(context).colorScheme.secondary)),
label: Text(dataTypeFriendlyJob[type]!),
showCheckmark: false,
selected: widget.selectedDataTypesJob.contains(type),
onSelected: (bool selected) {
setState(() {
if (selected) {
widget.selectedDataTypesJob.add(type);
} else {
widget.selectedDataTypesJob.remove(type);
}
});
widget.updateCallback(
selectedDataTypesJob: widget.selectedDataTypesJob);
}),
));
}
}
return chips;
}
@override
Widget build(BuildContext context) {
return Wrap(
children: filterDataTypeChips(),
);
}
}
Future _generatePDF(
BuildContext context,
String documentType,
Set? selectedBusinesses,
Set? dataTypesBusinessInput,
Set? dataTypesJobInput) async {
Set dataTypesBusiness = {};
Set dataTypesJob = {};
List headerColumns = [];
List tableRows = [];
List businesses = await fetchBusinesses(
selectedBusinesses!.map((business) => business.id).toList());
if (documentType == 'Business') {
dataTypesBusiness = Set.from(dataTypesBusinessInput!);
if (dataTypesBusiness.isEmpty) {
dataTypesBusiness.addAll(DataTypeBusiness.values);
}
dataTypesBusiness = sortDataTypesBusiness(dataTypesBusiness);
for (Business business in businesses) {
List businessRow = [];
if (dataTypesBusiness.contains(DataTypeBusiness.logo)) {
var apiLogo = await getLogo(business.id);
if (apiLogo.runtimeType != String) {
businessRow.add(pw.Padding(
child: pw.ClipRRect(
child:
pw.Image(pw.MemoryImage(apiLogo), height: 24, width: 24),
horizontalRadius: 4,
verticalRadius: 4),
padding: const pw.EdgeInsets.all(4.0)));
} else {
businessRow.add(pw.Padding(
child: pw.Icon(const pw.IconData(0xe0af), size: 24),
padding: const pw.EdgeInsets.all(4.0)));
}
}
for (DataTypeBusiness dataType in dataTypesBusiness) {
if (dataType != DataTypeBusiness.logo) {
businessRow.add(pw.Padding(
child: pw.Text(businessValueFromDataType(business, dataType)),
padding: const pw.EdgeInsets.all(4.0)));
}
}
tableRows.add(pw.TableRow(children: businessRow));
}
for (var filter in dataTypesBusiness) {
headerColumns.add(pw.Padding(
child: pw.Text(dataTypeFriendlyBusiness[filter]!,
style: const pw.TextStyle(fontSize: 10)),
padding: const pw.EdgeInsets.all(4.0)));
}
} else if (documentType == 'Job Listing') {
dataTypesJob = Set.from(dataTypesJobInput!);
if (dataTypesJob.isEmpty) {
dataTypesJob.addAll(DataTypeJob.values);
}
List dataTypesJobList =
sortDataTypesJob(dataTypesJob).toList();
List