diff --git a/fbla_ui/lib/pages/export_data.dart b/fbla_ui/lib/pages/export_data.dart deleted file mode 100644 index a80e5e0..0000000 --- a/fbla_ui/lib/pages/export_data.dart +++ /dev/null @@ -1,850 +0,0 @@ -// 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> nameMapping = await fetchBusinessNames(); - - for (Business business in businesses) { - for (JobListing job in business.listings!) { - List jobRow = []; - for (DataTypeJob dataType in dataTypesJobList) { - jobRow.add(pw.Padding( - child: pw.Text(jobValueFromDataType(job, dataType, nameMapping)), - padding: const pw.EdgeInsets.all(4.0))); - } - tableRows.add(pw.TableRow(children: jobRow)); - } - } - - for (var filter in dataTypesJobList) { - headerColumns.add(pw.Padding( - child: pw.Text(dataTypeFriendlyJob[filter]!, - style: const pw.TextStyle(fontSize: 10)), - padding: const pw.EdgeInsets.all(4.0))); - } - } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text( - 'Could not identify document type! Please select a type in the generation settings.'))); - return; - } - - // Final Generation - 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 <= 12 - ? '${dateTime.hour}:${minute}AM' - : '${dateTime.hour - 12}:${minute}PM'; - String fileName = - '$documentType Data - ${dateTime.month}-${dateTime.day}-${dateTime.year} $time.pdf'; - - final pdf = pw.Document(); - var svgBytes = await marinoDevLogo(); - - 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, - 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('$documentType 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: documentType == 'Business' - ? _businessColumnSizes(dataTypesBusiness) - : _jobColumnSizes(dataTypesJob), - 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: headerColumns, - repeat: true, - ), - ...tableRows, - ]) - ]; - })); - - Uint8List pdfBytes = await pdf.save(); - - if (kIsWeb) { - // List fileInts = List.from(pdfBytes); - // html.AnchorElement( - // href: - // 'data:application/octet-stream;charset=utf-16le;base64,${base64.encode(fileInts)}') - // ..setAttribute('download', fileName) - // ..click(); - - 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); - } -} - -Map _businessColumnSizes( - Set dataTypes) { - double space = 744.0; - Map map = {}; - - if (dataTypes.contains(DataTypeBusiness.logo)) { - space -= 28; - map.addAll({ - dataTypePriorityBusiness[DataTypeBusiness.logo]!: - const pw.FixedColumnWidth(28) - }); - } - if (dataTypes.contains(DataTypeBusiness.contactName)) { - space -= 72; - map.addAll({ - dataTypePriorityBusiness[DataTypeBusiness.contactName]!: - const pw.FixedColumnWidth(72) - }); - } - if (dataTypes.contains(DataTypeBusiness.contactPhone)) { - space -= 76; - map.addAll({ - dataTypePriorityBusiness[DataTypeBusiness.contactPhone]!: - const pw.FixedColumnWidth(76) - }); - } - double leftNum = 0; - if (dataTypes.contains(DataTypeBusiness.name)) { - leftNum += 1; - } - if (dataTypes.contains(DataTypeBusiness.website)) { - leftNum += 1; - } - if (dataTypes.contains(DataTypeBusiness.contactEmail)) { - leftNum += 1; - } - if (dataTypes.contains(DataTypeBusiness.notes)) { - leftNum += 2; - } - if (dataTypes.contains(DataTypeBusiness.description)) { - leftNum += 3; - } - leftNum = space / leftNum; - if (dataTypes.contains(DataTypeBusiness.name)) { - map.addAll({ - dataTypePriorityBusiness[DataTypeBusiness.name]!: - pw.FixedColumnWidth(leftNum) - }); - } - if (dataTypes.contains(DataTypeBusiness.website)) { - map.addAll({ - dataTypePriorityBusiness[DataTypeBusiness.website]!: - pw.FixedColumnWidth(leftNum) - }); - } - if (dataTypes.contains(DataTypeBusiness.contactEmail)) { - map.addAll({ - dataTypePriorityBusiness[DataTypeBusiness.contactEmail]!: - pw.FixedColumnWidth(leftNum) - }); - } - if (dataTypes.contains(DataTypeBusiness.notes)) { - map.addAll({ - dataTypePriorityBusiness[DataTypeBusiness.notes]!: - pw.FixedColumnWidth(leftNum * 2) - }); - } - if (dataTypes.contains(DataTypeBusiness.description)) { - map.addAll({ - dataTypePriorityBusiness[DataTypeBusiness.description]!: - pw.FixedColumnWidth(leftNum * 3) - }); - } - - return map; -} - -Map _jobColumnSizes(Set dataTypes) { - Map map = {}; - List sortedDataTypes = sortDataTypesJob(dataTypes).toList(); - - if (dataTypes.contains(DataTypeJob.businessName)) { - map.addAll({ - sortedDataTypes.indexOf(sortedDataTypes - .where((element) => element == DataTypeJob.businessName) - .first): const pw.FractionColumnWidth(0.2) - }); - } - if (dataTypes.contains(DataTypeJob.name)) { - map.addAll({ - sortedDataTypes.indexOf(sortedDataTypes - .where((element) => element == DataTypeJob.name) - .first): const pw.FractionColumnWidth(0.2) - }); - } - if (dataTypes.contains(DataTypeJob.description)) { - map.addAll({ - sortedDataTypes.indexOf(sortedDataTypes - .where((element) => element == DataTypeJob.description) - .first): const pw.FractionColumnWidth(0.4) - }); - } - if (dataTypes.contains(DataTypeJob.wage)) { - map.addAll({ - sortedDataTypes.indexOf(sortedDataTypes - .where((element) => element == DataTypeJob.wage) - .first): const pw.FractionColumnWidth(0.15) - }); - } - if (dataTypes.contains(DataTypeJob.link)) { - map.addAll({ - sortedDataTypes.indexOf(sortedDataTypes - .where((element) => element == DataTypeJob.link) - .first): const pw.FractionColumnWidth(0.2) - }); - } - - return map; -} - -dynamic businessValueFromDataType( - Business business, DataTypeBusiness dataType) { - switch (dataType) { - case DataTypeBusiness.name: - return business.name; - case DataTypeBusiness.description: - return business.description; - case DataTypeBusiness.website: - return business.website; - case DataTypeBusiness.contactName: - return business.contactName; - case DataTypeBusiness.contactEmail: - return business.contactEmail; - case DataTypeBusiness.contactPhone: - return business.contactPhone; - case DataTypeBusiness.notes: - return business.notes; - case DataTypeBusiness.logo: - return null; - } -} - -dynamic jobValueFromDataType(JobListing job, DataTypeJob dataType, - List> nameMapping) { - switch (dataType) { - case DataTypeJob.name: - return job.name; - case DataTypeJob.description: - return job.description; - case DataTypeJob.wage: - return job.wage; - case DataTypeJob.link: - return job.link; - case DataTypeJob.businessName: - return nameMapping - .where((element) => element['id'] == job.businessId) - .first['name']; - } -} diff --git a/fbla_ui/lib/shared.dart b/fbla_ui/lib/shared.dart deleted file mode 100644 index 2ec4339..0000000 --- a/fbla_ui/lib/shared.dart +++ /dev/null @@ -1,951 +0,0 @@ -import 'package:fbla_ui/api_logic.dart'; -import 'package:fbla_ui/pages/business_detail.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -import 'package:pdf/pdf.dart'; -import 'package:pdf/widgets.dart' as pw; -import 'package:sliver_tools/sliver_tools.dart'; -import 'package:url_launcher/url_launcher.dart'; - -late String jwt; -String searchFilter = ''; -Set selectedBusinesses = {}; - -enum DataTypeBusiness { - logo, - name, - description, - website, - contactName, - contactEmail, - contactPhone, - notes, -} - -enum DataTypeJob { - businessName, - name, - description, - wage, - link, -} - -Map dataTypePriorityBusiness = { - DataTypeBusiness.logo: 0, - DataTypeBusiness.name: 1, - DataTypeBusiness.description: 2, - // DataType.type: 3, - DataTypeBusiness.website: 4, - DataTypeBusiness.contactName: 5, - DataTypeBusiness.contactEmail: 6, - DataTypeBusiness.contactPhone: 7, - DataTypeBusiness.notes: 8 -}; - -Map dataTypeFriendlyBusiness = { - DataTypeBusiness.logo: 'Logo', - DataTypeBusiness.name: 'Name', - DataTypeBusiness.description: 'Description', - // DataType.type: 'Type', - DataTypeBusiness.website: 'Website', - DataTypeBusiness.contactName: 'Contact Name', - DataTypeBusiness.contactEmail: 'Contact Email', - DataTypeBusiness.contactPhone: 'Contact Phone', - DataTypeBusiness.notes: 'Notes' -}; - -Map dataTypePriorityJob = { - DataTypeJob.businessName: 1, - DataTypeJob.name: 2, - DataTypeJob.description: 3, - DataTypeJob.wage: 4, - DataTypeJob.link: 5, -}; - -Map dataTypeFriendlyJob = { - DataTypeJob.businessName: 'Business Name', - DataTypeJob.name: 'Listing Name', - DataTypeJob.description: 'Description', - DataTypeJob.wage: 'Wage', - DataTypeJob.link: 'Link', -}; - -Set sortDataTypesBusiness(Set set) { - List list = set.toList(); - list.sort((a, b) { - return dataTypePriorityBusiness[a]!.compareTo(dataTypePriorityBusiness[b]!); - }); - set = list.toSet(); - return set; -} - -Set sortDataTypesJob(Set set) { - List list = set.toList(); - list.sort((a, b) { - return dataTypePriorityJob[a]!.compareTo(dataTypePriorityJob[b]!); - }); - set = list.toSet(); - return set; -} - -enum BusinessType { - food, - shop, - outdoors, - manufacturing, - entertainment, - other, -} - -enum JobType { cashier, server, mechanic, other } - -class JobListing { - int? id; - int? businessId; - String name; - String description; - JobType type; - String? wage; - String? link; - - JobListing( - {this.id, - this.businessId, - required this.name, - required this.description, - required this.type, - this.wage, - this.link}); - - factory JobListing.copy(JobListing input) { - return JobListing( - id: input.id, - businessId: input.businessId, - name: input.name, - description: input.description, - type: input.type, - wage: input.wage, - link: input.link, - ); - } -} - -class Business { - int id; - String name; - String description; - String website; - String? contactName; - String contactEmail; - String? contactPhone; - String? notes; - String locationName; - String? locationAddress; - List? listings; - - Business( - {required this.id, - required this.name, - required this.description, - required this.website, - this.contactName, - required this.contactEmail, - this.contactPhone, - this.notes, - required this.locationName, - this.locationAddress, - this.listings}); - - factory Business.fromJson(Map json) { - List? listings; - if (json['listings'] != null) { - listings = []; - for (int i = 0; i < json['listings'].length; i++) { - listings.add(JobListing( - id: json['listings'][i]['id'], - businessId: json['listings'][i]['businessId'], - name: json['listings'][i]['name'], - description: json['listings'][i]['description'], - type: JobType.values.byName(json['listings'][i]['type']), - wage: json['listings'][i]['wage'], - link: json['listings'][i]['link'])); - } - } - - return Business( - id: json['id'], - name: json['name'], - description: json['description'], - website: json['website'], - contactName: json['contactName'], - contactEmail: json['contactEmail'], - contactPhone: json['contactPhone'], - notes: json['notes'], - locationName: json['locationName'], - locationAddress: json['locationAddress'], - listings: listings); - } - - factory Business.copy(Business input) { - return Business( - id: input.id, - name: input.name, - description: input.description, - website: input.website, - contactName: input.contactName, - contactEmail: input.contactEmail, - contactPhone: input.contactPhone, - notes: input.notes, - locationName: input.locationName, - locationAddress: input.locationAddress, - listings: input.listings); - } -} - -// Map> groupBusinesses(List businesses) { -// Map> groupedBusinesses = -// groupBy(businesses, (business) => business.type!); -// -// return groupedBusinesses; -// } - -Icon getIconFromBusinessType(BusinessType type, double size, Color color) { - switch (type) { - case BusinessType.food: - return Icon( - Icons.restaurant, - size: size, - color: color, - ); - case BusinessType.shop: - return Icon( - Icons.store, - size: size, - color: color, - ); - case BusinessType.outdoors: - return Icon( - Icons.forest, - size: size, - color: color, - ); - case BusinessType.manufacturing: - return Icon( - Icons.factory, - size: size, - color: color, - ); - case BusinessType.entertainment: - return Icon( - Icons.live_tv, - size: size, - color: color, - ); - case BusinessType.other: - return Icon( - Icons.business, - size: size, - color: color, - ); - } -} - -Icon getIconFromJobType(JobType type, double size, Color color) { - switch (type) { - case JobType.cashier: - return Icon( - Icons.shopping_bag, - size: size, - color: color, - ); - case JobType.server: - return Icon( - Icons.restaurant, - size: size, - color: color, - ); - case JobType.mechanic: - return Icon( - Icons.construction, - size: size, - color: color, - ); - case JobType.other: - return Icon( - Icons.work, - size: size, - color: color, - ); - } -} - -pw.Icon getPwIconFromBusinessType( - BusinessType type, double size, PdfColor color) { - switch (type) { - case BusinessType.food: - return pw.Icon(const pw.IconData(0xe56c), size: size, color: color); - case BusinessType.shop: - return pw.Icon(const pw.IconData(0xea12), size: size, color: color); - case BusinessType.outdoors: - return pw.Icon(const pw.IconData(0xea99), size: size, color: color); - case BusinessType.manufacturing: - return pw.Icon(const pw.IconData(0xebbc), size: size, color: color); - case BusinessType.entertainment: - return pw.Icon(const pw.IconData(0xe639), size: size, color: color); - case BusinessType.other: - return pw.Icon(const pw.IconData(0xe0af), size: size, color: color); - } -} - -pw.Icon getPwIconFromJobType(JobType type, double size, PdfColor color) { - switch (type) { - case JobType.cashier: - return pw.Icon(const pw.IconData(0xf1cc), size: size, color: color); - case JobType.server: - return pw.Icon(const pw.IconData(0xe56c), size: size, color: color); - case JobType.mechanic: - return pw.Icon(const pw.IconData(0xea3c), size: size, color: color); - case JobType.other: - return pw.Icon(const pw.IconData(0xe8f9), size: size, color: color); - } -} - -String getNameFromBusinessType(BusinessType type) { - switch (type) { - case BusinessType.food: - return 'Food Related'; - case BusinessType.shop: - return 'Shops'; - case BusinessType.outdoors: - return 'Outdoors'; - case BusinessType.manufacturing: - return 'Manufacturing'; - case BusinessType.entertainment: - return 'Entertainment'; - case BusinessType.other: - return 'Other'; - } -} - -String getNameFromJobType(JobType type) { - switch (type) { - case JobType.cashier: - return 'Cashier'; - case JobType.server: - return 'Server'; - case JobType.mechanic: - return 'Mechanic'; - case JobType.other: - return 'Other'; - } -} - -Icon getIconFromThemeMode(ThemeMode theme) { - switch (theme) { - case ThemeMode.dark: - return const Icon(Icons.dark_mode); - case ThemeMode.light: - return const Icon(Icons.light_mode); - case ThemeMode.system: - return const Icon(Icons.brightness_4); - } -} - -class BusinessDisplayPanel extends StatefulWidget { - final Map> groupedBusinesses; - final bool widescreen; - final bool selectable; - - const BusinessDisplayPanel( - {super.key, - required this.groupedBusinesses, - required this.widescreen, - required this.selectable}); - - @override - State createState() => _BusinessDisplayPanelState(); -} - -class _BusinessDisplayPanelState extends State { - Set selectedBusinesses = {}; - - @override - Widget build(BuildContext context) { - List headers = []; - // List filteredBusinesses = []; - // for (var business in widget.groupedBusinesses.) { - // if (business.name.toLowerCase().contains(searchFilter.toLowerCase())) { - // filteredBusinesses.add(business); - // } - // } - - // if (filters.isNotEmpty) { - // isFiltered = true; - // } - - // for (var i = 0; i < businessTypes.length; i++) { - // if (filters.contains(businessTypes[i])) { - // isFiltered = true; - // } - // } - - // if (isFiltered) { - // for (JobType jobType in widget.groupedBusinesses.keys) { - // if (filters.contains(jobType)) { - // headers.add(BusinessHeader( - // type: jobType, - // widescreen: widget.widescreen, - // selectable: widget.selectable, - // selectedBusinesses: selectedBusinesses, - // businesses: widget.groupedBusinesses[jobType]!)); - // } - // } - // } else { - for (JobType jobType in widget.groupedBusinesses.keys) { - headers.add(BusinessHeader( - type: jobType, - widescreen: widget.widescreen, - selectable: widget.selectable, - selectedBusinesses: selectedBusinesses, - businesses: widget.groupedBusinesses[jobType]!)); - } - // } - headers.sort((a, b) => a.type.index.compareTo(b.type.index)); - return MultiSliver(children: headers); - } -} - -class BusinessHeader extends StatefulWidget { - final JobType type; - final List businesses; - final Set selectedBusinesses; - final bool widescreen; - final bool selectable; - - const BusinessHeader({ - super.key, - required this.type, - required this.businesses, - required this.selectedBusinesses, - required this.widescreen, - required this.selectable, - }); - - @override - State createState() => _BusinessHeaderState(); -} - -class _BusinessHeaderState extends State { - refresh() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - return SliverStickyHeader( - header: Container( - height: 55.0, - color: Theme.of(context).colorScheme.primary, - padding: const EdgeInsets.symmetric(horizontal: 16.0), - alignment: Alignment.centerLeft, - child: _getHeaderRow(widget.selectable), - ), - sliver: _getChildSliver( - widget.businesses, widget.widescreen, widget.selectable), - ); - } - - Widget _getHeaderRow(bool selectable) { - if (selectable) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 4.0, right: 12.0), - child: getIconFromJobType( - widget.type, 24, Theme.of(context).colorScheme.onPrimary), - ), - Text(getNameFromJobType(widget.type)), - ], - ), - Padding( - padding: const EdgeInsets.only(right: 12.0), - child: Checkbox( - checkColor: Theme.of(context).colorScheme.primary, - activeColor: Theme.of(context).colorScheme.onPrimary, - value: selectedBusinesses.containsAll(widget.businesses), - onChanged: (value) { - if (value!) { - setState(() { - selectedBusinesses.addAll(widget.businesses); - }); - } else { - setState(() { - selectedBusinesses.removeAll(widget.businesses); - }); - } - }, - ), - ), - ], - ); - } else { - return Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 4.0, right: 12.0), - child: getIconFromJobType( - widget.type, 24, Theme.of(context).colorScheme.onPrimary), - ), - Text( - getNameFromJobType(widget.type), - style: TextStyle(color: Theme.of(context).colorScheme.onPrimary), - ), - ], - ); - } - } - - Widget _getChildSliver( - List businesses, bool widescreen, bool selectable) { - if (widescreen) { - return SliverGrid( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - mainAxisExtent: 250.0, - maxCrossAxisExtent: 400.0, - mainAxisSpacing: 10.0, - crossAxisSpacing: 10.0, - // childAspectRatio: 4.0, - ), - delegate: SliverChildBuilderDelegate( - childCount: businesses.length, - (BuildContext context, int index) { - return BusinessCard( - business: businesses[index], - selectable: selectable, - widescreen: widescreen, - callback: refresh, - type: widget.type, - ); - }, - ), - ); - } else { - return SliverList( - delegate: SliverChildBuilderDelegate( - childCount: businesses.length, - (BuildContext context, int index) { - return BusinessCard( - business: businesses[index], - selectable: selectable, - widescreen: widescreen, - callback: refresh, - type: widget.type, - ); - }, - ), - ); - } - } -} - -class BusinessCard extends StatefulWidget { - final Business business; - final bool widescreen; - final bool selectable; - final Function callback; - final JobType type; - - const BusinessCard( - {super.key, - required this.business, - required this.widescreen, - required this.selectable, - required this.callback, - required this.type}); - - @override - State createState() => _BusinessCardState(); -} - -class _BusinessCardState extends State { - @override - Widget build(BuildContext context) { - if (widget.widescreen) { - return _businessTile(widget.business, widget.selectable, widget.type); - } else { - return _businessListItem( - widget.business, widget.selectable, widget.callback, widget.type); - } - } - - Widget _businessTile(Business business, bool selectable, JobType type) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => BusinessDetail( - id: business.id, - name: business.name, - clickFromType: type, - ))); - }, - child: Card( - clipBehavior: Clip.antiAlias, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _getTileRow(business, selectable, widget.callback, type), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - business.description, - maxLines: selectable ? 7 : 5, - overflow: TextOverflow.ellipsis, - ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.all(8.0), - child: !selectable - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - icon: const Icon(Icons.link), - onPressed: () { - launchUrl( - Uri.parse('https://${business.website}')); - }, - ), - if (business.locationName != '') - IconButton( - icon: const Icon(Icons.location_on), - onPressed: () { - launchUrl(Uri.parse(Uri.encodeFull( - 'https://www.google.com/maps/search/?api=1&query=${business.locationName}'))); - }, - ), - if ((business.contactPhone != null) && - (business.contactPhone != '')) - IconButton( - icon: const Icon(Icons.phone), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: Theme.of(context) - .colorScheme - .surface, - title: Text((business.contactName == - null || - business.contactName == '') - ? 'Contact ${business.name}?' - : 'Contact ${business.contactName}'), - content: Text((business.contactName == - null || - business.contactName == '') - ? 'Would you like to call or text ${business.name}?' - : 'Would you like to call or text ${business.contactName}?'), - actions: [ - TextButton( - child: const Text('Text'), - onPressed: () { - launchUrl(Uri.parse( - 'sms:${business.contactPhone}')); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Call'), - onPressed: () async { - launchUrl(Uri.parse( - 'tel:${business.contactPhone}')); - Navigator.of(context).pop(); - }), - ], - ); - }); - }, - ), - if (business.contactEmail != '') - IconButton( - icon: const Icon(Icons.email), - onPressed: () { - launchUrl(Uri.parse( - 'mailto:${business.contactEmail}')); - }, - ), - ], - ) - : null), - ], - ), - ), - ), - ); - } - - Widget _getTileRow( - Business business, bool selectable, Function callback, JobType type) { - if (selectable) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(6.0), - child: Image.network('$apiAddress/logos/${business.id}', - height: 48, width: 48, errorBuilder: (BuildContext context, - Object exception, StackTrace? stackTrace) { - return getIconFromJobType( - type, 48, Theme.of(context).colorScheme.onSurface); - }), - ), - ), - Flexible( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - business.name, - style: - const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 24.0), - child: _checkbox(callback), - ) - ], - ); - } else { - return Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(6.0), - child: Image.network('$apiAddress/logos/${business.id}', - height: 48, width: 48, errorBuilder: (BuildContext context, - Object exception, StackTrace? stackTrace) { - return getIconFromJobType( - type, 48, Theme.of(context).colorScheme.onSurface); - }), - )), - Flexible( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - business.name, - style: - const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ); - } - } - - Widget _businessListItem( - Business business, bool selectable, Function callback, JobType type) { - return Card( - child: ListTile( - leading: ClipRRect( - borderRadius: BorderRadius.circular(3.0), - child: Image.network('$apiAddress/logos/${business.id}', - height: 24, width: 24, errorBuilder: (BuildContext context, - Object exception, StackTrace? stackTrace) { - return getIconFromJobType( - type, 24, Theme.of(context).colorScheme.onSurface); - })), - title: Text(business.name), - subtitle: Text(business.description, - maxLines: 1, overflow: TextOverflow.ellipsis), - trailing: _getCheckbox(selectable, callback), - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => BusinessDetail( - id: business.id, - name: business.name, - clickFromType: type, - ))); - }, - ), - ); - } - - Widget _checkbox(Function callback) { - return Checkbox( - value: selectedBusinesses.contains(widget.business), - onChanged: (value) { - if (value!) { - setState(() { - selectedBusinesses.add(widget.business); - }); - } else { - setState(() { - selectedBusinesses.remove(widget.business); - }); - } - callback(); - }, - ); - } - - Widget? _getCheckbox(bool selectable, Function callback) { - if (selectable) { - return _checkbox(callback); - } else { - return null; - } - } -} - -class BusinessSearchBar extends StatefulWidget { - final Set filters; - final Future Function(Set) setFiltersCallback; - final Future Function(String) setSearchCallback; - - const BusinessSearchBar( - {super.key, - required this.filters, - required this.setFiltersCallback, - required this.setSearchCallback}); - - @override - State createState() => _BusinessSearchBarState(); -} - -class _BusinessSearchBarState extends State { - bool isFiltered = false; - - @override - Widget build(BuildContext context) { - Set selectedChips = Set.from(widget.filters); - return SizedBox( - width: 800, - height: 50, - child: SearchBar( - backgroundColor: WidgetStateProperty.resolveWith((notNeeded) { - return Theme.of(context).colorScheme.surfaceContainer; - }), - onChanged: (query) { - widget.setSearchCallback(query); - }, - leading: const Padding( - padding: EdgeInsets.only(left: 8.0), - child: Icon(Icons.search), - ), - trailing: [ - IconButton( - tooltip: 'Filters', - icon: Icon(Icons.filter_list, - color: isFiltered - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - // DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree - title: const Text('Filter Options'), - content: FilterChips( - selectedChips: selectedChips, - ), - actions: [ - TextButton( - child: const Text('Reset'), - onPressed: () async { - setState(() { - selectedChips = {}; - isFiltered = false; - }); - widget.setFiltersCallback({}); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Cancel'), - onPressed: () { - selectedChips = Set.from(widget.filters); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Apply'), - onPressed: () async { - widget.setFiltersCallback( - Set.from(selectedChips)); - if (selectedChips.isNotEmpty) { - setState(() { - isFiltered = true; - }); - } else { - setState(() { - isFiltered = false; - }); - } - Navigator.of(context).pop(); - }), - ], - ); - }); - }, - ) - ]), - ); - } -} - -class FilterChips extends StatefulWidget { - final Set selectedChips; - - const FilterChips({super.key, required this.selectedChips}); - - @override - State createState() => _FilterChipsState(); -} - -class _FilterChipsState extends State { - List filterChips() { - List chips = []; - - for (var type in JobType.values) { - chips.add(Padding( - padding: const EdgeInsets.only(left: 4.0, right: 4.0), - child: FilterChip( - showCheckmark: false, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - label: Text(getNameFromJobType(type)), - selected: widget.selectedChips.contains(type), - onSelected: (bool selected) { - setState(() { - if (selected) { - widget.selectedChips.add(type); - } else { - widget.selectedChips.remove(type); - } - }); - }), - )); - } - return chips; - } - - @override - Widget build(BuildContext context) { - return Wrap( - children: filterChips(), - ); - } -} diff --git a/fbla_ui/lib/api_logic.dart b/fbla_ui/lib/shared/api_logic.dart similarity index 77% rename from fbla_ui/lib/api_logic.dart rename to fbla_ui/lib/shared/api_logic.dart index 18e3f55..89203ff 100644 --- a/fbla_ui/lib/api_logic.dart +++ b/fbla_ui/lib/shared/api_logic.dart @@ -2,7 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:fbla_ui/shared.dart'; +import 'package:fbla_ui/shared/global_vars.dart'; +import 'package:fbla_ui/shared/utils.dart'; import 'package:http/http.dart' as http; var apiAddress = 'https://homelab.marinodev.com/fbla-api'; @@ -49,14 +50,14 @@ Future fetchBusinessNames() async { } } -Future fetchBusinessDataOverview({List? typeFilters}) async { +Future fetchBusinessDataOverviewJobs({List? typeFilters}) async { try { String? typeString = typeFilters?.map((jobType) => jobType.name).toList().join(','); Uri uri = - Uri.parse('$apiAddress/businessdata/overview?filters=$typeString'); + Uri.parse('$apiAddress/businessdata/overview/jobs?filters=$typeString'); if (typeFilters == null || typeFilters.isEmpty) { - uri = Uri.parse('$apiAddress/businessdata/overview'); + uri = Uri.parse('$apiAddress/businessdata/overview/jobs'); } var response = await http.get(uri).timeout(const Duration(seconds: 20)); if (response.statusCode == 200) { @@ -85,6 +86,43 @@ Future fetchBusinessDataOverview({List? typeFilters}) async { } } +Future fetchBusinessDataOverviewTypes({List? typeFilters}) async { + try { + String? typeString = + typeFilters?.map((jobType) => jobType.name).toList().join(','); + Uri uri = Uri.parse( + '$apiAddress/businessdata/overview/types?filters=$typeString'); + if (typeFilters == null || typeFilters.isEmpty) { + uri = Uri.parse('$apiAddress/businessdata/overview/types'); + } + var response = await http.get(uri).timeout(const Duration(seconds: 20)); + if (response.statusCode == 200) { + var decodedResponse = json.decode(response.body); + Map> groupedBusinesses = {}; + + for (String stringType in decodedResponse.keys) { + List businesses = []; + + for (Map map in decodedResponse[stringType]) { + map.addAll({'type': stringType}); + Business business = Business.fromJson(map); + businesses.add(business); + } + + groupedBusinesses + .addAll({BusinessType.values.byName(stringType): businesses}); + } + return groupedBusinesses; + } else { + return 'Error ${response.statusCode}! Please try again later!'; + } + } on TimeoutException { + return 'Unable to connect to server (timeout).\nPlease try again later.'; + } on SocketException { + return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n'; + } +} + Future fetchBusinesses(List ids) async { try { var response = await http @@ -129,13 +167,34 @@ Future fetchBusiness(int id) async { } } +Future fetchJob(int id) async { + try { + var response = await http + .get(Uri.parse('$apiAddress/businessdata/jobs/$id')) + .timeout(const Duration(seconds: 20)); + if (response.statusCode == 200) { + var decodedResponse = json.decode(response.body); + Business business = Business.fromJson(decodedResponse); + + return business; + } else { + return 'Error ${response.statusCode}! Please try again later!'; + } + } on TimeoutException { + return 'Unable to connect to server (timeout).\nPlease try again later.'; + } on SocketException { + return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n'; + } +} + Future createBusiness(Business business) async { var json = ''' { "id": ${business.id}, "name": "${business.name}", - "description": "${business.description}", + "description": "${business.description?.replaceAll('\n', '\\n')}", "website": "${business.website}", + "type": "${business.type!.name}", "contactName": "${business.contactName}", "contactEmail": "${business.contactEmail}", "contactPhone": "${business.contactPhone}", @@ -165,14 +224,14 @@ Future createListing(JobListing listing) async { "id": ${listing.id}, "businessId": ${listing.businessId}, "name": "${listing.name}", - "description": "${listing.description}", + "description": "${listing.description.replaceAll('\n', '\\n')}", "wage": "${listing.wage}", "link": "${listing.link}" } '''; try { - var response = await http.post(Uri.parse('$apiAddress/createbusiness'), + var response = await http.post(Uri.parse('$apiAddress/createlisting'), body: json, headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20)); if (response.statusCode != 200) { @@ -232,8 +291,9 @@ Future editBusiness(Business business) async { { "id": ${business.id}, "name": "${business.name}", - "description": "${business.description}", + "description": "${business.description?.replaceAll('\n', '\\n')}", "website": "${business.website}", + "type": "${business.type!.name}", "contactName": "${business.contactName}", "contactEmail": "${business.contactEmail}", "contactPhone": "${business.contactPhone}", @@ -262,8 +322,8 @@ Future editListing(JobListing listing) async { "id": ${listing.id}, "businessId": ${listing.businessId}, "name": "${listing.name}", - "description": "${listing.description}", - "type": "${listing.type.name}", + "description": "${listing.description.replaceAll('\n', '\\n')}", + "type": "${listing.type!.name}", "wage": "${listing.wage}", "link": "${listing.link}" } diff --git a/fbla_ui/lib/shared/export.dart b/fbla_ui/lib/shared/export.dart new file mode 100644 index 0000000..0d112f3 --- /dev/null +++ b/fbla_ui/lib/shared/export.dart @@ -0,0 +1,488 @@ +import 'dart:io'; + +import 'package:fbla_ui/shared/api_logic.dart'; +import 'package:fbla_ui/shared/utils.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.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'; + +class _FilterBusinessDataTypeChips extends StatefulWidget { + final Set selectedDataTypesBusiness; + + const _FilterBusinessDataTypeChips({required this.selectedDataTypesBusiness}); + + @override + State<_FilterBusinessDataTypeChips> createState() => + _FilterBusinessDataTypeChipsState(); +} + +class _FilterBusinessDataTypeChipsState + extends State<_FilterBusinessDataTypeChips> { + @override + Widget build(BuildContext context) { + List chips = []; + 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); + } + }); + }), + )); + } + return Wrap( + children: chips, + ); + } +} + +class _FilterJobDataTypeChips extends StatefulWidget { + final Set selectedDataTypesJob; + + const _FilterJobDataTypeChips({required this.selectedDataTypesJob}); + + @override + State<_FilterJobDataTypeChips> createState() => + _FilterJobDataTypeChipsState(); +} + +class _FilterJobDataTypeChipsState extends State<_FilterJobDataTypeChips> { + @override + Widget build(BuildContext context) { + List chips = []; + 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); + } + }); + }), + )); + } + return Wrap( + children: chips, + ); + } +} + +Future generatePDF( + {required BuildContext context, + required int documentTypeIndex, + Set? selectedBusinesses, + Set? selectedJobs}) async { + List headerColumns = []; + List tableRows = []; + + Set dataTypesBusiness = {}; + Set dataTypesJob = {}; + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Export Settings'), + content: SizedBox( + width: 400, + height: 200, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Text('Data columns you want to export:'), + ), + documentTypeIndex == 0 + ? _FilterBusinessDataTypeChips( + selectedDataTypesBusiness: dataTypesBusiness, + ) + : _FilterJobDataTypeChips( + selectedDataTypesJob: dataTypesJob) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }), + TextButton( + child: const Text('Generate'), + onPressed: () async { + if (documentTypeIndex == 0) { + List businesses = await fetchBusinesses( + selectedBusinesses! + .map((business) => business.id) + .toList()); + 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( + getPwIconFromBusinessType(business.type!), + size: 24), + padding: const pw.EdgeInsets.all(4.0))); + } + } + for (DataTypeBusiness dataType in dataTypesBusiness) { + if (dataType != DataTypeBusiness.logo) { + var currentValue = + businessValueFromDataType(business, dataType); + if (currentValue != null) { + businessRow.add(pw.Padding( + child: pw.Text(businessValueFromDataType( + business, dataType)), + padding: const pw.EdgeInsets.all(4.0))); + } else { + businessRow.add(pw.Container()); + } + } + } + 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 (dataTypesJob.isEmpty) { + dataTypesJob.addAll(DataTypeJob.values); + } + dataTypesJob = sortDataTypesJob(dataTypesJob); + + // List> nameMapping = + // await fetchBusinessNames(); + + for (Business business in selectedJobs!) { + for (JobListing job in business.listings!) { + List jobRow = []; + for (DataTypeJob dataType in dataTypesJob) { + if (dataType != DataTypeJob.businessName) { + var currentValue = + jobValueFromDataType(job, dataType); + if (currentValue != null) { + jobRow.add(pw.Padding( + child: pw.Text(currentValue), + padding: const pw.EdgeInsets.all(4.0))); + } else { + jobRow.add(pw.Container()); + } + } else { + jobRow.add(pw.Padding( + child: pw.Text(business.name!), + padding: const pw.EdgeInsets.all(4.0))); + } + } + tableRows.add(pw.TableRow(children: jobRow)); + } + } + + for (var filter in dataTypesJob) { + headerColumns.add(pw.Padding( + child: pw.Text(dataTypeFriendlyJob[filter]!, + style: const pw.TextStyle(fontSize: 10)), + padding: const pw.EdgeInsets.all(4.0))); + } + } + // Final Generation + 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 <= 12 + ? '${dateTime.hour}:${minute}AM' + : '${dateTime.hour - 12}:${minute}PM'; + String fileName = + '${documentTypeIndex == 0 ? 'Business' : 'Job Listing'} Data - ${dateTime.month}-${dateTime.day}-${dateTime.year} $time.pdf'; + + final pdf = pw.Document(); + var svg = await rootBundle.loadString('assets/MarinoDev.svg'); + + 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, + 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: svg, height: 40), + pw.Padding( + padding: const pw.EdgeInsets.all(8.0), + child: pw.Text( + '${documentTypeIndex == 0 ? 'Business' : 'Job Listing'} 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: documentTypeIndex == 0 + ? _businessColumnSizes(dataTypesBusiness) + : _jobColumnSizes(dataTypesJob), + 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: headerColumns, + repeat: true, + ), + ...tableRows, + ]) + ]; + })); + + 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); + } + Navigator.of(context).pop(); + }), + ], + ); + }); +} + +Map _businessColumnSizes( + Set dataTypes) { + double space = 744.0; + List sorted = sortDataTypesBusiness(dataTypes).toList(); + Map map = {}; + + if (sorted.contains(DataTypeBusiness.logo)) { + space -= 32; + map.addAll( + {sorted.indexOf(DataTypeBusiness.logo): const pw.FixedColumnWidth(32)}); + } + if (dataTypes.contains(DataTypeBusiness.contactName)) { + space -= 72; + map.addAll({ + sorted.indexOf(DataTypeBusiness.contactName): + const pw.FixedColumnWidth(72) + }); + } + if (dataTypes.contains(DataTypeBusiness.contactPhone)) { + space -= 76; + map.addAll({ + sorted.indexOf(DataTypeBusiness.contactPhone): + const pw.FixedColumnWidth(76) + }); + } + double leftNum = 0; + if (dataTypes.contains(DataTypeBusiness.name)) { + leftNum += 1; + } + if (dataTypes.contains(DataTypeBusiness.website)) { + leftNum += 1; + } + if (dataTypes.contains(DataTypeBusiness.contactEmail)) { + leftNum += 1; + } + if (dataTypes.contains(DataTypeBusiness.notes)) { + leftNum += 2; + } + if (dataTypes.contains(DataTypeBusiness.description)) { + leftNum += 3; + } + leftNum = space / leftNum; + if (dataTypes.contains(DataTypeBusiness.name)) { + map.addAll( + {sorted.indexOf(DataTypeBusiness.name): pw.FixedColumnWidth(leftNum)}); + } + if (dataTypes.contains(DataTypeBusiness.website)) { + map.addAll({ + sorted.indexOf(DataTypeBusiness.website): pw.FixedColumnWidth(leftNum) + }); + } + if (dataTypes.contains(DataTypeBusiness.contactEmail)) { + map.addAll({ + sorted.indexOf(DataTypeBusiness.contactEmail): + pw.FixedColumnWidth(leftNum) + }); + } + if (dataTypes.contains(DataTypeBusiness.notes)) { + map.addAll({ + sorted.indexOf(DataTypeBusiness.notes): pw.FixedColumnWidth(leftNum * 2) + }); + } + if (dataTypes.contains(DataTypeBusiness.description)) { + map.addAll({ + sorted.indexOf(DataTypeBusiness.description): + pw.FixedColumnWidth(leftNum * 3) + }); + } + + return map; +} + +Map _jobColumnSizes(Set dataTypes) { + Map map = {}; + List sortedDataTypes = sortDataTypesJob(dataTypes).toList(); + + if (dataTypes.contains(DataTypeJob.businessName)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.businessName) + .first): const pw.FractionColumnWidth(0.2) + }); + } + if (dataTypes.contains(DataTypeJob.name)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.name) + .first): const pw.FractionColumnWidth(0.2) + }); + } + if (dataTypes.contains(DataTypeJob.description)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.description) + .first): const pw.FractionColumnWidth(0.4) + }); + } + if (dataTypes.contains(DataTypeJob.wage)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.wage) + .first): const pw.FractionColumnWidth(0.15) + }); + } + if (dataTypes.contains(DataTypeJob.link)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.link) + .first): const pw.FractionColumnWidth(0.2) + }); + } + + return map; +} + +dynamic businessValueFromDataType( + Business business, DataTypeBusiness dataType) { + switch (dataType) { + case DataTypeBusiness.name: + return business.name; + case DataTypeBusiness.description: + return business.description; + case DataTypeBusiness.type: + return business.type; + case DataTypeBusiness.website: + return business.website; + case DataTypeBusiness.contactName: + return business.contactName; + case DataTypeBusiness.contactEmail: + return business.contactEmail; + case DataTypeBusiness.contactPhone: + return business.contactPhone; + case DataTypeBusiness.notes: + return business.notes; + case DataTypeBusiness.logo: + return null; + } +} + +dynamic jobValueFromDataType(JobListing job, DataTypeJob dataType) { + switch (dataType) { + case DataTypeJob.name: + return job.name; + case DataTypeJob.description: + return job.description; + case DataTypeJob.wage: + return job.wage; + case DataTypeJob.link: + return job.link; + case DataTypeJob.businessName: + return null; + } +} diff --git a/fbla_ui/lib/shared/global_vars.dart b/fbla_ui/lib/shared/global_vars.dart new file mode 100644 index 0000000..586d944 --- /dev/null +++ b/fbla_ui/lib/shared/global_vars.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +late String jwt; +const int widescreenWidth = 600; +bool loggedIn = false; +ThemeMode themeMode = ThemeMode.system; diff --git a/fbla_ui/lib/shared/utils.dart b/fbla_ui/lib/shared/utils.dart new file mode 100644 index 0000000..312e656 --- /dev/null +++ b/fbla_ui/lib/shared/utils.dart @@ -0,0 +1,300 @@ +import 'package:flutter/material.dart'; +import 'package:pdf/widgets.dart' as pw; + +enum DataTypeBusiness { + logo, + name, + description, + website, + contactName, + contactEmail, + contactPhone, + notes, + type, +} + +enum DataTypeJob { + businessName, + name, + description, + wage, + link, +} + +Map dataTypePriorityBusiness = { + DataTypeBusiness.logo: 0, + DataTypeBusiness.name: 1, + DataTypeBusiness.description: 2, + DataTypeBusiness.type: 3, + DataTypeBusiness.website: 4, + DataTypeBusiness.contactName: 5, + DataTypeBusiness.contactEmail: 6, + DataTypeBusiness.contactPhone: 7, + DataTypeBusiness.notes: 8 +}; + +Map dataTypeFriendlyBusiness = { + DataTypeBusiness.logo: 'Logo', + DataTypeBusiness.name: 'Name', + DataTypeBusiness.description: 'Description', + DataTypeBusiness.type: 'Type', + DataTypeBusiness.website: 'Website', + DataTypeBusiness.contactName: 'Contact Name', + DataTypeBusiness.contactEmail: 'Contact Email', + DataTypeBusiness.contactPhone: 'Contact Phone', + DataTypeBusiness.notes: 'Notes' +}; + +Map dataTypePriorityJob = { + DataTypeJob.businessName: 1, + DataTypeJob.name: 2, + DataTypeJob.description: 3, + DataTypeJob.wage: 4, + DataTypeJob.link: 5, +}; + +Map dataTypeFriendlyJob = { + DataTypeJob.businessName: 'Business Name', + DataTypeJob.name: 'Job Listing Name', + DataTypeJob.description: 'Description', + DataTypeJob.wage: 'Wage', + DataTypeJob.link: 'Additional Info Link', +}; + +Set sortDataTypesBusiness(Set set) { + List list = set.toList(); + list.sort((a, b) { + return dataTypePriorityBusiness[a]!.compareTo(dataTypePriorityBusiness[b]!); + }); + set = list.toSet(); + return set; +} + +Set sortDataTypesJob(Set set) { + List list = set.toList(); + list.sort((a, b) { + return dataTypePriorityJob[a]!.compareTo(dataTypePriorityJob[b]!); + }); + set = list.toSet(); + return set; +} + +enum BusinessType { + food, + shop, + outdoors, + manufacturing, + entertainment, + other, +} + +enum JobType { cashier, server, mechanic, other } + +class JobListing { + int? id; + int? businessId; + String name; + String description; + JobType? type; + String? wage; + String? link; + + JobListing( + {this.id, + this.businessId, + required this.name, + required this.description, + this.type, + this.wage, + this.link}); + + factory JobListing.copy(JobListing input) { + return JobListing( + id: input.id, + businessId: input.businessId, + name: input.name, + description: input.description, + type: input.type, + wage: input.wage, + link: input.link, + ); + } +} + +class Business { + int id; + String? name; + String? description; + BusinessType? type; + String? website; + String? contactName; + String? contactEmail; + String? contactPhone; + String? notes; + String locationName; + String? locationAddress; + List? listings; + + Business( + {required this.id, + required this.name, + required this.description, + required this.website, + this.type, + this.contactName, + this.contactEmail, + this.contactPhone, + this.notes, + required this.locationName, + this.locationAddress, + this.listings}); + + factory Business.fromJson(Map json) { + List? listings; + if (json['listings'] != null) { + listings = []; + for (int i = 0; i < json['listings'].length; i++) { + listings.add(JobListing( + id: json['listings'][i]['id'], + businessId: json['listings'][i]['businessId'], + name: json['listings'][i]['name'], + description: json['listings'][i]['description'], + type: JobType.values.byName(json['listings'][i]['type']), + wage: json['listings'][i]['wage'], + link: json['listings'][i]['link'])); + } + } + + return Business( + id: json['id'], + name: json['name'], + description: json['description'], + type: json['type'] != null + ? BusinessType.values.byName(json['type']) + : null, + website: json['website'], + contactName: json['contactName'], + contactEmail: json['contactEmail'], + contactPhone: json['contactPhone'], + notes: json['notes'], + locationName: json['locationName'], + locationAddress: json['locationAddress'], + listings: listings); + } + + factory Business.copy(Business input) { + return Business( + id: input.id, + name: input.name, + description: input.description, + website: input.website, + contactName: input.contactName, + contactEmail: input.contactEmail, + contactPhone: input.contactPhone, + notes: input.notes, + locationName: input.locationName, + locationAddress: input.locationAddress, + listings: input.listings); + } +} + +IconData getIconFromBusinessType(BusinessType type) { + switch (type) { + case BusinessType.food: + return Icons.restaurant; + case BusinessType.shop: + return Icons.store; + case BusinessType.outdoors: + return Icons.forest; + case BusinessType.manufacturing: + return Icons.factory; + case BusinessType.entertainment: + return Icons.live_tv; + case BusinessType.other: + return Icons.business; + } +} + +IconData getIconFromJobType(JobType type) { + switch (type) { + case JobType.cashier: + return Icons.shopping_bag; + case JobType.server: + return Icons.restaurant; + case JobType.mechanic: + return Icons.construction; + case JobType.other: + return Icons.work; + } +} + +pw.IconData getPwIconFromBusinessType(BusinessType type) { + switch (type) { + case BusinessType.food: + return const pw.IconData(0xe56c); + case BusinessType.shop: + return const pw.IconData(0xea12); + case BusinessType.outdoors: + return const pw.IconData(0xea99); + case BusinessType.manufacturing: + return const pw.IconData(0xebbc); + case BusinessType.entertainment: + return const pw.IconData(0xe639); + case BusinessType.other: + return const pw.IconData(0xe0af); + } +} + +pw.IconData getPwIconFromJobType(JobType type) { + switch (type) { + case JobType.cashier: + return const pw.IconData(0xf1cc); + case JobType.server: + return const pw.IconData(0xe56c); + case JobType.mechanic: + return const pw.IconData(0xea3c); + case JobType.other: + return const pw.IconData(0xe8f9); + } +} + +String getNameFromBusinessType(BusinessType type) { + switch (type) { + case BusinessType.food: + return 'Food Related'; + case BusinessType.shop: + return 'Shops'; + case BusinessType.outdoors: + return 'Outdoors'; + case BusinessType.manufacturing: + return 'Manufacturing'; + case BusinessType.entertainment: + return 'Entertainment'; + case BusinessType.other: + return 'Other'; + } +} + +String getNameFromJobType(JobType type) { + switch (type) { + case JobType.cashier: + return 'Cashier'; + case JobType.server: + return 'Server'; + case JobType.mechanic: + return 'Mechanic'; + case JobType.other: + return 'Other'; + } +} + +IconData getIconFromThemeMode(ThemeMode theme) { + switch (theme) { + case ThemeMode.dark: + return Icons.dark_mode; + case ThemeMode.light: + return Icons.light_mode; + case ThemeMode.system: + return Icons.brightness_4; + } +} diff --git a/fbla_ui/lib/shared/widgets.dart b/fbla_ui/lib/shared/widgets.dart new file mode 100644 index 0000000..4d9c677 --- /dev/null +++ b/fbla_ui/lib/shared/widgets.dart @@ -0,0 +1,821 @@ +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:fbla_ui/pages/signin_page.dart'; +import 'package:fbla_ui/shared/global_vars.dart'; +import 'package:fbla_ui/shared/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; + +// class BusinessDisplayPanel extends StatefulWidget { +// final Map>? jobGroupedBusinesses; +// final Map>? businessGroupedBusinesses; +// final bool widescreen; +// final Set? selectedBusinesses; +// +// const BusinessDisplayPanel( +// {super.key, +// this.jobGroupedBusinesses, +// this.businessGroupedBusinesses, +// required this.widescreen, +// this.selectedBusinesses}); +// +// @override +// State createState() => _BusinessDisplayPanelState(); +// } +// +// class _BusinessDisplayPanelState extends State { +// @override +// Widget build(BuildContext context) { +// if ((widget.businessGroupedBusinesses?.keys ?? []).isEmpty && +// (widget.jobGroupedBusinesses?.keys ?? []).isEmpty) { +// return const SliverToBoxAdapter( +// child: Center( +// child: Padding( +// padding: EdgeInsets.all(16.0), +// child: Text( +// 'No results found!\nPlease change your search filters.', +// textAlign: TextAlign.center, +// style: TextStyle(fontSize: 18), +// ), +// ), +// ), +// ); +// } +// +// List headers = []; +// if (widget.jobGroupedBusinesses != null) { +// for (JobType jobType in widget.jobGroupedBusinesses!.keys) { +// headers.add(BusinessHeader( +// jobType: jobType, +// widescreen: widget.widescreen, +// // selectable: widget.selectable, +// selectedBusinesses: widget.selectedBusinesses, +// // updateSelectedBusinessesCallback: +// // widget.updateSelectedBusinessesCallback, +// businesses: widget.jobGroupedBusinesses![jobType]!)); +// } +// headers.sort((a, b) => a.jobType!.index.compareTo(b.jobType!.index)); +// return MultiSliver(children: headers); +// } else if (widget.businessGroupedBusinesses != null) { +// for (BusinessType businessType +// in widget.businessGroupedBusinesses!.keys) { +// headers.add(BusinessHeader( +// businessType: businessType, +// widescreen: widget.widescreen, +// selectedBusinesses: widget.selectedBusinesses, +// businesses: widget.businessGroupedBusinesses![businessType]!)); +// } +// headers.sort( +// (a, b) => a.businessType!.index.compareTo(b.businessType!.index)); +// return MultiSliver(children: headers); +// } +// return const Text('Error with input data!'); +// } +// } +// +// class BusinessHeader extends StatefulWidget { +// final JobType? jobType; +// final BusinessType? businessType; +// final List businesses; +// final Set? selectedBusinesses; +// final bool widescreen; +// final void Function()? updateSelectedBusinessesCallback; +// +// const BusinessHeader({ +// super.key, +// this.jobType, +// this.businessType, +// required this.businesses, +// required this.widescreen, +// this.updateSelectedBusinessesCallback, +// this.selectedBusinesses, +// }); +// +// @override +// State createState() => _BusinessHeaderState(); +// } +// +// class _BusinessHeaderState extends State { +// refresh() { +// setState(() {}); +// } +// +// @override +// Widget build(BuildContext context) { +// return SliverStickyHeader( +// header: Container( +// height: 55.0, +// color: Theme.of(context).colorScheme.primary, +// padding: const EdgeInsets.symmetric(horizontal: 16.0), +// alignment: Alignment.centerLeft, +// child: _getHeaderRow(widget.selectedBusinesses), +// ), +// sliver: _getChildSliver( +// widget.businesses, widget.widescreen, widget.selectedBusinesses), +// ); +// } +// +// Widget _getHeaderRow(Set? selectedBusinesses) { +// if (selectedBusinesses != null) { +// return Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Row( +// children: [ +// Padding( +// padding: const EdgeInsets.only(left: 4.0, right: 12.0), +// child: Icon( +// widget.jobType != null +// ? getIconFromJobType(widget.jobType!) +// : getIconFromBusinessType(widget.businessType!), +// color: Theme.of(context).colorScheme.onPrimary, +// )), +// Text(widget.jobType != null +// ? getNameFromJobType(widget.jobType!) +// : getNameFromBusinessType(widget.businessType!)), +// ], +// ), +// Padding( +// padding: const EdgeInsets.only(right: 12.0), +// child: Checkbox( +// checkColor: Theme.of(context).colorScheme.primary, +// activeColor: Theme.of(context).colorScheme.onPrimary, +// value: widget.selectedBusinesses!.containsAll(widget.businesses), +// onChanged: (value) { +// if (value!) { +// setState(() { +// widget.selectedBusinesses!.addAll(widget.businesses); +// }); +// } else { +// setState(() { +// widget.selectedBusinesses!.removeAll(widget.businesses); +// }); +// } +// }, +// ), +// ), +// ], +// ); +// } else { +// return Row( +// children: [ +// Padding( +// padding: const EdgeInsets.only(left: 4.0, right: 12.0), +// child: Icon( +// widget.jobType != null +// ? getIconFromJobType(widget.jobType!) +// : getIconFromBusinessType(widget.businessType!), +// color: Theme.of(context).colorScheme.onPrimary, +// ), +// ), +// Text( +// widget.jobType != null +// ? getNameFromJobType(widget.jobType!) +// : getNameFromBusinessType(widget.businessType!), +// style: TextStyle(color: Theme.of(context).colorScheme.onPrimary), +// ), +// ], +// ); +// } +// } +// +// Widget _getChildSliver(List businesses, bool widescreen, +// Set? selectedBusinesses) { +// if (widescreen) { +// return SliverPadding( +// padding: const EdgeInsets.all(4), +// sliver: SliverGrid( +// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( +// mainAxisExtent: 250.0, +// maxCrossAxisExtent: 400.0, +// mainAxisSpacing: 4.0, +// crossAxisSpacing: 4.0, +// ), +// delegate: SliverChildBuilderDelegate( +// childCount: businesses.length, +// (BuildContext context, int index) { +// return BusinessCard( +// business: businesses[index], +// selectedBusinesses: selectedBusinesses, +// widescreen: widescreen, +// callback: refresh, +// jobType: widget.jobType, +// ); +// }, +// ), +// ), +// ); +// } else { +// return SliverList( +// delegate: SliverChildBuilderDelegate( +// childCount: businesses.length, +// (BuildContext context, int index) { +// return BusinessCard( +// business: businesses[index], +// selectedBusinesses: selectedBusinesses, +// widescreen: widescreen, +// callback: refresh, +// jobType: widget.jobType, +// ); +// }, +// ), +// ); +// } +// } +// } +// +// class BusinessCard extends StatefulWidget { +// final Business business; +// final bool widescreen; +// final Set? selectedBusinesses; +// final Function callback; +// final JobType? jobType; +// final BusinessType? businessType; +// +// const BusinessCard({ +// super.key, +// required this.business, +// required this.widescreen, +// required this.callback, +// this.jobType, +// this.businessType, +// this.selectedBusinesses, +// }); +// +// @override +// State createState() => _BusinessCardState(); +// } +// +// class _BusinessCardState extends State { +// @override +// Widget build(BuildContext context) { +// if (widget.widescreen) { +// return _businessTile(widget.business, widget.selectedBusinesses, +// widget.jobType, widget.businessType); +// } else { +// return _businessListItem(widget.business, widget.selectedBusinesses, +// widget.callback, widget.jobType, widget.businessType); +// } +// } +// +// Widget _businessTile(Business business, Set? selectedBusinesses, +// JobType? jobType, BusinessType? businessType) { +// return MouseRegion( +// cursor: SystemMouseCursors.click, +// child: GestureDetector( +// onTap: () { +// Navigator.of(context).push(MaterialPageRoute( +// builder: (context) => BusinessDetail( +// id: business.id, +// name: business.name!, +// ))); +// }, +// child: Card( +// clipBehavior: Clip.antiAlias, +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.center, +// children: [ +// _getTileRow(business, selectedBusinesses, widget.callback), +// Padding( +// padding: const EdgeInsets.all(8.0), +// child: Text( +// business.description!, +// maxLines: selectedBusinesses != null ? 7 : 5, +// overflow: TextOverflow.ellipsis, +// ), +// ), +// const Spacer(), +// Padding( +// padding: const EdgeInsets.all(8.0), +// child: selectedBusinesses == null +// ? Row( +// mainAxisAlignment: MainAxisAlignment.spaceEvenly, +// children: [ +// IconButton( +// icon: const Icon(Icons.link), +// onPressed: () { +// launchUrl( +// Uri.parse('https://${business.website}')); +// }, +// ), +// if (business.locationName != '') +// IconButton( +// icon: const Icon(Icons.location_on), +// onPressed: () { +// launchUrl(Uri.parse(Uri.encodeFull( +// 'https://www.google.com/maps/search/?api=1&query=${business.locationName}'))); +// }, +// ), +// if ((business.contactPhone != null) && +// (business.contactPhone != '')) +// IconButton( +// icon: const Icon(Icons.phone), +// onPressed: () { +// showDialog( +// context: context, +// builder: (BuildContext context) { +// return AlertDialog( +// backgroundColor: Theme.of(context) +// .colorScheme +// .surface, +// title: Text((business.contactName == +// null || +// business.contactName == '') +// ? 'Contact ${business.name}?' +// : 'Contact ${business.contactName}'), +// content: Text((business.contactName == +// null || +// business.contactName == '') +// ? 'Would you like to call or text ${business.name}?' +// : 'Would you like to call or text ${business.contactName}?'), +// actions: [ +// TextButton( +// child: const Text('Text'), +// onPressed: () { +// launchUrl(Uri.parse( +// 'sms:${business.contactPhone}')); +// Navigator.of(context).pop(); +// }), +// TextButton( +// child: const Text('Call'), +// onPressed: () async { +// launchUrl(Uri.parse( +// 'tel:${business.contactPhone}')); +// Navigator.of(context).pop(); +// }), +// ], +// ); +// }); +// }, +// ), +// if (business.contactEmail != '') +// IconButton( +// icon: const Icon(Icons.email), +// onPressed: () { +// launchUrl(Uri.parse( +// 'mailto:${business.contactEmail}')); +// }, +// ), +// ], +// ) +// : null), +// ], +// ), +// ), +// ), +// ); +// } +// +// Widget _getTileRow( +// Business business, Set? selectedBusinesses, Function callback) { +// if (selectedBusinesses != null) { +// return Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Padding( +// padding: const EdgeInsets.all(8.0), +// child: ClipRRect( +// borderRadius: BorderRadius.circular(6.0), +// child: Image.network('$apiAddress/logos/${business.id}', +// height: 48, width: 48, errorBuilder: (BuildContext context, +// Object exception, StackTrace? stackTrace) { +// return Icon( +// getIconFromBusinessType(business.type!), +// size: 48, +// ); +// }), +// ), +// ), +// Flexible( +// child: Padding( +// padding: const EdgeInsets.all(8.0), +// child: Text( +// business.name!, +// style: +// const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), +// maxLines: 2, +// overflow: TextOverflow.ellipsis, +// ), +// ), +// ), +// Padding( +// padding: const EdgeInsets.only(right: 24.0), +// child: _checkbox(callback, selectedBusinesses), +// ) +// ], +// ); +// } else { +// return Row( +// children: [ +// Padding( +// padding: const EdgeInsets.all(8.0), +// child: ClipRRect( +// borderRadius: BorderRadius.circular(6.0), +// child: Image.network('$apiAddress/logos/${business.id}', +// height: 48, width: 48, errorBuilder: (BuildContext context, +// Object exception, StackTrace? stackTrace) { +// return Icon(getIconFromBusinessType(business.type!), +// size: 48); +// }), +// )), +// Flexible( +// child: Padding( +// padding: const EdgeInsets.all(8.0), +// child: Text( +// business.name!, +// style: +// const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), +// maxLines: 2, +// overflow: TextOverflow.ellipsis, +// ), +// ), +// ), +// ], +// ); +// } +// } +// +// Widget _businessListItem(Business business, Set? selectedBusinesses, +// Function callback, JobType? jobType, BusinessType? businessType) { +// return Card( +// child: ListTile( +// leading: ClipRRect( +// borderRadius: BorderRadius.circular(3.0), +// child: Image.network('$apiAddress/logos/${business.id}', +// height: 24, width: 24, errorBuilder: (BuildContext context, +// Object exception, StackTrace? stackTrace) { +// return Icon(getIconFromBusinessType(business.type!)); +// })), +// title: Text(business.name!), +// subtitle: Text(business.description!, +// maxLines: 1, overflow: TextOverflow.ellipsis), +// trailing: _getCheckbox(selectedBusinesses, callback), +// onTap: () { +// Navigator.of(context).push(MaterialPageRoute( +// builder: (context) => BusinessDetail( +// id: business.id, +// name: business.name!, +// ))); +// }, +// ), +// ); +// } +// +// Widget _checkbox(Function callback, Set selectedBusinesses) { +// return Checkbox( +// value: selectedBusinesses.contains(widget.business), +// onChanged: (value) { +// if (value!) { +// setState(() { +// selectedBusinesses.add(widget.business); +// }); +// } else { +// setState(() { +// selectedBusinesses.remove(widget.business); +// }); +// } +// callback(); +// }, +// ); +// } +// +// Widget? _getCheckbox(Set? selectedBusinesses, Function callback) { +// if (selectedBusinesses != null) { +// return _checkbox(callback, selectedBusinesses); +// } else { +// return null; +// } +// } +// } + +class BusinessSearchBar extends StatefulWidget { + final String searchTextHint; + final Widget filterIconButton; + final void Function(String) setSearchCallback; + + const BusinessSearchBar( + {super.key, + required this.setSearchCallback, + required this.searchTextHint, + required this.filterIconButton}); + + @override + State createState() => _BusinessSearchBarState(); +} + +class _BusinessSearchBarState extends State { + @override + Widget build(BuildContext context) { + return SizedBox( + width: 450, + height: 50, + child: SearchBar( + hintText: widget.searchTextHint, + backgroundColor: WidgetStateProperty.resolveWith((notNeeded) { + return Theme.of(context).colorScheme.surfaceContainer; + }), + onChanged: (query) { + widget.setSearchCallback(query); + }, + leading: const Padding( + padding: EdgeInsets.only(left: 8.0), + child: Icon(Icons.search), + ), + trailing: [widget.filterIconButton]), + ); + } +} + +class FilterChips extends StatefulWidget { + final Set? selectedJobChips; + final Set? selectedBusinessChips; + + const FilterChips( + {super.key, this.selectedJobChips, this.selectedBusinessChips}); + + @override + State createState() => _FilterChipsState(); +} + +class _FilterChipsState extends State { + List filterChips() { + List chips = []; + + if (widget.selectedJobChips != null) { + for (var type in JobType.values) { + chips.add(Padding( + padding: const EdgeInsets.only(left: 4.0, right: 4.0), + child: FilterChip( + showCheckmark: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + label: Text(getNameFromJobType(type)), + selected: widget.selectedJobChips!.contains(type), + onSelected: (bool selected) { + setState(() { + if (selected) { + widget.selectedJobChips!.add(type); + } else { + widget.selectedJobChips!.remove(type); + } + }); + }), + )); + } + } else if (widget.selectedBusinessChips != null) { + for (var type in BusinessType.values) { + chips.add(Padding( + padding: const EdgeInsets.only(left: 4.0, right: 4.0), + child: FilterChip( + showCheckmark: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + label: Text(getNameFromBusinessType(type)), + selected: widget.selectedBusinessChips!.contains(type), + onSelected: (bool selected) { + setState(() { + if (selected) { + widget.selectedBusinessChips!.add(type); + } else { + widget.selectedBusinessChips!.remove(type); + } + }); + }), + )); + } + } + return chips; + } + + @override + Widget build(BuildContext context) { + return Wrap( + children: filterChips(), + ); + } +} + +class MainSliverAppBar extends StatefulWidget { + final bool widescreen; + final Widget filterIconButton; + final void Function(String) setSearch; + final void Function() themeCallback; + final void Function() generatePDF; + final void Function(bool) updateLoggedIn; + final String searchHintText; + + const MainSliverAppBar({ + super.key, + required this.widescreen, + required this.setSearch, + required this.searchHintText, + required this.themeCallback, + required this.filterIconButton, + required this.updateLoggedIn, + required this.generatePDF, + }); + + @override + State createState() => _MainSliverAppBarState(); +} + +class _MainSliverAppBarState extends State { + @override + Widget build(BuildContext context) { + return SliverAppBar( + title: widget.widescreen + ? BusinessSearchBar( + setSearchCallback: widget.setSearch, + searchTextHint: widget.searchHintText, + filterIconButton: widget.filterIconButton, + ) + : const Text('Job Link'), + toolbarHeight: 70, + stretch: false, + backgroundColor: Theme.of(context).colorScheme.surface, + pinned: true, + // floating: true, + scrolledUnderElevation: 0, + centerTitle: !widget.widescreen, + expandedHeight: widget.widescreen ? 70 : 120, + bottom: _getBottom(widget.widescreen), + leading: !widget.widescreen + ? IconButton( + icon: Icon(getIconFromThemeMode(themeMode)), + onPressed: () { + setState(() { + widget.themeCallback(); + }); + }, + ) + : null, + actions: [ + IconButton( + icon: const Icon(Icons.file_download_outlined), + onPressed: widget.generatePDF, + ), + IconButton( + icon: const Icon(Icons.help), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('About'), + backgroundColor: Theme.of(context).colorScheme.surface, + content: SizedBox( + width: 500, + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Welcome to my FBLA 2024 Coding and Programming submission!\n\n' + 'MarinoDev Job Link aims to provide comprehensive details of businesses and community partners' + ' for Waukesha West High School\'s Career and Technical Education Department.\n\n'), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Git Repo:'), + Text( + 'https://git.marinodev.com/MarinoDev/FBLA24\n', + style: TextStyle(color: Colors.blue)), + ], + ), + onTap: () { + launchUrl(Uri.https('git.marinodev.com', + '/MarinoDev/FBLA24')); + }, + ), + ), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Please direct any questions to'), + Text('drake@marinodev.com', + style: TextStyle(color: Colors.blue)), + ], + ), + onTap: () { + launchUrl( + Uri.parse('mailto:drake@marinodev.com')); + }, + ), + ) + ], + ), + ), + ), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }), + ], + ); + }); + }, + ), +// IconButton( +// icon: const Icon(Icons.picture_as_pdf), +// onPressed: () async { +// if (!_isPreviousData) { +// ScaffoldMessenger.of(context).clearSnackBars(); +// ScaffoldMessenger.of(context).showSnackBar( +// const SnackBar( +// width: 300, +// behavior: SnackBarBehavior.floating, +// content: Text('There is no data!'), +// duration: Duration(seconds: 2), +// ), +// ); +// } else { +// selectedDataTypesBusiness = {}; +// Navigator.push( +// context, +// MaterialPageRoute( +// builder: (context) => ExportData( +// groupedBusinesses: overviewBusinesses))); +// } +// }, +// ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: IconButton( + icon: loggedIn + ? const Icon(Icons.account_circle) + : const Icon(Icons.login), + onPressed: () { + if (loggedIn) { + var payload = JWT.decode(jwt).payload; + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text('Hi, ${payload['username']}!'), + content: Text( + 'You are logged in as an admin with username ${payload['username']}.'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }), + TextButton( + child: const Text('Logout'), + onPressed: () async { + final prefs = + await SharedPreferences.getInstance(); + prefs.setBool('rememberMe', false); + prefs.setString('username', ''); + prefs.setString('password', ''); + + widget.updateLoggedIn(false); + Navigator.of(context).pop(); + }), + ], + ); + }); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + SignInPage(refreshAccount: widget.updateLoggedIn))); + } + }, + ), + ), + ], + ); + } + + PreferredSizeWidget? _getBottom(bool widescreen) { + if (!widescreen) { + return PreferredSize( + preferredSize: const Size.fromHeight(0), + child: SizedBox( + height: 70, + child: Padding( + padding: const EdgeInsets.all(10), + child: BusinessSearchBar( + filterIconButton: widget.filterIconButton, + setSearchCallback: widget.setSearch, + searchTextHint: widget.searchHintText, + ), + ), + ), + ); + } + return null; + } +}