API Rewrite with UI

This commit is contained in:
Drake Marino 2024-06-13 15:30:48 -05:00
parent 64e493012a
commit cfade0e075
5 changed files with 587 additions and 389 deletions

View File

@ -22,15 +22,15 @@ class Home extends StatefulWidget {
}
class _HomeState extends State<Home> {
late Future refreshBusinessDataFuture;
late Future refreshBusinessDataOverviewFuture;
bool _isPreviousData = false;
late List<Business> businesses;
late Map<JobType, List<Business>> overviewBusinesses;
@override
void initState() {
super.initState();
refreshBusinessDataFuture = fetchBusinessData();
refreshBusinessDataOverviewFuture = fetchBusinessDataOverview();
initialLogin();
}
@ -65,10 +65,10 @@ class _HomeState extends State<Home> {
body: RefreshIndicator(
edgeOffset: 120,
onRefresh: () async {
var refreshedData = fetchBusinessData();
var refreshedData = fetchBusinessDataOverview();
await refreshedData;
setState(() {
refreshBusinessDataFuture = refreshedData;
refreshBusinessDataOverviewFuture = refreshedData;
});
},
child: CustomScrollView(
@ -185,8 +185,8 @@ class _HomeState extends State<Home> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ExportData(businesses: businesses)));
builder: (context) => ExportData(
groupedBusinesses: overviewBusinesses)));
}
},
),
@ -245,7 +245,7 @@ class _HomeState extends State<Home> {
],
),
FutureBuilder(
future: refreshBusinessDataFuture,
future: refreshBusinessDataOverviewFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
@ -263,9 +263,11 @@ class _HomeState extends State<Home> {
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
var refreshedData = fetchBusinessData();
var refreshedData =
fetchBusinessDataOverview();
setState(() {
refreshBusinessDataFuture = refreshedData;
refreshBusinessDataOverviewFuture =
refreshedData;
});
},
),
@ -274,11 +276,11 @@ class _HomeState extends State<Home> {
));
}
businesses = snapshot.data;
overviewBusinesses = snapshot.data;
_isPreviousData = true;
return BusinessDisplayPanel(
businesses: businesses,
groupedBusinesses: overviewBusinesses,
widescreen: widescreen,
selectable: false);
} else if (snapshot.hasError) {
@ -293,7 +295,7 @@ class _HomeState extends State<Home> {
ConnectionState.waiting) {
if (_isPreviousData) {
return BusinessDisplayPanel(
businesses: businesses,
groupedBusinesses: overviewBusinesses,
widescreen: widescreen,
selectable: false);
} else {
@ -339,7 +341,7 @@ class _HomeState extends State<Home> {
Widget? _getFAB() {
if (loggedIn) {
return FloatingActionButton(
child: const Icon(Icons.add),
child: const Icon(Icons.add_business),
onPressed: () {
Navigator.push(
context,

View File

@ -4,229 +4,311 @@ import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/pages/signin_page.dart';
import 'package:fbla_ui/shared.dart';
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:url_launcher/url_launcher.dart';
class BusinessDetail extends StatefulWidget {
final Business inputBusiness;
final int id;
final String name;
final JobType clickFromType;
const BusinessDetail({super.key, required this.inputBusiness});
const BusinessDetail(
{super.key,
required this.id,
required this.name,
required this.clickFromType});
@override
State<BusinessDetail> createState() => _CreateBusinessDetailState();
}
class _CreateBusinessDetailState extends State<BusinessDetail> {
late Future loadBusiness;
@override
void initState() {
super.initState();
loadBusiness = fetchBusiness(widget.id);
}
@override
Widget build(BuildContext context) {
Business business = Business.copy(widget.inputBusiness);
return Scaffold(
appBar: AppBar(
title: Text(business.name),
actions: _getActions(business),
),
body: ListView(
children: [
// Title, logo, desc, website
Card(
return FutureBuilder(
future: loadBusiness,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType != String) {
return Scaffold(
appBar: AppBar(
title: Text(snapshot.data.name),
actions: _getActions(snapshot.data, widget.clickFromType),
),
body: _detailBody(snapshot.data),
);
} else {
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child:
Text(snapshot.data, textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
var refreshedData = fetchBusiness(widget.id);
setState(() {
loadBusiness = refreshedData;
});
},
),
),
]),
),
);
}
}
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child:
RiveAnimation.asset('assets/mdev_triangle_loading.riv'),
)),
);
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
});
}
ListView _detailBody(Business business) {
return ListView(
children: [
// Title, logo, desc, website
Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
ListTile(
title: Text(business.name,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)),
subtitle: Text(
business.description,
textAlign: TextAlign.left,
),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
width: 48,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromJobType(widget.clickFromType, 48,
Theme.of(context).colorScheme.onSurface);
}),
),
),
ListTile(
leading: const Icon(Icons.link),
title: const Text('Website'),
subtitle: Text(business.website!,
style: const TextStyle(color: Colors.blue)),
onTap: () {
launchUrl(Uri.parse('https://${business.website}'));
},
),
],
),
),
// Available positions
Card(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text(
'Available Postitions',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
// Container(
// height: 400,
// width: 300,
ListView(
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: [
ListTile(
title: Text('Postition 1'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
ListTile(
title: Text('Postition 2'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
ListTile(
title: Text('Postition 3'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
],
),
]),
),
),
// Contact info
Visibility(
visible:
(business.contactEmail != null || business.contactPhone != null),
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
ListTile(
title: Text(business.name,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)),
subtitle: Text(
business.description,
textAlign: TextAlign.left,
),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
width: 48,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48,
Theme.of(context).colorScheme.onSurface);
}),
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Text(
business.contactName ?? 'Contact ${business.name}',
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
],
),
Visibility(
visible: business.contactPhone != null,
child: ListTile(
leading: Icon(Icons.phone),
title: Text(business.contactPhone!),
// maybe replace ! with ?? ''. same is true for below
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.background,
title: Text(business.contactName!.isEmpty
? 'Contact ${business.name}?'
: 'Contact ${business.contactName}'),
content: Text(business.contactName!.isEmpty
? '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();
}),
],
);
});
},
),
),
ListTile(
leading: const Icon(Icons.link),
title: const Text('Website'),
subtitle: Text(business.website,
style: const TextStyle(color: Colors.blue)),
onTap: () {
launchUrl(Uri.parse('https://${business.website}'));
},
Visibility(
visible: business.contactEmail != null,
child: ListTile(
leading: const Icon(Icons.email),
title: Text(business.contactEmail!),
onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}'));
},
),
),
],
),
),
// Available positions
Card(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Available Postitions',
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
// Container(
// height: 400,
// width: 300,
ListView(
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: [
ListTile(
title: Text('Postition 1'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
ListTile(
title: Text('Postition 2'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
ListTile(
title: Text('Postition 3'),
leading: Icon(Icons.work),
onTap: () {
// launchUrl(Uri.parse(''));
},
),
],
),
]),
),
// Location
Visibility(
child: Card(
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: const Icon(Icons.location_on),
title: Text(business.locationName!),
subtitle: Text(business.locationAddress!),
onTap: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
),
// Contact info
Visibility(
visible: (business.contactEmail.isNotEmpty ||
business.contactPhone.isNotEmpty),
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Text(
business.contactName.isEmpty
? 'Contact ${business.name}'
: business.contactName,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
],
),
Visibility(
visible: business.contactPhone.isNotEmpty,
child: ListTile(
leading: Icon(Icons.phone),
title: Text(business.contactPhone),
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.background,
title: Text(business.contactName.isEmpty
? 'Contact ${business.name}?'
: 'Contact ${business.contactName}'),
content: Text(business.contactName.isEmpty
? '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();
}),
],
);
});
},
),
),
Visibility(
visible: business.contactEmail.isNotEmpty,
child: ListTile(
leading: const Icon(Icons.email),
title: Text(business.contactEmail),
onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}'));
},
),
),
],
),
// Notes
Visibility(
visible: business.notes != null,
child: Card(
child: ListTile(
leading: const Icon(Icons.notes),
title: const Text(
'Additional Notes:',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
subtitle: Text(business.notes!),
),
),
// Location
Visibility(
child: Card(
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: const Icon(Icons.location_on),
title: Text(business.locationName),
subtitle: Text(business.locationAddress),
onTap: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
),
),
// Notes
Visibility(
visible: business.notes.isNotEmpty,
child: Card(
child: ListTile(
leading: const Icon(Icons.notes),
title: const Text(
'Additional Notes:',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
subtitle: Text(business.notes),
),
),
),
],
),
),
],
);
}
List<Widget>? _getActions(Business business) {
List<Widget>? _getActions(Business business, JobType clickFromType) {
if (loggedIn) {
return [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
CreateEditBusiness(inputBusiness: business)));
builder: (context) => CreateEditBusiness(
inputBusiness: business,
clickFromType: clickFromType,
)));
},
),
IconButton(
@ -250,7 +332,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
child: const Text('Yes'),
onPressed: () async {
String? deleteResult =
await deleteBusiness(business, jwt);
await deleteBusiness(business.id, jwt);
if (deleteResult != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(

View File

@ -6,8 +6,9 @@ import 'package:flutter/services.dart';
class CreateEditBusiness extends StatefulWidget {
final Business? inputBusiness;
final JobType? clickFromType;
const CreateEditBusiness({super.key, this.inputBusiness});
const CreateEditBusiness({super.key, this.inputBusiness, this.clickFromType});
@override
State<CreateEditBusiness> createState() => _CreateEditBusinessState();
@ -27,7 +28,6 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
id: 0,
name: 'Business',
description: 'Add details about the business below.',
type: BusinessType.other,
website: '',
contactName: '',
contactEmail: '',
@ -155,7 +155,9 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
'https://logo.clearbit.com/${business.website}',
errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48,
return getIconFromJobType(
widget.clickFromType ?? JobType.other,
48,
Theme.of(context).colorScheme.onBackground);
}),
),
@ -239,7 +241,8 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Business Description (required)',
labelText:
'Business Description (required)',
),
validator: (value) {
if (value != null && value.isEmpty) {
@ -357,46 +360,49 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text('Type of Business',
style: TextStyle(fontSize: 16)),
DropdownMenu<BusinessType>(
initialSelection: business.type,
controller: businessTypeController,
label: const Text('Business Type'),
dropdownMenuEntries: const [
DropdownMenuEntry(
value: BusinessType.food,
label: 'Food Related'),
DropdownMenuEntry(
value: BusinessType.shop,
label: 'Shop'),
DropdownMenuEntry(
value: BusinessType.outdoors,
label: 'Outdoors'),
DropdownMenuEntry(
value: BusinessType.manufacturing,
label: 'Manufacturing'),
DropdownMenuEntry(
value: BusinessType.entertainment,
label: 'Entertainment'),
DropdownMenuEntry(
value: BusinessType.other,
label: 'Other'),
],
onSelected: (inputType) {
business.type = inputType!;
},
),
],
),
),
// Business Type Dropdown
// Padding(
// padding: const EdgeInsets.only(
// left: 8.0, right: 8.0, bottom: 8.0),
// child: Row(
// mainAxisAlignment:
// MainAxisAlignment.spaceBetween,
// children: [
// const Text('Type of Business',
// style: TextStyle(fontSize: 16)),
// DropdownMenu<BusinessType>(
// initialSelection: business.type,
// controller: businessTypeController,
// label: const Text('Business Type'),
// dropdownMenuEntries: const [
// DropdownMenuEntry(
// value: BusinessType.food,
// label: 'Food Related'),
// DropdownMenuEntry(
// value: BusinessType.shop,
// label: 'Shop'),
// DropdownMenuEntry(
// value: BusinessType.outdoors,
// label: 'Outdoors'),
// DropdownMenuEntry(
// value: BusinessType.manufacturing,
// label: 'Manufacturing'),
// DropdownMenuEntry(
// value: BusinessType.entertainment,
// label: 'Entertainment'),
// DropdownMenuEntry(
// value: BusinessType.other,
// label: 'Other'),
// ],
// onSelected: (inputType) {
// business.type = inputType!;
// },
// ),
// ],
// ),
// ),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
@ -409,8 +415,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Contact Information Name',
labelText: 'Contact Information Name',
),
),
),

View File

@ -16,9 +16,9 @@ bool isBusinessesFiltered = true;
bool _isLoading = false;
class ExportData extends StatefulWidget {
final List<Business> businesses;
final Map<JobType, List<Business>> groupedBusinesses;
const ExportData({super.key, required this.businesses});
const ExportData({super.key, required this.groupedBusinesses});
@override
State<ExportData> createState() => _ExportDataState();
@ -31,7 +31,7 @@ class _ExportDataState extends State<ExportData> {
void initState() {
super.initState();
refreshBusinessDataFuture = fetchBusinessData();
refreshBusinessDataFuture = fetchBusinessDataOverview();
_isLoading = false;
selectedBusinesses = <Business>{};
}
@ -39,7 +39,7 @@ class _ExportDataState extends State<ExportData> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _FAB(businesses: widget.businesses),
floatingActionButton: _FAB(groupedBusinesses: widget.groupedBusinesses),
body: CustomScrollView(
slivers: [
SliverAppBar(
@ -196,7 +196,7 @@ class _ExportDataState extends State<ExportData> {
),
),
BusinessDisplayPanel(
businesses: widget.businesses,
groupedBusinesses: widget.groupedBusinesses,
widescreen: MediaQuery.sizeOf(context).width >= 1000,
selectable: true),
const SliverToBoxAdapter(
@ -211,15 +211,26 @@ class _ExportDataState extends State<ExportData> {
}
class _FAB extends StatefulWidget {
final List<Business> businesses;
final Map<JobType, List<Business>> groupedBusinesses;
const _FAB({required this.businesses});
const _FAB({required this.groupedBusinesses});
@override
State<_FAB> createState() => _FABState();
}
class _FABState extends State<_FAB> {
List<Business> allBusinesses = [];
@override
void initState() {
super.initState();
for (JobType type in widget.groupedBusinesses.keys) {
allBusinesses.addAll(widget.groupedBusinesses[type]!);
}
}
@override
Widget build(BuildContext context) {
return FloatingActionButton(
@ -273,7 +284,7 @@ class _FABState extends State<_FAB> {
List<pw.TableRow> rows = [];
if (selectedBusinesses.isEmpty) {
selectedBusinesses.addAll(widget.businesses);
selectedBusinesses.addAll(allBusinesses);
isBusinessesFiltered = false;
} else {
isBusinessesFiltered = true;
@ -284,9 +295,9 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.logo)) {
remainingSpace -= 32;
}
if (dataTypeFilters.contains(DataType.type)) {
remainingSpace -= 56;
}
// if (dataTypeFilters.contains(DataType.type)) {
// remainingSpace -= 56;
// }
if (dataTypeFilters.contains(DataType.contactName)) {
remainingSpace -= 72;
}
@ -331,9 +342,9 @@ class _FABState extends State<_FAB> {
} else if (dataType == DataType.description) {
width = pw.FixedColumnWidth(descriptionWidth);
columnNum++;
} else if (dataType == DataType.type) {
width = const pw.FixedColumnWidth(56);
columnNum++;
// } else if (dataType == DataType.type) {
// width = const pw.FixedColumnWidth(56);
// columnNum++;
} else if (dataType == DataType.website) {
width = pw.FixedColumnWidth(websiteWidth);
columnNum++;
@ -394,18 +405,18 @@ class _FABState extends State<_FAB> {
),
padding: const pw.EdgeInsets.all(4.0)));
}
if (dataTypeFilters.contains(DataType.type)) {
data.add(pw.Padding(
child: pw.Text(
business.type.name,
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
}
// if (dataTypeFilters.contains(DataType.type)) {
// data.add(pw.Padding(
// child: pw.Text(
// business.type.name,
// // style: const pw.TextStyle(fontSize: 10)
// ),
// padding: const pw.EdgeInsets.all(4.0)));
// }
if (dataTypeFilters.contains(DataType.website)) {
data.add(pw.Padding(
child: pw.Text(
business.website,
business.website ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@ -413,7 +424,7 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.contactName)) {
data.add(pw.Padding(
child: pw.Text(
business.contactName,
business.contactName ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@ -421,7 +432,7 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.contactEmail)) {
data.add(pw.Padding(
child: pw.Text(
business.contactEmail,
business.contactEmail ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@ -429,7 +440,7 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.contactPhone)) {
data.add(pw.Padding(
child: pw.Text(
business.contactPhone,
business.contactPhone ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@ -440,7 +451,7 @@ class _FABState extends State<_FAB> {
style = const pw.TextStyle(fontSize: 8);
}
data.add(pw.Padding(
child: pw.Text(business.notes, style: style),
child: pw.Text(business.notes ?? '', style: style),
padding: const pw.EdgeInsets.all(4.0)));
}
@ -461,10 +472,10 @@ class _FABState extends State<_FAB> {
} else {
rows.add(pw.TableRow(
children: [
pw.Padding(
child: getPwIconFromType(
business.type, 24, PdfColors.black),
padding: const pw.EdgeInsets.all(4.0)),
// pw.Padding(
// child: getPwIconFromType(
// business.type, 24, PdfColors.black),
// padding: const pw.EdgeInsets.all(4.0)),
...data
],
));

View File

@ -1,4 +1,3 @@
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';
@ -21,7 +20,7 @@ enum DataType {
logo,
name,
description,
type,
// type,
website,
contactName,
contactEmail,
@ -33,7 +32,7 @@ Map<DataType, int> dataTypeValues = {
DataType.logo: 0,
DataType.name: 1,
DataType.description: 2,
DataType.type: 3,
// DataType.type: 3,
DataType.website: 4,
DataType.contactName: 5,
DataType.contactEmail: 6,
@ -45,7 +44,7 @@ Map<DataType, String> dataTypeFriendly = {
DataType.logo: 'Logo',
DataType.name: 'Name',
DataType.description: 'Description',
DataType.type: 'Type',
// DataType.type: 'Type',
DataType.website: 'Website',
DataType.contactName: 'Contact Name',
DataType.contactEmail: 'Contact Email',
@ -71,80 +70,100 @@ enum BusinessType {
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;
BusinessType type;
String website;
String contactName;
String contactEmail;
String contactPhone;
String notes;
String locationName;
String locationAddress;
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.type,
required this.website,
required this.contactName,
required this.contactEmail,
required this.contactPhone,
required this.notes,
required this.locationName,
required this.locationAddress,
});
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<String, dynamic> json) {
bool typeValid = true;
try {
BusinessType.values.byName(json['type']);
} catch (e) {
typeValid = false;
List<JobListing>? 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'],
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'],
);
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,
type: input.type,
website: input.website,
contactName: input.contactName,
contactEmail: input.contactEmail,
contactPhone: input.contactPhone,
notes: input.notes,
locationName: input.locationName,
locationAddress: input.locationAddress,
);
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;
}
// Map<BusinessType, List<Business>> groupBusinesses(List<Business> businesses) {
// Map<BusinessType, List<Business>> groupedBusinesses =
// groupBy<Business, BusinessType>(businesses, (business) => business.type!);
//
// return groupedBusinesses;
// }
Icon getIconFromType(BusinessType type, double size, Color color) {
switch (type) {
@ -187,6 +206,35 @@ Icon getIconFromType(BusinessType type, double 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:
@ -204,6 +252,19 @@ pw.Icon getPwIconFromType(BusinessType type, double size, PdfColor 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:
@ -221,6 +282,19 @@ Text getNameFromType(BusinessType type, 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:
@ -233,13 +307,13 @@ Icon getIconFromThemeMode(ThemeMode theme) {
}
class BusinessDisplayPanel extends StatefulWidget {
final List<Business> businesses;
final Map<JobType, List<Business>> groupedBusinesses;
final bool widescreen;
final bool selectable;
const BusinessDisplayPanel(
{super.key,
required this.businesses,
required this.groupedBusinesses,
required this.widescreen,
required this.selectable});
@ -253,40 +327,42 @@ class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
@override
Widget build(BuildContext context) {
List<BusinessHeader> headers = [];
List<Business> 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();
// List<Business> filteredBusinesses = [];
// for (var business in widget.groupedBusinesses.) {
// if (business.name.toLowerCase().contains(searchFilter.toLowerCase())) {
// filteredBusinesses.add(business);
// }
// }
for (var i = 0; i < businessTypes.length; i++) {
if (filters.contains(businessTypes[i])) {
isFiltered = true;
}
if (filters.isNotEmpty) {
isFiltered = true;
}
// 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])) {
for (JobType jobType in widget.groupedBusinesses.keys) {
if (filters.contains(jobType)) {
headers.add(BusinessHeader(
type: businessTypes[i],
type: jobType,
widescreen: widget.widescreen,
selectable: widget.selectable,
selectedBusinesses: selectedBusinesses,
businesses: groupedBusinesses[businessTypes[i]]!));
businesses: widget.groupedBusinesses[jobType]!));
}
}
} else {
for (var i = 0; i < businessTypes.length; i++) {
for (JobType jobType in widget.groupedBusinesses.keys) {
headers.add(BusinessHeader(
type: businessTypes[i],
type: jobType,
widescreen: widget.widescreen,
selectable: widget.selectable,
selectedBusinesses: selectedBusinesses,
businesses: groupedBusinesses[businessTypes[i]]!));
businesses: widget.groupedBusinesses[jobType]!));
}
}
headers.sort((a, b) => a.type.index.compareTo(b.type.index));
@ -295,7 +371,7 @@ class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
}
class BusinessHeader extends StatefulWidget {
final BusinessType type;
final JobType type;
final List<Business> businesses;
final Set<Business> selectedBusinesses;
final bool widescreen;
@ -343,10 +419,10 @@ class _BusinessHeaderState extends State<BusinessHeader> {
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromType(
child: getIconFromJobType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary),
),
getNameFromType(
getNameFromJobType(
widget.type, Theme.of(context).colorScheme.onPrimary),
],
),
@ -376,10 +452,11 @@ class _BusinessHeaderState extends State<BusinessHeader> {
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromType(
child: getIconFromJobType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary),
),
getNameFromType(widget.type, Theme.of(context).colorScheme.onPrimary),
getNameFromJobType(
widget.type, Theme.of(context).colorScheme.onPrimary),
],
);
}
@ -404,6 +481,7 @@ class _BusinessHeaderState extends State<BusinessHeader> {
selectable: selectable,
widescreen: widescreen,
callback: refresh,
type: widget.type,
);
},
),
@ -418,6 +496,7 @@ class _BusinessHeaderState extends State<BusinessHeader> {
selectable: selectable,
widescreen: widescreen,
callback: refresh,
type: widget.type,
);
},
),
@ -431,13 +510,15 @@ class BusinessCard extends StatefulWidget {
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.callback,
required this.type});
@override
State<BusinessCard> createState() => _BusinessCardState();
@ -447,27 +528,31 @@ class _BusinessCardState extends State<BusinessCard> {
@override
Widget build(BuildContext context) {
if (widget.widescreen) {
return _businessTile(widget.business, widget.selectable);
return _businessTile(widget.business, widget.selectable, widget.type);
} else {
return _businessListItem(
widget.business, widget.selectable, widget.callback);
widget.business, widget.selectable, widget.callback, widget.type);
}
}
Widget _businessTile(Business business, bool selectable) {
Widget _businessTile(Business business, bool selectable, JobType type) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(inputBusiness: business)));
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),
_getTileRow(business, selectable, widget.callback, type),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
@ -490,7 +575,8 @@ class _BusinessCardState extends State<BusinessCard> {
Uri.parse('https://${business.website}'));
},
),
if (business.locationName.isNotEmpty)
if ((business.locationName != null) &&
(business.locationName != ''))
IconButton(
icon: const Icon(Icons.location_on),
onPressed: () {
@ -498,7 +584,8 @@ class _BusinessCardState extends State<BusinessCard> {
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
if (business.contactPhone.isNotEmpty)
if ((business.contactPhone != null) &&
(business.contactPhone != ''))
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {
@ -509,10 +596,14 @@ class _BusinessCardState extends State<BusinessCard> {
backgroundColor: Theme.of(context)
.colorScheme
.background,
title: Text(business.contactName.isEmpty
title: Text((business.contactName ==
null ||
business.contactName == '')
? 'Contact ${business.name}?'
: 'Contact ${business.contactName}'),
content: Text(business.contactName.isEmpty
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: [
@ -535,7 +626,8 @@ class _BusinessCardState extends State<BusinessCard> {
});
},
),
if (business.contactEmail.isNotEmpty)
if ((business.contactEmail != null) &&
(business.contactEmail != ''))
IconButton(
icon: const Icon(Icons.email),
onPressed: () {
@ -553,7 +645,8 @@ class _BusinessCardState extends State<BusinessCard> {
);
}
Widget _getTileRow(Business business, bool selectable, Function callback) {
Widget _getTileRow(
Business business, bool selectable, Function callback, JobType type) {
if (selectable) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -565,8 +658,8 @@ class _BusinessCardState extends State<BusinessCard> {
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);
return getIconFromJobType(
type, 48, Theme.of(context).colorScheme.onSurface);
}),
),
),
@ -598,8 +691,8 @@ class _BusinessCardState extends State<BusinessCard> {
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);
return getIconFromJobType(
type, 48, Theme.of(context).colorScheme.onSurface);
}),
)),
Flexible(
@ -620,7 +713,7 @@ class _BusinessCardState extends State<BusinessCard> {
}
Widget _businessListItem(
Business business, bool selectable, Function callback) {
Business business, bool selectable, Function callback, JobType type) {
return Card(
child: ListTile(
leading: ClipRRect(
@ -628,8 +721,8 @@ class _BusinessCardState extends State<BusinessCard> {
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);
return getIconFromJobType(
type, 24, Theme.of(context).colorScheme.onSurface);
})),
title: Text(business.name),
subtitle: Text(business.description,
@ -637,7 +730,11 @@ class _BusinessCardState extends State<BusinessCard> {
trailing: _getCheckbox(selectable, callback),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(inputBusiness: business)));
builder: (context) => BusinessDetail(
id: business.id,
name: business.name,
clickFromType: type,
)));
},
),
);
@ -688,7 +785,8 @@ class _FilterChipsState extends State<FilterChips> {
showCheckmark: false,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
label: getNameFromType(type, Theme.of(context).colorScheme.onSurface),
label:
getNameFromType(type, Theme.of(context).colorScheme.onSurface),
selected: selectedChips.contains(type),
onSelected: (bool selected) {
setState(() {