FBLA24/fbla_ui/lib/shared.dart

952 lines
29 KiB
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;
String searchFilter = '';
Set<Business> selectedBusinesses = <Business>{};
enum DataTypeBusiness {
logo,
name,
description,
website,
contactName,
contactEmail,
contactPhone,
notes,
}
enum DataTypeJob {
businessName,
name,
description,
wage,
link,
}
Map<DataTypeBusiness, int> 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<DataTypeBusiness, String> 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<DataTypeJob, int> dataTypePriorityJob = {
DataTypeJob.businessName: 1,
DataTypeJob.name: 2,
DataTypeJob.description: 3,
DataTypeJob.wage: 4,
DataTypeJob.link: 5,
};
Map<DataTypeJob, String> dataTypeFriendlyJob = {
DataTypeJob.businessName: 'Business Name',
DataTypeJob.name: 'Listing Name',
DataTypeJob.description: 'Description',
DataTypeJob.wage: 'Wage',
DataTypeJob.link: 'Link',
};
Set<DataTypeBusiness> sortDataTypesBusiness(Set<DataTypeBusiness> set) {
List<DataTypeBusiness> list = set.toList();
list.sort((a, b) {
return dataTypePriorityBusiness[a]!.compareTo(dataTypePriorityBusiness[b]!);
});
set = list.toSet();
return set;
}
Set<DataTypeJob> sortDataTypesJob(Set<DataTypeJob> set) {
List<DataTypeJob> 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<JobListing>? 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<String, dynamic> json) {
List<JobListing>? 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<BusinessType, List<Business>> groupBusinesses(List<Business> businesses) {
// Map<BusinessType, List<Business>> groupedBusinesses =
// groupBy<Business, BusinessType>(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<JobType, List<Business>> groupedBusinesses;
final bool widescreen;
final bool selectable;
const BusinessDisplayPanel(
{super.key,
required this.groupedBusinesses,
required this.widescreen,
required this.selectable});
@override
State<BusinessDisplayPanel> createState() => _BusinessDisplayPanelState();
}
class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
Set<Business> selectedBusinesses = <Business>{};
@override
Widget build(BuildContext context) {
List<BusinessHeader> headers = [];
// List<Business> 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<Business> businesses;
final Set<Business> 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<BusinessHeader> createState() => _BusinessHeaderState();
}
class _BusinessHeaderState extends State<BusinessHeader> {
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<Business> 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<BusinessCard> createState() => _BusinessCardState();
}
class _BusinessCardState extends State<BusinessCard> {
@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<JobType> filters;
final Future<void> Function(Set<JobType>) setFiltersCallback;
final Future<void> Function(String) setSearchCallback;
const BusinessSearchBar(
{super.key,
required this.filters,
required this.setFiltersCallback,
required this.setSearchCallback});
@override
State<BusinessSearchBar> createState() => _BusinessSearchBarState();
}
class _BusinessSearchBarState extends State<BusinessSearchBar> {
bool isFiltered = false;
@override
Widget build(BuildContext context) {
Set<JobType> 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 = <JobType>{};
isFiltered = false;
});
widget.setFiltersCallback(<JobType>{});
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<JobType> selectedChips;
const FilterChips({super.key, required this.selectedChips});
@override
State<FilterChips> createState() => _FilterChipsState();
}
class _FilterChipsState extends State<FilterChips> {
List<Padding> filterChips() {
List<Padding> 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(),
);
}
}