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 Map> groupedBusinesses; const ExportData({super.key, required this.groupedBusinesses}); @override State createState() => _ExportDataState(); } class _ExportDataState extends State { late Future refreshBusinessDataFuture; @override void initState() { super.initState(); refreshBusinessDataFuture = fetchBusinessDataOverview(); _isLoading = false; selectedBusinesses = {}; } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: _FAB(groupedBusinesses: widget.groupedBusinesses), 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 = {}; selectedDataTypes = {}; 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 = {}; selectedChips = {}; 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( groupedBusinesses: widget.groupedBusinesses, widescreen: MediaQuery.sizeOf(context).width >= 1000, selectable: true), const SliverToBoxAdapter( child: SizedBox( height: 100, ), ), ], ), ); } } class _FAB extends StatefulWidget { final Map> groupedBusinesses; const _FAB({required this.groupedBusinesses}); @override State<_FAB> createState() => _FABState(); } class _FABState extends State<_FAB> { List allBusinesses = []; @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; }); 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 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 rows = []; if (selectedBusinesses.isEmpty) { selectedBusinesses.addAll(allBusinesses); 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 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 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 = {}; } 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), )); } }, ); } }