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(), ); } }