575 lines
22 KiB
Dart
575 lines
22 KiB
Dart
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';
|
|
|
|
bool isDataTypesFiltered = false;
|
|
bool isBusinessesFiltered = true;
|
|
bool _isLoading = false;
|
|
|
|
class ExportData extends StatefulWidget {
|
|
final List<Business> businesses;
|
|
|
|
const ExportData({super.key, required this.businesses});
|
|
|
|
@override
|
|
State<ExportData> createState() => _ExportDataState();
|
|
}
|
|
|
|
class _ExportDataState extends State<ExportData> {
|
|
late Future refreshBusinessDataFuture;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
refreshBusinessDataFuture = fetchBusinessData();
|
|
_isLoading = false;
|
|
selectedBusinesses = <Business>{};
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
floatingActionButton: _FAB(businesses: widget.businesses),
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
SliverAppBar(
|
|
forceMaterialTransparency: false,
|
|
title: const Text('Export Data'),
|
|
toolbarHeight: 70,
|
|
pinned: true,
|
|
centerTitle: true,
|
|
expandedHeight: 120,
|
|
backgroundColor: Theme.of(context).colorScheme.background,
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.settings),
|
|
onPressed: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
// DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.background,
|
|
title: const Text('Data Types'),
|
|
content: const SizedBox(
|
|
width: 400,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'Data Columns you would like to show on the datasheet'),
|
|
FilterDataTypeChips(),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text('Reset'),
|
|
onPressed: () {
|
|
setState(() {
|
|
dataTypeFilters = <DataType>{};
|
|
selectedDataTypes = <DataType>{};
|
|
isDataTypesFiltered = false;
|
|
});
|
|
Navigator.of(context).pop();
|
|
}),
|
|
TextButton(
|
|
child: const Text('Cancel'),
|
|
onPressed: () {
|
|
selectedDataTypes = Set.from(dataTypeFilters);
|
|
Navigator.of(context).pop();
|
|
}),
|
|
TextButton(
|
|
child: const Text('Apply'),
|
|
onPressed: () {
|
|
setState(() {
|
|
selectedDataTypes =
|
|
sortDataTypes(selectedDataTypes);
|
|
dataTypeFilters =
|
|
Set.from(selectedDataTypes);
|
|
if (dataTypeFilters.isNotEmpty) {
|
|
isDataTypesFiltered = true;
|
|
} else {
|
|
isDataTypesFiltered = false;
|
|
}
|
|
});
|
|
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: TextField(
|
|
onChanged: (query) {
|
|
setState(() {
|
|
searchFilter = query;
|
|
});
|
|
},
|
|
decoration: InputDecoration(
|
|
labelText: 'Search',
|
|
hintText: 'Search',
|
|
prefixIcon: const Padding(
|
|
padding: EdgeInsets.only(left: 8.0),
|
|
child: Icon(Icons.search),
|
|
),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(25.0)),
|
|
),
|
|
suffixIcon: Padding(
|
|
padding: const EdgeInsets.only(right: 8.0),
|
|
child: IconButton(
|
|
icon: Icon(Icons.filter_list,
|
|
color: isFiltered
|
|
? Theme.of(context).colorScheme.primary
|
|
: Theme.of(context).colorScheme.onBackground),
|
|
onPressed: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
// DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree
|
|
backgroundColor: Theme.of(context)
|
|
.colorScheme
|
|
.background,
|
|
title: const Text('Filter Options'),
|
|
content: const FilterChips(),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text('Reset'),
|
|
onPressed: () {
|
|
setState(() {
|
|
filters = <BusinessType>{};
|
|
selectedChips = <BusinessType>{};
|
|
isFiltered = false;
|
|
});
|
|
Navigator.of(context).pop();
|
|
}),
|
|
TextButton(
|
|
child: const Text('Cancel'),
|
|
onPressed: () {
|
|
selectedChips = Set.from(filters);
|
|
Navigator.of(context).pop();
|
|
}),
|
|
TextButton(
|
|
child: const Text('Apply'),
|
|
onPressed: () {
|
|
setState(() {
|
|
filters = selectedChips;
|
|
if (filters.isNotEmpty) {
|
|
isFiltered = true;
|
|
} else {
|
|
isFiltered = false;
|
|
}
|
|
});
|
|
Navigator.of(context).pop();
|
|
}),
|
|
],
|
|
);
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
BusinessDisplayPanel(
|
|
businesses: widget.businesses,
|
|
widescreen: MediaQuery.sizeOf(context).width >= 1000,
|
|
selectable: true),
|
|
const SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 100,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FAB extends StatefulWidget {
|
|
final List<Business> businesses;
|
|
|
|
const _FAB({required this.businesses});
|
|
|
|
@override
|
|
State<_FAB> createState() => _FABState();
|
|
}
|
|
|
|
class _FABState extends State<_FAB> {
|
|
@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;
|
|
});
|
|
|
|
try {
|
|
DateTime dateTime = DateTime.now();
|
|
String minute = '00';
|
|
if (dateTime.minute.toString().length < 2) {
|
|
minute = '0${dateTime.minute}';
|
|
} else {
|
|
minute = dateTime.minute.toString();
|
|
}
|
|
|
|
String time = dateTime.hour <= 13
|
|
? '${dateTime.hour}:${minute}AM'
|
|
: '${dateTime.hour - 12}:${minute}PM';
|
|
String fileName =
|
|
'Business Data - ${dateTime.month}-${dateTime.day}-${dateTime.year} $time.pdf';
|
|
|
|
final pdf = pw.Document();
|
|
var svgBytes = await marinoDevLogo();
|
|
selectedDataTypes = sortDataTypes(selectedDataTypes);
|
|
|
|
List<pw.Padding> headers = [];
|
|
if (selectedDataTypes.isEmpty) {
|
|
dataTypeFilters.addAll(DataType.values);
|
|
} else {
|
|
for (var filter in selectedDataTypes) {
|
|
dataTypeFilters.add(filter);
|
|
}
|
|
}
|
|
for (var filter in dataTypeFilters) {
|
|
headers.add(pw.Padding(
|
|
child: pw.Text(dataTypeFriendly[filter]!,
|
|
style: const pw.TextStyle(fontSize: 10)),
|
|
padding: const pw.EdgeInsets.all(4.0)));
|
|
}
|
|
|
|
List<pw.TableRow> rows = [];
|
|
if (selectedBusinesses.isEmpty) {
|
|
selectedBusinesses.addAll(widget.businesses);
|
|
isBusinessesFiltered = false;
|
|
} else {
|
|
isBusinessesFiltered = true;
|
|
}
|
|
|
|
double remainingSpace = 744;
|
|
|
|
if (dataTypeFilters.contains(DataType.logo)) {
|
|
remainingSpace -= 32;
|
|
}
|
|
if (dataTypeFilters.contains(DataType.type)) {
|
|
remainingSpace -= 56;
|
|
}
|
|
if (dataTypeFilters.contains(DataType.contactName)) {
|
|
remainingSpace -= 72;
|
|
}
|
|
if (dataTypeFilters.contains(DataType.contactPhone)) {
|
|
remainingSpace -= 76;
|
|
}
|
|
|
|
double nameWidth = 0;
|
|
double websiteWidth = 0;
|
|
double contactEmailWidth = 0;
|
|
double notesWidth = 0;
|
|
double descriptionWidth = 0;
|
|
if (dataTypeFilters.contains(DataType.name)) {
|
|
nameWidth = (remainingSpace / 6);
|
|
}
|
|
if (dataTypeFilters.contains(DataType.website)) {
|
|
websiteWidth = (remainingSpace / 5);
|
|
}
|
|
if (dataTypeFilters.contains(DataType.contactEmail)) {
|
|
contactEmailWidth = (remainingSpace / 5);
|
|
}
|
|
if (dataTypeFilters.contains(DataType.notes)) {
|
|
notesWidth = (remainingSpace / 7);
|
|
}
|
|
remainingSpace -=
|
|
(nameWidth + websiteWidth + contactEmailWidth + notesWidth);
|
|
if (dataTypeFilters.contains(DataType.description)) {
|
|
descriptionWidth = remainingSpace;
|
|
}
|
|
|
|
Map<int, pw.TableColumnWidth> columnWidths = {};
|
|
|
|
int columnNum = -1;
|
|
for (var dataType in dataTypeFilters) {
|
|
pw.TableColumnWidth width = const pw.FixedColumnWidth(0);
|
|
if (dataType == DataType.logo) {
|
|
width = const pw.FixedColumnWidth(32);
|
|
columnNum++;
|
|
} else if (dataType == DataType.name) {
|
|
width = pw.FixedColumnWidth(nameWidth);
|
|
columnNum++;
|
|
} else if (dataType == DataType.description) {
|
|
width = pw.FixedColumnWidth(descriptionWidth);
|
|
columnNum++;
|
|
} else if (dataType == DataType.type) {
|
|
width = const pw.FixedColumnWidth(56);
|
|
columnNum++;
|
|
} else if (dataType == DataType.website) {
|
|
width = pw.FixedColumnWidth(websiteWidth);
|
|
columnNum++;
|
|
} else if (dataType == DataType.contactName) {
|
|
width = const pw.FixedColumnWidth(72);
|
|
columnNum++;
|
|
} else if (dataType == DataType.contactEmail) {
|
|
width = pw.FixedColumnWidth(contactEmailWidth);
|
|
columnNum++;
|
|
} else if (dataType == DataType.contactPhone) {
|
|
width = const pw.FixedColumnWidth(76);
|
|
columnNum++;
|
|
} else if (dataType == DataType.notes) {
|
|
width = pw.FixedColumnWidth(notesWidth);
|
|
columnNum++;
|
|
}
|
|
|
|
columnWidths.addAll({columnNum: width});
|
|
}
|
|
|
|
for (var business in selectedBusinesses) {
|
|
List<pw.Padding> data = [];
|
|
bool hasLogo = false;
|
|
Uint8List businessLogo = Uint8List(0);
|
|
if (dataTypeFilters.contains(DataType.logo)) {
|
|
try {
|
|
var apiLogo = await getLogo(business.id);
|
|
if (apiLogo.runtimeType != String) {
|
|
businessLogo = apiLogo;
|
|
hasLogo = true;
|
|
}
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
print('Logo not available! $e');
|
|
}
|
|
}
|
|
}
|
|
if (dataTypeFilters.contains(DataType.name)) {
|
|
data.add(pw.Padding(
|
|
child: pw.Text(
|
|
business.name,
|
|
// style: const pw.TextStyle(fontSize: 10)
|
|
),
|
|
padding: const pw.EdgeInsets.all(4.0)));
|
|
}
|
|
if (dataTypeFilters.contains(DataType.description)) {
|
|
pw.TextStyle style = const pw.TextStyle(fontSize: 9);
|
|
if (business.description.length >= 200) {
|
|
style = const pw.TextStyle(fontSize: 8);
|
|
}
|
|
if (business.description.length >= 400) {
|
|
style = const pw.TextStyle(fontSize: 7);
|
|
}
|
|
data.add(pw.Padding(
|
|
child: pw.Text(
|
|
business.description,
|
|
style: style,
|
|
),
|
|
padding: const pw.EdgeInsets.all(4.0)));
|
|
}
|
|
if (dataTypeFilters.contains(DataType.type)) {
|
|
data.add(pw.Padding(
|
|
child: pw.Text(
|
|
business.type.name,
|
|
// style: const pw.TextStyle(fontSize: 10)
|
|
),
|
|
padding: const pw.EdgeInsets.all(4.0)));
|
|
}
|
|
if (dataTypeFilters.contains(DataType.website)) {
|
|
data.add(pw.Padding(
|
|
child: pw.Text(
|
|
business.website,
|
|
// style: const pw.TextStyle(fontSize: 10)
|
|
),
|
|
padding: const pw.EdgeInsets.all(4.0)));
|
|
}
|
|
if (dataTypeFilters.contains(DataType.contactName)) {
|
|
data.add(pw.Padding(
|
|
child: pw.Text(
|
|
business.contactName,
|
|
// style: const pw.TextStyle(fontSize: 10)
|
|
),
|
|
padding: const pw.EdgeInsets.all(4.0)));
|
|
}
|
|
if (dataTypeFilters.contains(DataType.contactEmail)) {
|
|
data.add(pw.Padding(
|
|
child: pw.Text(
|
|
business.contactEmail,
|
|
// style: const pw.TextStyle(fontSize: 10)
|
|
),
|
|
padding: const pw.EdgeInsets.all(4.0)));
|
|
}
|
|
if (dataTypeFilters.contains(DataType.contactPhone)) {
|
|
data.add(pw.Padding(
|
|
child: pw.Text(
|
|
business.contactPhone,
|
|
// style: const pw.TextStyle(fontSize: 10)
|
|
),
|
|
padding: const pw.EdgeInsets.all(4.0)));
|
|
}
|
|
if (dataTypeFilters.contains(DataType.notes)) {
|
|
pw.TextStyle style = const pw.TextStyle(fontSize: 9);
|
|
if (business.description.length >= 200) {
|
|
style = const pw.TextStyle(fontSize: 8);
|
|
}
|
|
data.add(pw.Padding(
|
|
child: pw.Text(business.notes, style: style),
|
|
padding: const pw.EdgeInsets.all(4.0)));
|
|
}
|
|
|
|
if (dataTypeFilters.contains(DataType.logo)) {
|
|
if (hasLogo) {
|
|
rows.add(pw.TableRow(
|
|
children: [
|
|
pw.Padding(
|
|
child: pw.ClipRRect(
|
|
child: pw.Image(pw.MemoryImage(businessLogo),
|
|
height: 24, width: 24),
|
|
horizontalRadius: 4,
|
|
verticalRadius: 4),
|
|
padding: const pw.EdgeInsets.all(4.0)),
|
|
...data
|
|
],
|
|
));
|
|
} else {
|
|
rows.add(pw.TableRow(
|
|
children: [
|
|
pw.Padding(
|
|
child: getPwIconFromType(
|
|
business.type, 24, PdfColors.black),
|
|
padding: const pw.EdgeInsets.all(4.0)),
|
|
...data
|
|
],
|
|
));
|
|
}
|
|
} else {
|
|
rows.add(pw.TableRow(
|
|
children: data,
|
|
));
|
|
}
|
|
}
|
|
|
|
var themeIcon = pw.ThemeData.withFont(
|
|
base: await PdfGoogleFonts.notoSansDisplayMedium(),
|
|
icons: await PdfGoogleFonts.materialIcons());
|
|
|
|
var finaltheme = themeIcon.copyWith(
|
|
defaultTextStyle: const pw.TextStyle(fontSize: 9),
|
|
);
|
|
pdf.addPage(pw.MultiPage(
|
|
theme: finaltheme,
|
|
// theme: pw.ThemeData(
|
|
// tableCell: const pw.TextStyle(fontSize: 4),
|
|
// defaultTextStyle: const pw.TextStyle(fontSize: 4),
|
|
// header0: const pw.TextStyle(fontSize: 4),
|
|
// paragraphStyle: const pw.TextStyle(fontSize: 4),
|
|
// ),
|
|
// theme: pw.ThemeData.withFont(
|
|
// icons: await PdfGoogleFonts.materialIcons()),
|
|
pageFormat: PdfPageFormat.letter,
|
|
orientation: pw.PageOrientation.landscape,
|
|
margin: const pw.EdgeInsets.all(24),
|
|
build: (pw.Context context) {
|
|
return [
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.SvgImage(svg: utf8.decode(svgBytes), height: 40),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(8.0),
|
|
child: pw.Text('Business Datasheet',
|
|
style: pw.TextStyle(
|
|
fontSize: 32,
|
|
fontWeight: pw.FontWeight.bold)),
|
|
),
|
|
pw.Text(
|
|
'Generated on ${dateTime.month}/${dateTime.day}/${dateTime.year} at $time',
|
|
style: const pw.TextStyle(fontSize: 12),
|
|
textAlign: pw.TextAlign.right),
|
|
//
|
|
]),
|
|
pw.Table(
|
|
columnWidths: columnWidths,
|
|
// defaultColumnWidth: pw.IntrinsicColumnWidth(),
|
|
border: const pw.TableBorder(
|
|
bottom: pw.BorderSide(),
|
|
left: pw.BorderSide(),
|
|
right: pw.BorderSide(),
|
|
top: pw.BorderSide(),
|
|
horizontalInside: pw.BorderSide(),
|
|
verticalInside: pw.BorderSide()),
|
|
children: [
|
|
pw.TableRow(
|
|
decoration:
|
|
const pw.BoxDecoration(color: PdfColors.blue400),
|
|
children: headers,
|
|
repeat: true,
|
|
),
|
|
...rows,
|
|
]),
|
|
];
|
|
}));
|
|
|
|
Uint8List pdfBytes = await pdf.save();
|
|
|
|
if (kIsWeb) {
|
|
await Printing.sharePdf(
|
|
bytes: await pdf.save(),
|
|
filename: fileName,
|
|
);
|
|
} else {
|
|
var dir = await getTemporaryDirectory();
|
|
var tempDir = dir.path;
|
|
|
|
File pdfFile = File('$tempDir/$fileName');
|
|
pdfFile.writeAsBytesSync(pdfBytes);
|
|
|
|
OpenFilex.open(pdfFile.path);
|
|
}
|
|
|
|
if (!isBusinessesFiltered) {
|
|
selectedBusinesses = <Business>{};
|
|
}
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
content: Text('Error generating PDF! $e'),
|
|
width: 300,
|
|
behavior: SnackBarBehavior.floating,
|
|
duration: const Duration(seconds: 2),
|
|
));
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|