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

View File

@ -4,27 +4,111 @@ import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/pages/signin_page.dart'; import 'package:fbla_ui/pages/signin_page.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/shared.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class BusinessDetail extends StatefulWidget { 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 @override
State<BusinessDetail> createState() => _CreateBusinessDetailState(); State<BusinessDetail> createState() => _CreateBusinessDetailState();
} }
class _CreateBusinessDetailState extends State<BusinessDetail> { class _CreateBusinessDetailState extends State<BusinessDetail> {
late Future loadBusiness;
@override
void initState() {
super.initState();
loadBusiness = fetchBusiness(widget.id);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Business business = Business.copy(widget.inputBusiness); return FutureBuilder(
future: loadBusiness,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType != String) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(business.name), title: Text(snapshot.data.name),
actions: _getActions(business), actions: _getActions(snapshot.data, widget.clickFromType),
), ),
body: ListView( 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: [ children: [
// Title, logo, desc, website // Title, logo, desc, website
Card( Card(
@ -46,7 +130,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
width: 48, width: 48,
height: 48, errorBuilder: (BuildContext context, height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) { Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48, return getIconFromJobType(widget.clickFromType, 48,
Theme.of(context).colorScheme.onSurface); Theme.of(context).colorScheme.onSurface);
}), }),
), ),
@ -54,7 +138,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
ListTile( ListTile(
leading: const Icon(Icons.link), leading: const Icon(Icons.link),
title: const Text('Website'), title: const Text('Website'),
subtitle: Text(business.website, subtitle: Text(business.website!,
style: const TextStyle(color: Colors.blue)), style: const TextStyle(color: Colors.blue)),
onTap: () { onTap: () {
launchUrl(Uri.parse('https://${business.website}')); launchUrl(Uri.parse('https://${business.website}'));
@ -67,13 +151,11 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
Card( Card(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0), padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Column( child:
crossAxisAlignment: CrossAxisAlignment.start, Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ const Text(
Text(
'Available Postitions', 'Available Postitions',
style: const TextStyle( style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
fontSize: 20, fontWeight: FontWeight.bold),
), ),
// Container( // Container(
// height: 400, // height: 400,
@ -110,8 +192,8 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
), ),
// Contact info // Contact info
Visibility( Visibility(
visible: (business.contactEmail.isNotEmpty || visible:
business.contactPhone.isNotEmpty), (business.contactEmail != null || business.contactPhone != null),
child: Card( child: Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
@ -121,9 +203,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
Padding( Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0), padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Text( child: Text(
business.contactName.isEmpty business.contactName ?? 'Contact ${business.name}',
? 'Contact ${business.name}'
: business.contactName,
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold), fontSize: 20, fontWeight: FontWeight.bold),
@ -132,10 +212,11 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
], ],
), ),
Visibility( Visibility(
visible: business.contactPhone.isNotEmpty, visible: business.contactPhone != null,
child: ListTile( child: ListTile(
leading: Icon(Icons.phone), leading: Icon(Icons.phone),
title: Text(business.contactPhone), title: Text(business.contactPhone!),
// maybe replace ! with ?? ''. same is true for below
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
@ -143,10 +224,10 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
return AlertDialog( return AlertDialog(
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.background, Theme.of(context).colorScheme.background,
title: Text(business.contactName.isEmpty title: Text(business.contactName!.isEmpty
? 'Contact ${business.name}?' ? 'Contact ${business.name}?'
: 'Contact ${business.contactName}'), : 'Contact ${business.contactName}'),
content: Text(business.contactName.isEmpty content: Text(business.contactName!.isEmpty
? 'Would you like to call or text ${business.name}?' ? 'Would you like to call or text ${business.name}?'
: 'Would you like to call or text ${business.contactName}?'), : 'Would you like to call or text ${business.contactName}?'),
actions: [ actions: [
@ -171,10 +252,10 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
), ),
), ),
Visibility( Visibility(
visible: business.contactEmail.isNotEmpty, visible: business.contactEmail != null,
child: ListTile( child: ListTile(
leading: const Icon(Icons.email), leading: const Icon(Icons.email),
title: Text(business.contactEmail), title: Text(business.contactEmail!),
onTap: () { onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}')); launchUrl(Uri.parse('mailto:${business.contactEmail}'));
}, },
@ -190,8 +271,8 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: ListTile( child: ListTile(
leading: const Icon(Icons.location_on), leading: const Icon(Icons.location_on),
title: Text(business.locationName), title: Text(business.locationName!),
subtitle: Text(business.locationAddress), subtitle: Text(business.locationAddress!),
onTap: () { onTap: () {
launchUrl(Uri.parse(Uri.encodeFull( launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}'))); 'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
@ -201,7 +282,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
), ),
// Notes // Notes
Visibility( Visibility(
visible: business.notes.isNotEmpty, visible: business.notes != null,
child: Card( child: Card(
child: ListTile( child: ListTile(
leading: const Icon(Icons.notes), leading: const Icon(Icons.notes),
@ -209,24 +290,25 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
'Additional Notes:', 'Additional Notes:',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
), ),
subtitle: Text(business.notes), subtitle: Text(business.notes!),
), ),
), ),
), ),
], ],
),
); );
} }
List<Widget>? _getActions(Business business) { List<Widget>? _getActions(Business business, JobType clickFromType) {
if (loggedIn) { if (loggedIn) {
return [ return [
IconButton( IconButton(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
onPressed: () { onPressed: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) => builder: (context) => CreateEditBusiness(
CreateEditBusiness(inputBusiness: business))); inputBusiness: business,
clickFromType: clickFromType,
)));
}, },
), ),
IconButton( IconButton(
@ -250,7 +332,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
child: const Text('Yes'), child: const Text('Yes'),
onPressed: () async { onPressed: () async {
String? deleteResult = String? deleteResult =
await deleteBusiness(business, jwt); await deleteBusiness(business.id, jwt);
if (deleteResult != null) { if (deleteResult != null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import 'package:collection/collection.dart';
import 'package:fbla_ui/api_logic.dart'; import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/pages/business_detail.dart'; import 'package:fbla_ui/pages/business_detail.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -21,7 +20,7 @@ enum DataType {
logo, logo,
name, name,
description, description,
type, // type,
website, website,
contactName, contactName,
contactEmail, contactEmail,
@ -33,7 +32,7 @@ Map<DataType, int> dataTypeValues = {
DataType.logo: 0, DataType.logo: 0,
DataType.name: 1, DataType.name: 1,
DataType.description: 2, DataType.description: 2,
DataType.type: 3, // DataType.type: 3,
DataType.website: 4, DataType.website: 4,
DataType.contactName: 5, DataType.contactName: 5,
DataType.contactEmail: 6, DataType.contactEmail: 6,
@ -45,7 +44,7 @@ Map<DataType, String> dataTypeFriendly = {
DataType.logo: 'Logo', DataType.logo: 'Logo',
DataType.name: 'Name', DataType.name: 'Name',
DataType.description: 'Description', DataType.description: 'Description',
DataType.type: 'Type', // DataType.type: 'Type',
DataType.website: 'Website', DataType.website: 'Website',
DataType.contactName: 'Contact Name', DataType.contactName: 'Contact Name',
DataType.contactEmail: 'Contact Email', DataType.contactEmail: 'Contact Email',
@ -71,47 +70,68 @@ enum BusinessType {
other, 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 { class Business {
int id; int id;
String name; String name;
String description; String description;
BusinessType type; String? website;
String website; String? contactName;
String contactName; String? contactEmail;
String contactEmail; String? contactPhone;
String contactPhone; String? notes;
String notes; String? locationName;
String locationName; String? locationAddress;
String locationAddress; List<JobListing>? listings;
Business({ Business(
required this.id, {required this.id,
required this.name, required this.name,
required this.description, required this.description,
required this.type, this.website,
required this.website, this.contactName,
required this.contactName, this.contactEmail,
required this.contactEmail, this.contactPhone,
required this.contactPhone, this.notes,
required this.notes, this.locationName,
required this.locationName, this.locationAddress,
required this.locationAddress, this.listings});
});
factory Business.fromJson(Map<String, dynamic> json) { factory Business.fromJson(Map<String, dynamic> json) {
bool typeValid = true; List<JobListing>? listings = [];
try { for (int i = 0; i < json['listings'].length; i++) {
BusinessType.values.byName(json['type']); listings.add(JobListing(
} catch (e) { name: json['listings']['name'],
typeValid = false; description: json['listings']['description'],
type: json['listings']['type'],
wage: json['listings']['wage'],
link: json['listings']['link']));
} }
return Business( return Business(
id: json['id'], id: json['id'],
name: json['name'], name: json['name'],
description: json['description'], description: json['description'],
type: typeValid
? BusinessType.values.byName(json['type'])
: BusinessType.other,
website: json['website'], website: json['website'],
contactName: json['contactName'], contactName: json['contactName'],
contactEmail: json['contactEmail'], contactEmail: json['contactEmail'],
@ -119,7 +139,7 @@ class Business {
notes: json['notes'], notes: json['notes'],
locationName: json['locationName'], locationName: json['locationName'],
locationAddress: json['locationAddress'], locationAddress: json['locationAddress'],
); listings: listings);
} }
factory Business.copy(Business input) { factory Business.copy(Business input) {
@ -127,7 +147,6 @@ class Business {
id: input.id, id: input.id,
name: input.name, name: input.name,
description: input.description, description: input.description,
type: input.type,
website: input.website, website: input.website,
contactName: input.contactName, contactName: input.contactName,
contactEmail: input.contactEmail, contactEmail: input.contactEmail,
@ -135,16 +154,16 @@ class Business {
notes: input.notes, notes: input.notes,
locationName: input.locationName, locationName: input.locationName,
locationAddress: input.locationAddress, locationAddress: input.locationAddress,
); listings: input.listings);
} }
} }
Map<BusinessType, List<Business>> groupBusinesses(List<Business> businesses) { // Map<BusinessType, List<Business>> groupBusinesses(List<Business> businesses) {
Map<BusinessType, List<Business>> groupedBusinesses = // Map<BusinessType, List<Business>> groupedBusinesses =
groupBy<Business, BusinessType>(businesses, (business) => business.type); // groupBy<Business, BusinessType>(businesses, (business) => business.type!);
//
return groupedBusinesses; // return groupedBusinesses;
} // }
Icon getIconFromType(BusinessType type, double size, Color color) { Icon getIconFromType(BusinessType type, double size, Color color) {
switch (type) { 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) { pw.Icon getPwIconFromType(BusinessType type, double size, PdfColor color) {
switch (type) { switch (type) {
case BusinessType.food: 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) { Text getNameFromType(BusinessType type, Color color) {
switch (type) { switch (type) {
case BusinessType.food: 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) { Icon getIconFromThemeMode(ThemeMode theme) {
switch (theme) { switch (theme) {
case ThemeMode.dark: case ThemeMode.dark:
@ -233,13 +307,13 @@ Icon getIconFromThemeMode(ThemeMode theme) {
} }
class BusinessDisplayPanel extends StatefulWidget { class BusinessDisplayPanel extends StatefulWidget {
final List<Business> businesses; final Map<JobType, List<Business>> groupedBusinesses;
final bool widescreen; final bool widescreen;
final bool selectable; final bool selectable;
const BusinessDisplayPanel( const BusinessDisplayPanel(
{super.key, {super.key,
required this.businesses, required this.groupedBusinesses,
required this.widescreen, required this.widescreen,
required this.selectable}); required this.selectable});
@ -253,40 +327,42 @@ class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<BusinessHeader> headers = []; List<BusinessHeader> headers = [];
List<Business> filteredBusinesses = []; // List<Business> filteredBusinesses = [];
for (var business in widget.businesses) { // for (var business in widget.groupedBusinesses.) {
if (business.name.toLowerCase().contains(searchFilter.toLowerCase())) { // if (business.name.toLowerCase().contains(searchFilter.toLowerCase())) {
filteredBusinesses.add(business); // filteredBusinesses.add(business);
} // }
} // }
var groupedBusinesses = groupBusinesses(filteredBusinesses);
var businessTypes = groupedBusinesses.keys.toList();
for (var i = 0; i < businessTypes.length; i++) { if (filters.isNotEmpty) {
if (filters.contains(businessTypes[i])) {
isFiltered = true; isFiltered = true;
} }
}
// for (var i = 0; i < businessTypes.length; i++) {
// if (filters.contains(businessTypes[i])) {
// isFiltered = true;
// }
// }
if (isFiltered) { if (isFiltered) {
for (var i = 0; i < businessTypes.length; i++) { for (JobType jobType in widget.groupedBusinesses.keys) {
if (filters.contains(businessTypes[i])) { if (filters.contains(jobType)) {
headers.add(BusinessHeader( headers.add(BusinessHeader(
type: businessTypes[i], type: jobType,
widescreen: widget.widescreen, widescreen: widget.widescreen,
selectable: widget.selectable, selectable: widget.selectable,
selectedBusinesses: selectedBusinesses, selectedBusinesses: selectedBusinesses,
businesses: groupedBusinesses[businessTypes[i]]!)); businesses: widget.groupedBusinesses[jobType]!));
} }
} }
} else { } else {
for (var i = 0; i < businessTypes.length; i++) { for (JobType jobType in widget.groupedBusinesses.keys) {
headers.add(BusinessHeader( headers.add(BusinessHeader(
type: businessTypes[i], type: jobType,
widescreen: widget.widescreen, widescreen: widget.widescreen,
selectable: widget.selectable, selectable: widget.selectable,
selectedBusinesses: selectedBusinesses, selectedBusinesses: selectedBusinesses,
businesses: groupedBusinesses[businessTypes[i]]!)); businesses: widget.groupedBusinesses[jobType]!));
} }
} }
headers.sort((a, b) => a.type.index.compareTo(b.type.index)); headers.sort((a, b) => a.type.index.compareTo(b.type.index));
@ -295,7 +371,7 @@ class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
} }
class BusinessHeader extends StatefulWidget { class BusinessHeader extends StatefulWidget {
final BusinessType type; final JobType type;
final List<Business> businesses; final List<Business> businesses;
final Set<Business> selectedBusinesses; final Set<Business> selectedBusinesses;
final bool widescreen; final bool widescreen;
@ -343,10 +419,10 @@ class _BusinessHeaderState extends State<BusinessHeader> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0), padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromType( child: getIconFromJobType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary), widget.type, 24, Theme.of(context).colorScheme.onPrimary),
), ),
getNameFromType( getNameFromJobType(
widget.type, Theme.of(context).colorScheme.onPrimary), widget.type, Theme.of(context).colorScheme.onPrimary),
], ],
), ),
@ -376,10 +452,11 @@ class _BusinessHeaderState extends State<BusinessHeader> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0), padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromType( child: getIconFromJobType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary), 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, selectable: selectable,
widescreen: widescreen, widescreen: widescreen,
callback: refresh, callback: refresh,
type: widget.type,
); );
}, },
), ),
@ -418,6 +496,7 @@ class _BusinessHeaderState extends State<BusinessHeader> {
selectable: selectable, selectable: selectable,
widescreen: widescreen, widescreen: widescreen,
callback: refresh, callback: refresh,
type: widget.type,
); );
}, },
), ),
@ -431,13 +510,15 @@ class BusinessCard extends StatefulWidget {
final bool widescreen; final bool widescreen;
final bool selectable; final bool selectable;
final Function callback; final Function callback;
final JobType type;
const BusinessCard( const BusinessCard(
{super.key, {super.key,
required this.business, required this.business,
required this.widescreen, required this.widescreen,
required this.selectable, required this.selectable,
required this.callback}); required this.callback,
required this.type});
@override @override
State<BusinessCard> createState() => _BusinessCardState(); State<BusinessCard> createState() => _BusinessCardState();
@ -447,27 +528,31 @@ class _BusinessCardState extends State<BusinessCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.widescreen) { if (widget.widescreen) {
return _businessTile(widget.business, widget.selectable); return _businessTile(widget.business, widget.selectable, widget.type);
} else { } else {
return _businessListItem( 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( return MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(inputBusiness: business))); builder: (context) => BusinessDetail(
id: business.id,
name: business.name,
clickFromType: type,
)));
}, },
child: Card( child: Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
_getTileRow(business, selectable, widget.callback), _getTileRow(business, selectable, widget.callback, type),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
@ -490,7 +575,8 @@ class _BusinessCardState extends State<BusinessCard> {
Uri.parse('https://${business.website}')); Uri.parse('https://${business.website}'));
}, },
), ),
if (business.locationName.isNotEmpty) if ((business.locationName != null) &&
(business.locationName != ''))
IconButton( IconButton(
icon: const Icon(Icons.location_on), icon: const Icon(Icons.location_on),
onPressed: () { onPressed: () {
@ -498,7 +584,8 @@ class _BusinessCardState extends State<BusinessCard> {
'https://www.google.com/maps/search/?api=1&query=${business.locationName}'))); 'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
}, },
), ),
if (business.contactPhone.isNotEmpty) if ((business.contactPhone != null) &&
(business.contactPhone != ''))
IconButton( IconButton(
icon: const Icon(Icons.phone), icon: const Icon(Icons.phone),
onPressed: () { onPressed: () {
@ -509,10 +596,14 @@ class _BusinessCardState extends State<BusinessCard> {
backgroundColor: Theme.of(context) backgroundColor: Theme.of(context)
.colorScheme .colorScheme
.background, .background,
title: Text(business.contactName.isEmpty title: Text((business.contactName ==
null ||
business.contactName == '')
? 'Contact ${business.name}?' ? 'Contact ${business.name}?'
: 'Contact ${business.contactName}'), : '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.name}?'
: 'Would you like to call or text ${business.contactName}?'), : 'Would you like to call or text ${business.contactName}?'),
actions: [ actions: [
@ -535,7 +626,8 @@ class _BusinessCardState extends State<BusinessCard> {
}); });
}, },
), ),
if (business.contactEmail.isNotEmpty) if ((business.contactEmail != null) &&
(business.contactEmail != ''))
IconButton( IconButton(
icon: const Icon(Icons.email), icon: const Icon(Icons.email),
onPressed: () { 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) { if (selectable) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -565,8 +658,8 @@ class _BusinessCardState extends State<BusinessCard> {
child: Image.network('$apiAddress/logos/${business.id}', child: Image.network('$apiAddress/logos/${business.id}',
height: 48, width: 48, errorBuilder: (BuildContext context, height: 48, width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) { Object exception, StackTrace? stackTrace) {
return getIconFromType( return getIconFromJobType(
business.type, 48, Theme.of(context).colorScheme.onSurface); type, 48, Theme.of(context).colorScheme.onSurface);
}), }),
), ),
), ),
@ -598,8 +691,8 @@ class _BusinessCardState extends State<BusinessCard> {
child: Image.network('$apiAddress/logos/${business.id}', child: Image.network('$apiAddress/logos/${business.id}',
height: 48, width: 48, errorBuilder: (BuildContext context, height: 48, width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) { Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48, return getIconFromJobType(
Theme.of(context).colorScheme.onSurface); type, 48, Theme.of(context).colorScheme.onSurface);
}), }),
)), )),
Flexible( Flexible(
@ -620,7 +713,7 @@ class _BusinessCardState extends State<BusinessCard> {
} }
Widget _businessListItem( Widget _businessListItem(
Business business, bool selectable, Function callback) { Business business, bool selectable, Function callback, JobType type) {
return Card( return Card(
child: ListTile( child: ListTile(
leading: ClipRRect( leading: ClipRRect(
@ -628,8 +721,8 @@ class _BusinessCardState extends State<BusinessCard> {
child: Image.network('$apiAddress/logos/${business.id}', child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context, height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) { Object exception, StackTrace? stackTrace) {
return getIconFromType( return getIconFromJobType(
business.type, 24, Theme.of(context).colorScheme.onSurface); type, 24, Theme.of(context).colorScheme.onSurface);
})), })),
title: Text(business.name), title: Text(business.name),
subtitle: Text(business.description, subtitle: Text(business.description,
@ -637,7 +730,11 @@ class _BusinessCardState extends State<BusinessCard> {
trailing: _getCheckbox(selectable, callback), trailing: _getCheckbox(selectable, callback),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( 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, showCheckmark: false,
shape: shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), 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), selected: selectedChips.contains(type),
onSelected: (bool selected) { onSelected: (bool selected) {
setState(() { setState(() {