952 lines
29 KiB
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(),
|
|
);
|
|
}
|
|
}
|