import 'package:collection/collection.dart'; 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; Set filters = {}; Set selectedChips = {}; String searchFilter = ''; bool isFiltered = false; Set selectedBusinesses = {}; Set selectedDataTypes = {}; Set dataTypeFilters = {}; enum DataType { logo, name, description, type, website, contactName, contactEmail, contactPhone, notes, } Map dataTypeValues = { DataType.logo: 0, DataType.name: 1, DataType.description: 2, DataType.type: 3, DataType.website: 4, DataType.contactName: 5, DataType.contactEmail: 6, DataType.contactPhone: 7, DataType.notes: 8 }; Map dataTypeFriendly = { DataType.logo: 'Logo', DataType.name: 'Name', DataType.description: 'Description', DataType.type: 'Type', DataType.website: 'Website', DataType.contactName: 'Contact Name', DataType.contactEmail: 'Contact Email', DataType.contactPhone: 'Contact Phone', DataType.notes: 'Notes' }; Set sortDataTypes(Set set) { List list = set.toList(); list.sort((a, b) { return dataTypeValues[a]!.compareTo(dataTypeValues[b]!); }); set = list.toSet(); return set; } enum BusinessType { food, shop, outdoors, manufacturing, other, } class Business { int id; String name; String description; BusinessType type; String website; String contactName; String contactEmail; String contactPhone; String notes; String locationName; String locationAddress; Business({ required this.id, required this.name, required this.description, required this.type, required this.website, required this.contactName, required this.contactEmail, required this.contactPhone, required this.notes, required this.locationName, required this.locationAddress, }); factory Business.fromJson(Map json) { bool typeValid = true; try { BusinessType.values.byName(json['type']); } catch (e) { typeValid = false; } return Business( id: json['id'], name: json['name'], description: json['description'], type: typeValid ? BusinessType.values.byName(json['type']) : BusinessType.other, website: json['website'], contactName: json['contactName'], contactEmail: json['contactEmail'], contactPhone: json['contactPhone'], notes: json['notes'], locationName: json['locationName'], locationAddress: json['locationAddress'], ); } factory Business.copy(Business input) { return Business( id: input.id, name: input.name, description: input.description, type: input.type, website: input.website, contactName: input.contactName, contactEmail: input.contactEmail, contactPhone: input.contactPhone, notes: input.notes, locationName: input.locationName, locationAddress: input.locationAddress, ); } } Map> groupBusinesses(List businesses) { Map> groupedBusinesses = groupBy(businesses, (business) => business.type); return groupedBusinesses; } Icon getIconFromType(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.other: return Icon( Icons.business, size: size, color: color, ); } } pw.Icon getPwIconFromType(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.other: return pw.Icon(const pw.IconData(0xe0af), size: size, color: color); } } Text getNameFromType(BusinessType type, Color color) { switch (type) { case BusinessType.food: return Text('Food Related', style: TextStyle(color: color)); case BusinessType.shop: return Text('Shops', style: TextStyle(color: color)); case BusinessType.outdoors: return Text('Outdoors', style: TextStyle(color: color)); case BusinessType.manufacturing: return Text('Manufacturing', style: TextStyle(color: color)); case BusinessType.other: return Text('Other', style: TextStyle(color: color)); } } 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 List businesses; final bool widescreen; final bool selectable; const BusinessDisplayPanel( {super.key, required this.businesses, 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.businesses) { if (business.name.toLowerCase().contains(searchFilter.toLowerCase())) { filteredBusinesses.add(business); } } var groupedBusinesses = groupBusinesses(filteredBusinesses); var businessTypes = groupedBusinesses.keys.toList(); for (var i = 0; i < businessTypes.length; i++) { if (filters.contains(businessTypes[i])) { isFiltered = true; } } if (isFiltered) { for (var i = 0; i < businessTypes.length; i++) { if (filters.contains(businessTypes[i])) { headers.add(BusinessHeader( type: businessTypes[i], widescreen: widget.widescreen, selectable: widget.selectable, selectedBusinesses: selectedBusinesses, businesses: groupedBusinesses[businessTypes[i]]!)); } } } else { for (var i = 0; i < businessTypes.length; i++) { headers.add(BusinessHeader( type: businessTypes[i], widescreen: widget.widescreen, selectable: widget.selectable, selectedBusinesses: selectedBusinesses, businesses: groupedBusinesses[businessTypes[i]]!)); } } headers.sort((a, b) => a.type.index.compareTo(b.type.index)); return MultiSliver(children: headers); } } class BusinessHeader extends StatefulWidget { final BusinessType 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: getIconFromType( widget.type, 24, Theme.of(context).colorScheme.onPrimary), ), getNameFromType( widget.type, Theme.of(context).colorScheme.onPrimary), ], ), 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: getIconFromType( widget.type, 24, Theme.of(context).colorScheme.onPrimary), ), getNameFromType(widget.type, 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, ); }, ), ); } else { return SliverList( delegate: SliverChildBuilderDelegate( childCount: businesses.length, (BuildContext context, int index) { return BusinessCard( business: businesses[index], selectable: selectable, widescreen: widescreen, callback: refresh, ); }, ), ); } } } class BusinessCard extends StatefulWidget { final Business business; final bool widescreen; final bool selectable; final Function callback; const BusinessCard( {super.key, required this.business, required this.widescreen, required this.selectable, required this.callback}); @override State createState() => _BusinessCardState(); } class _BusinessCardState extends State { @override Widget build(BuildContext context) { if (widget.widescreen) { return _businessTile(widget.business, widget.selectable); } else { return _businessListItem( widget.business, widget.selectable, widget.callback); } } Widget _businessTile(Business business, bool selectable) { return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => BusinessDetail(inputBusiness: business))); }, child: Card( clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ _getTileRow(business, selectable, widget.callback), 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.isNotEmpty) 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.isNotEmpty) IconButton( icon: const Icon(Icons.phone), onPressed: () { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: Theme.of(context) .colorScheme .background, title: Text( 'Contact ${business.contactName}?'), content: Text( '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.isNotEmpty) IconButton( icon: const Icon(Icons.email), onPressed: () { launchUrl(Uri.parse( 'mailto:${business.contactEmail}')); }, ), ], ) : null), ], ), ), ), ); } Widget _getTileRow(Business business, bool selectable, Function callback) { 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 getIconFromType( business.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 getIconFromType(business.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) { 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 getIconFromType( business.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(inputBusiness: business))); }, ), ); } 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 FilterChips extends StatefulWidget { const FilterChips({super.key}); @override State createState() => _FilterChipsState(); } class _FilterChipsState extends State { List filterChips() { List chips = []; 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(type.name), selected: selectedChips.contains(type), onSelected: (bool selected) { setState(() { if (selected) { selectedChips.add(type); } else { selectedChips.remove(type); } }); }), )); } return chips; } @override Widget build(BuildContext context) { return Wrap( children: filterChips(), ); } } class FilterDataTypeChips extends StatefulWidget { const FilterDataTypeChips({super.key}); @override State createState() => _FilterDataTypeChipsState(); } class _FilterDataTypeChipsState extends State { List filterDataTypeChips() { List chips = []; for (var type in DataType.values) { chips.add(Padding( padding: const EdgeInsets.only(left: 3.0, right: 3.0, bottom: 3.0, top: 3.0), // child: ActionChip( // avatar: selectedDataTypes.contains(type) ? Icon(Icons.check_box) : Icon(Icons.check_box_outline_blank), // label: Text(type.name), // onPressed: () { // if (!selectedDataTypes.contains(type)) { // setState(() { // selectedDataTypes.add(type); // }); // } else { // setState(() { // selectedDataTypes.remove(type); // }); // } // }, // ), child: FilterChip( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide(color: Theme.of(context).colorScheme.secondary)), label: Text(dataTypeFriendly[type]!), showCheckmark: false, selected: selectedDataTypes.contains(type), onSelected: (bool selected) { setState(() { if (selected) { selectedDataTypes.add(type); } else { selectedDataTypes.remove(type); } }); }), )); } return chips; } @override Widget build(BuildContext context) { return Wrap( children: filterDataTypeChips(), ); } }