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, entertainment, other, } enum JobType { cashier, server, mechanic, other } class JobListing { String? id; String? 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}); } 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, this.website, this.contactName, this.contactEmail, this.contactPhone, this.notes, this.locationName, this.locationAddress, this.listings}); factory Business.fromJson(Map json) { List? listings = []; for (int i = 0; i < json['listings'].length; i++) { listings.add(JobListing( name: json['listings']['name'], description: json['listings']['description'], type: json['listings']['type'], wage: json['listings']['wage'], link: json['listings']['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 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.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 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.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); } } 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.entertainment: return Text('Entertainment', style: TextStyle(color: color)); case BusinessType.other: return Text('Other', style: TextStyle(color: color)); } } Text getNameFromJobType(JobType type, Color color) { switch (type) { case JobType.cashier: return Text('Cashier', style: TextStyle(color: color)); case JobType.server: return Text('Server', style: TextStyle(color: color)); case JobType.mechanic: return Text('Mechanic', style: TextStyle(color: color)); case JobType.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 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), ), getNameFromJobType( 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: getIconFromJobType( widget.type, 24, Theme.of(context).colorScheme.onPrimary), ), getNameFromJobType( 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, 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 != null) && (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 .background, 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 != null) && (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 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: getNameFromType(type, Theme.of(context).colorScheme.onSurface), 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(), ); } }