diff --git a/fbla_ui/lib/home.dart b/fbla_ui/lib/home.dart index 4bb5b40..cda5496 100644 --- a/fbla_ui/lib/home.dart +++ b/fbla_ui/lib/home.dart @@ -22,15 +22,15 @@ class Home extends StatefulWidget { } class _HomeState extends State { - late Future refreshBusinessDataFuture; + late Future refreshBusinessDataOverviewFuture; bool _isPreviousData = false; - late List businesses; + late Map> overviewBusinesses; @override void initState() { super.initState(); - refreshBusinessDataFuture = fetchBusinessData(); + refreshBusinessDataOverviewFuture = fetchBusinessDataOverview(); initialLogin(); } @@ -65,10 +65,10 @@ class _HomeState extends State { 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 { Navigator.push( context, MaterialPageRoute( - builder: (context) => - ExportData(businesses: businesses))); + builder: (context) => ExportData( + groupedBusinesses: overviewBusinesses))); } }, ), @@ -245,7 +245,7 @@ class _HomeState extends State { ], ), FutureBuilder( - future: refreshBusinessDataFuture, + future: refreshBusinessDataOverviewFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasData) { @@ -263,9 +263,11 @@ class _HomeState extends State { 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 { )); } - 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 { ConnectionState.waiting) { if (_isPreviousData) { return BusinessDisplayPanel( - businesses: businesses, + groupedBusinesses: overviewBusinesses, widescreen: widescreen, selectable: false); } else { @@ -339,7 +341,7 @@ class _HomeState extends State { Widget? _getFAB() { if (loggedIn) { return FloatingActionButton( - child: const Icon(Icons.add), + child: const Icon(Icons.add_business), onPressed: () { Navigator.push( context, diff --git a/fbla_ui/lib/pages/business_detail.dart b/fbla_ui/lib/pages/business_detail.dart index d8f2d36..9ccdcfc 100644 --- a/fbla_ui/lib/pages/business_detail.dart +++ b/fbla_ui/lib/pages/business_detail.dart @@ -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 createState() => _CreateBusinessDetailState(); } class _CreateBusinessDetailState extends State { + 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? _getActions(Business business) { + List? _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 { 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( diff --git a/fbla_ui/lib/pages/create_edit_business.dart b/fbla_ui/lib/pages/create_edit_business.dart index 06a750f..1425dd3 100644 --- a/fbla_ui/lib/pages/create_edit_business.dart +++ b/fbla_ui/lib/pages/create_edit_business.dart @@ -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 createState() => _CreateEditBusinessState(); @@ -27,7 +28,6 @@ class _CreateEditBusinessState extends State { 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 { '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 { 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 { ), ), ), - 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( - 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( + // 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 { FocusScope.of(context).unfocus(); }, decoration: const InputDecoration( - labelText: - 'Contact Information Name', + labelText: 'Contact Information Name', ), ), ), diff --git a/fbla_ui/lib/pages/export_data.dart b/fbla_ui/lib/pages/export_data.dart index e955255..57a15d9 100644 --- a/fbla_ui/lib/pages/export_data.dart +++ b/fbla_ui/lib/pages/export_data.dart @@ -16,9 +16,9 @@ bool isBusinessesFiltered = true; bool _isLoading = false; class ExportData extends StatefulWidget { - final List businesses; + final Map> groupedBusinesses; - const ExportData({super.key, required this.businesses}); + const ExportData({super.key, required this.groupedBusinesses}); @override State createState() => _ExportDataState(); @@ -31,7 +31,7 @@ class _ExportDataState extends State { void initState() { super.initState(); - refreshBusinessDataFuture = fetchBusinessData(); + refreshBusinessDataFuture = fetchBusinessDataOverview(); _isLoading = false; selectedBusinesses = {}; } @@ -39,7 +39,7 @@ class _ExportDataState extends State { @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 { ), ), 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 { } class _FAB extends StatefulWidget { - final List businesses; + final Map> groupedBusinesses; - const _FAB({required this.businesses}); + const _FAB({required this.groupedBusinesses}); @override State<_FAB> createState() => _FABState(); } class _FABState extends State<_FAB> { + List 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 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 ], )); diff --git a/fbla_ui/lib/shared.dart b/fbla_ui/lib/shared.dart index f961ff4..58e5ac9 100644 --- a/fbla_ui/lib/shared.dart +++ b/fbla_ui/lib/shared.dart @@ -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 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 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? 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 json) { - bool typeValid = true; - try { - BusinessType.values.byName(json['type']); - } catch (e) { - typeValid = false; + List? 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> groupBusinesses(List businesses) { - Map> groupedBusinesses = - groupBy(businesses, (business) => business.type); - - return groupedBusinesses; -} +// Map> groupBusinesses(List businesses) { +// Map> groupedBusinesses = +// groupBy(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 businesses; + final Map> 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 { @override Widget build(BuildContext context) { List headers = []; - List 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 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 { } class BusinessHeader extends StatefulWidget { - final BusinessType type; + final JobType type; final List businesses; final Set selectedBusinesses; final bool widescreen; @@ -343,10 +419,10 @@ class _BusinessHeaderState extends State { 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 { 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 { selectable: selectable, widescreen: widescreen, callback: refresh, + type: widget.type, ); }, ), @@ -418,6 +496,7 @@ class _BusinessHeaderState extends State { 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 createState() => _BusinessCardState(); @@ -447,27 +528,31 @@ class _BusinessCardState extends State { @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 { 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 { '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 { 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 { }); }, ), - 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 { ); } - 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 { 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 { 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 { } 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 { 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 { 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 { 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(() {