From e1f8c15e9ad230831b8286553b0c7db2bf9eba12 Mon Sep 17 00:00:00 2001 From: drake Date: Tue, 25 Jun 2024 18:27:53 -0500 Subject: [PATCH] More fixes --- README.md | 41 ++++-- fbla-api/lib/fbla_api.dart | 99 ++++++------- fbla_ui/lib/pages/business_detail.dart | 65 +++++---- fbla_ui/lib/pages/businesses_overview.dart | 12 +- fbla_ui/lib/pages/create_edit_business.dart | 81 +++++++---- fbla_ui/lib/pages/create_edit_listing.dart | 152 ++++++++++++++------ fbla_ui/lib/pages/listing_detail.dart | 110 ++++++++------ fbla_ui/lib/pages/listings_overview.dart | 18 +-- fbla_ui/lib/shared/api_logic.dart | 45 ++++-- fbla_ui/lib/shared/utils.dart | 70 +++++++-- 10 files changed, 454 insertions(+), 239 deletions(-) diff --git a/README.md b/README.md index 349f708..7127191 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ - # Job Link This is my app `Job Link` for the 2023-2024 FBLA Coding and Programming event.\ WI SLC Winner. -It uses the [Flutter Framework](https://flutter.dev/) for the front end and the [Dart Language](https://dart.dev/) for both the front end and API. +It uses the [Flutter Framework](https://flutter.dev/) for the front end and +the [Dart Language](https://dart.dev/) for both the front end and API. ## 3rd Party Libraries @@ -16,24 +16,32 @@ It uses the [Flutter Framework](https://flutter.dev/) for the front end and the - [open_filex](https://pub.dev/packages/open_filex) - [postgres](https://pub.dev/packages/postgres) - [argon2](https://pub.dev/packages/argon2) +- [rive](https://pub.dev/packages/rive) + +[Clearbit's logo API](https://clearbit.com/logo) was also used to get logos for businesses. ## Requirements ### Job Link -- **OS**: Windows, Linux, MacOS, Android, IOS. Note that some releases may not contain a MacOS or IOS build. + +- **OS**: Windows, Linux, MacOS, Android, IOS. Note that some releases may not contain a MacOS or + IOS build. - Stable internet connection. ### API + - **OS**: Windows, Linux, MacOS. - Stable internet connection. ## Installation/Usage -Please view the README in [fbla_ui](fbla_ui/README.md) and [fbla-api](fbla-api/README.md) for specific instructions for installation and usage for each part of the app. +Please view the README in [fbla_ui](fbla_ui/README.md) and [fbla-api](fbla-api/README.md) for +specific instructions for installation and usage for each part of the app. ## Competitions -[Here](https://docs.google.com/presentation/d/1ZbSE9RqobU2T-NDIm3CUtT_9nEhOm3_B47NZl1-c_QA) is the presentation used for competitions. +[Here](https://docs.google.com/presentation/d/1ZbSE9RqobU2T-NDIm3CUtT_9nEhOm3_B47NZl1-c_QA) is the +presentation used for competitions. ### WI State Leadership Conference @@ -44,13 +52,24 @@ Used release 0.1.1\ Questions asked (with my answers): **Q**: Why did you decide to use the apps (framework and tools I assume) you used?\ -**A**: I decided to use Flutter primarily because of its cross-platform capabilities and its integration with the [Material design specification](https://m3.material.io/). I also decided to use it because I had some previous experience with Flutter and Dart, and Dart is somewhat similar to Java which I also had experience with. +**A**: I decided to use Flutter primarily because of its cross-platform capabilities and its +integration with the [Material design specification](https://m3.material.io/). I also decided to use +it because I had some previous experience with Flutter and Dart, and Dart is somewhat similar to +Java which I also had experience with. -This one seems to just be a clarification for the `User input is validated` category of the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) (page 5-6)\ -**Q**: You mentioned that you validated that the description [of the business] is required, do you validate all the inputs or just that?\ -**A**: I validate that all of the required fields are inputted, and I also check for a valid email address and website format. +This one seems to just be a clarification for the `User input is validated` category of +the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) ( +page 5-6)\ +**Q**: You mentioned that you validated that the description [of the business] is required, do you +validate all the inputs or just that?\ +**A**: I validate that all of the required fields are inputted, and I also check for a valid email +address and website format. -This seems to be for the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) (page 5-6) `Code Quality` section.\ +This seems to be for +the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) ( +page 5-6) `Code Quality` section.\ While looking through a binder of the presentation and source code:\ **Q**: Is your source code all properly commented?\ -**A**: While it is hard to comment UI code because of the nature of it, I did my best to use comments where applicable, and I also provide extensive documentation of the installation and usage of the app in the documentation in the README files. +**A**: While it is hard to comment UI code because of the nature of it, I did my best to use +comments where applicable, and I also provide extensive documentation of the installation and usage +of the app in the documentation in the README files. diff --git a/fbla-api/lib/fbla_api.dart b/fbla-api/lib/fbla_api.dart index f023d5d..7ee5881 100644 --- a/fbla-api/lib/fbla_api.dart +++ b/fbla-api/lib/fbla_api.dart @@ -22,7 +22,17 @@ enum BusinessType { other, } -enum JobType { cashier, server, mechanic, other } +enum JobType { + retail, + customerService, + foodService, + finance, + healthcare, + education, + maintenance, + manufacturing, + other, +} enum OfferType { job, internship, apprenticeship } @@ -177,53 +187,46 @@ void main() async { OfferType.values.asNameMap().keys; var postgresResult = (await postgres.query(''' - WITH business_listings AS ( - SELECT b.id AS business_id, - b.name AS business_name, - b."contactName" AS business_contactName, - b."contactEmail" AS business_contactEmail, - b."contactPhone" AS business_contactPhone, - b."locationName" AS business_locationName, - b."locationAddress" AS business_locationAddress, - l.type AS listing_type, - json_agg( - json_build_object( - 'id', l.id, - 'businessId', l."businessId", - 'name', l.name, - 'description', l.description, - 'type', l.type, - 'offerType', l."offerType", - 'wage', l.wage, - 'link', l.link - ) - ) AS listings - FROM businesses b - JOIN listings l ON b.id = l."businessId" - WHERE l.type IN (${typeFilters.map((element) => "'$element'").join(',')}) - AND l."offerType" IN (${offerFilters.map((element) => "'$element'").join(',')}) - GROUP BY b.id, b.name, b."contactName", b."contactEmail", b."contactPhone", - b."locationName", b."locationAddress", l.type - ), - aggregated_businesses AS ( - SELECT listing_type, - json_agg( - json_build_object( - 'id', business_id, - 'name', business_name, - 'contactName', business_contactName, - 'contactEmail', business_contactEmail, - 'contactPhone', business_contactPhone, - 'locationName', business_locationName, - 'locationAddress', business_locationAddress, - 'listings', listings - ) - ) AS businesses - FROM business_listings - GROUP BY listing_type - ) - SELECT jsonb_object_agg(listing_type, businesses) AS result - FROM aggregated_businesses; + SELECT jsonb_agg( + jsonb_build_object( + 'id', b.id, + 'name', b.name, + 'contactName', b."contactName", + 'contactEmail', b."contactEmail", + 'contactPhone', b."contactPhone", + 'locationName', b."locationName", + 'locationAddress', b."locationAddress", + 'listings', b.listings + ) + ) AS result + FROM ( + SELECT + businesses.id, + businesses.name, + businesses."contactName", + businesses."contactEmail", + businesses."contactPhone", + businesses."locationName", + businesses."locationAddress", + jsonb_agg( + jsonb_build_object( + 'id', listings.id, + 'name', listings.name, + 'description', listings.description, + 'type', listings.type, + 'wage', listings.wage, + 'link', listings.link, + 'offerType', listings."offerType" + ) + ) AS listings + FROM businesses + JOIN listings ON businesses.id = listings."businessId" + AND listings.type IN (${typeFilters.map((element) => "'$element'").join(',')}) + AND listings."offerType" IN (${offerFilters.map((element) => "'$element'").join(',')}) + GROUP BY businesses.id, businesses.name, businesses."contactName", businesses."contactEmail", + businesses."contactPhone", businesses."locationName", businesses."locationAddress" + ) b + WHERE b.listings IS NOT NULL; ''')); return Response.ok( diff --git a/fbla_ui/lib/pages/business_detail.dart b/fbla_ui/lib/pages/business_detail.dart index 34ffb7d..0fe6214 100644 --- a/fbla_ui/lib/pages/business_detail.dart +++ b/fbla_ui/lib/pages/business_detail.dart @@ -22,6 +22,7 @@ class BusinessDetail extends StatefulWidget { class _CreateBusinessDetailState extends State { late Future loadBusiness; + bool _isRetrying = false; @override void initState() { @@ -60,11 +61,17 @@ class _CreateBusinessDetailState extends State { padding: const EdgeInsets.all(8.0), child: FilledButton( child: const Text('Retry'), - onPressed: () { - var refreshedData = fetchBusiness(widget.id); - setState(() { - loadBusiness = refreshedData; - }); + onPressed: () async { + if (!_isRetrying) { + setState(() { + _isRetrying = true; + }); + var refreshedData = + await fetchBusiness(widget.id); + setState(() { + loadBusiness = refreshedData; + }); + } }, ), ), @@ -120,28 +127,32 @@ class _CreateBusinessDetailState extends State { clipBehavior: Clip.antiAlias, child: Column( children: [ - ListTile( - titleAlignment: ListTileTitleAlignment.titleHeight, - title: Text(business.name!, - style: const TextStyle( - fontSize: 24, fontWeight: FontWeight.bold)), - subtitle: Text( - business.description!, - ), - contentPadding: - const EdgeInsets.only(bottom: 8, left: 16), - 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 Icon( - getIconFromBusinessType( - business.type ?? BusinessType.other), - size: 48); - }), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ListTile( + titleAlignment: ListTileTitleAlignment.titleHeight, + title: Text(business.name!, + style: const TextStyle( + fontSize: 24, fontWeight: FontWeight.bold)), + subtitle: Text( + business.description!, + ), + contentPadding: + const EdgeInsets.only(bottom: 8, left: 16), + 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 Icon( + getIconFromBusinessType( + business.type ?? BusinessType.other), + size: 48); + }), + ), ), ), if (business.website != null) diff --git a/fbla_ui/lib/pages/businesses_overview.dart b/fbla_ui/lib/pages/businesses_overview.dart index 6bf3d93..bc0b4a6 100644 --- a/fbla_ui/lib/pages/businesses_overview.dart +++ b/fbla_ui/lib/pages/businesses_overview.dart @@ -40,6 +40,7 @@ class _BusinessesOverviewState extends State { ScrollController controller = ScrollController(); bool _extended = true; double prevPixelPosition = 0; + bool _isRetrying = false; Map> _filterBySearch( Map> businesses, String query) { @@ -138,9 +139,14 @@ class _BusinessesOverviewState extends State { padding: const EdgeInsets.all(8.0), child: FilledButton( child: const Text('Retry'), - onPressed: () { - widget.updateBusinessesCallback( - businessTypeFilters); + onPressed: () async { + if (!_isRetrying) { + setState(() { + _isRetrying = true; + }); + await widget.updateBusinessesCallback( + businessTypeFilters); + } }, ), ), diff --git a/fbla_ui/lib/pages/create_edit_business.dart b/fbla_ui/lib/pages/create_edit_business.dart index 68f92d8..553fba3 100644 --- a/fbla_ui/lib/pages/create_edit_business.dart +++ b/fbla_ui/lib/pages/create_edit_business.dart @@ -103,7 +103,17 @@ class _CreateEditBusinessState extends State { ) : const Icon(Icons.save), onPressed: () async { - await _saveBusiness(context); + if (!_isLoading) { + await _saveBusiness(context); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + width: 400, + behavior: SnackBarBehavior.floating, + content: Text('Please wait for it to save.'), + ), + ); + } }, ) : null, @@ -114,28 +124,40 @@ class _CreateEditBusinessState extends State { width: 800, child: Column( children: [ - ListTile( - titleAlignment: ListTileTitleAlignment.titleHeight, - title: Text(business.name!, - style: const TextStyle( - fontSize: 24, fontWeight: FontWeight.bold)), - subtitle: Text( - business.description!, - ), - contentPadding: - const EdgeInsets.only(bottom: 8, left: 16), - leading: ClipRRect( - borderRadius: BorderRadius.circular(6.0), - child: Image.network( - 'https://logo.clearbit.com/${business.website}', - width: 48, - height: 48, errorBuilder: (BuildContext context, - Object exception, StackTrace? stackTrace) { - return Icon( - getIconFromBusinessType( - business.type ?? BusinessType.other), - size: 48); - }), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Card( + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ListTile( + titleAlignment: + ListTileTitleAlignment.titleHeight, + title: Text(business.name!, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold)), + subtitle: Text( + business.description!, + ), + contentPadding: + const EdgeInsets.only(bottom: 8, left: 16), + leading: ClipRRect( + borderRadius: BorderRadius.circular(6.0), + child: Image.network( + 'https://logo.clearbit.com/${business.website}', + width: 48, + height: 48, errorBuilder: + (BuildContext context, + Object exception, + StackTrace? stackTrace) { + return Icon( + getIconFromBusinessType(business.type ?? + BusinessType.other), + size: 48); + }), + ), + ), + ), ), ), Card( @@ -540,7 +562,18 @@ class _CreateEditBusinessState extends State { ], ), onPressed: () async { - await _saveBusiness(context); + if (!_isLoading) { + await _saveBusiness(context); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + width: 400, + behavior: SnackBarBehavior.floating, + content: + Text('Please wait for it to save.'), + ), + ); + } }, ), ), diff --git a/fbla_ui/lib/pages/create_edit_listing.dart b/fbla_ui/lib/pages/create_edit_listing.dart index 9490c66..a19a4c9 100644 --- a/fbla_ui/lib/pages/create_edit_listing.dart +++ b/fbla_ui/lib/pages/create_edit_listing.dart @@ -38,6 +38,8 @@ class _CreateEditJobListingState extends State { link: null, offerType: null); bool _isLoading = false; + late String businessName; + bool _isRetrying = false; @override void initState() { @@ -57,6 +59,7 @@ class _CreateEditJobListingState extends State { .replaceAll('http://', '') .replaceAll('www.', '')); getBusinessNameMapping = fetchBusinessNames(); + businessName = widget.inputBusiness?.name ?? 'Offering business'; } final formKey = GlobalKey(); @@ -92,7 +95,17 @@ class _CreateEditJobListingState extends State { ) : const Icon(Icons.save), onPressed: () async { - await _saveListing(context); + if (!_isLoading) { + await _saveListing(context); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + width: 400, + behavior: SnackBarBehavior.floating, + content: Text('Please wait for it to save.'), + ), + ); + } }) : null, body: FutureBuilder( @@ -112,11 +125,17 @@ class _CreateEditJobListingState extends State { child: FilledButton( child: const Text('Retry'), onPressed: () async { - var refreshedData = fetchBusinessNames(); - await refreshedData; - setState(() { - getBusinessNameMapping = refreshedData; - }); + if (!_isRetrying) { + setState(() { + _isRetrying = true; + }); + var refreshedData = fetchBusinessNames(); + await refreshedData; + setState(() { + getBusinessNameMapping = refreshedData; + _isRetrying = false; + }); + } }, ), ), @@ -135,43 +154,55 @@ class _CreateEditJobListingState extends State { width: 800, child: Column( children: [ - ListTile( - titleAlignment: - ListTileTitleAlignment.titleHeight, - title: Text(listing.name, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold)), - subtitle: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - getNameFromJobType( - listing.type ?? JobType.other), - style: const TextStyle(fontSize: 18), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Card( + child: Padding( + padding: + const EdgeInsets.only(right: 8.0), + child: ListTile( + titleAlignment: ListTileTitleAlignment + .titleHeight, + title: Text(listing.name, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold)), + subtitle: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + businessName, + style: const TextStyle( + fontSize: 18), + ), + Text( + listing.description, + ), + ], + ), + contentPadding: + const EdgeInsets.only(left: 16), + leading: ClipRRect( + borderRadius: + BorderRadius.circular(6.0), + child: Image.network( + '$apiAddress/logos/${listing.businessId}', + width: 48, + height: 48, errorBuilder: + (BuildContext context, + Object exception, + StackTrace? + stackTrace) { + return Icon( + getIconFromJobType( + listing.type ?? + JobType.other), + size: 48); + }), + ), ), - Text( - listing.description, - ), - ], - ), - contentPadding: - const EdgeInsets.only(left: 16), - leading: ClipRRect( - borderRadius: BorderRadius.circular(6.0), - child: Image.network( - '$apiAddress/logos/${listing.businessId}', - width: 48, - height: 48, errorBuilder: - (BuildContext context, - Object exception, - StackTrace? stackTrace) { - return Icon( - getIconFromJobType( - listing.type ?? JobType.other), - size: 48); - }), + ), ), ), // Business Type Dropdown @@ -286,6 +317,11 @@ class _CreateEditJobListingState extends State { setState(() { listing.businessId = inputType!; + businessName = nameMapping + .where((element) => + element['id'] == + listing.businessId) + .first['name']; businessDropdownErrorText = null; }); @@ -435,7 +471,7 @@ class _CreateEditJobListingState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: FilledButton( - child: const Row( + child: Row( mainAxisSize: MainAxisSize.min, children: [ Padding( @@ -443,13 +479,39 @@ class _CreateEditJobListingState extends State { top: 8.0, right: 8.0, bottom: 8.0), - child: Icon(Icons.save), + child: _isLoading + ? SizedBox( + width: 24, + height: 24, + child: + CircularProgressIndicator( + color: + Theme.of(context) + .colorScheme + .onPrimary, + strokeWidth: 3.0, + ), + ) + : Icon(Icons.save), ), Text('Save'), ], ), onPressed: () async { - await _saveListing(context); + if (!_isLoading) { + await _saveListing(context); + } else { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + width: 400, + behavior: + SnackBarBehavior.floating, + content: Text( + 'Please wait for it to save.'), + ), + ); + } }, ), ), diff --git a/fbla_ui/lib/pages/listing_detail.dart b/fbla_ui/lib/pages/listing_detail.dart index 0b313a1..dcd4672 100644 --- a/fbla_ui/lib/pages/listing_detail.dart +++ b/fbla_ui/lib/pages/listing_detail.dart @@ -1,4 +1,5 @@ import 'package:fbla_ui/main.dart'; +import 'package:fbla_ui/pages/business_detail.dart'; import 'package:fbla_ui/pages/create_edit_listing.dart'; import 'package:fbla_ui/shared/api_logic.dart'; import 'package:fbla_ui/shared/global_vars.dart'; @@ -44,53 +45,70 @@ class _CreateBusinessDetailState extends State { clipBehavior: Clip.antiAlias, child: Column( children: [ - ListTile( - minVerticalPadding: 0, - titleAlignment: ListTileTitleAlignment.titleHeight, - title: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '${listing.name} (${getNameFromOfferType(listing.offerType!)})', - style: const TextStyle( - fontSize: 24, fontWeight: FontWeight.bold)), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - getNameFromJobType(listing.type!), - style: const TextStyle(fontSize: 18), - ), - Text( - listing.description, - ), - ], - ), - contentPadding: - const EdgeInsets.only(bottom: 8, left: 16), - leading: Badge( - label: Text( - getLetterFromOfferType(listing.offerType!), - style: const TextStyle(fontSize: 16), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ListTile( + minVerticalPadding: 0, + titleAlignment: ListTileTitleAlignment.titleHeight, + title: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '${listing.name} (${getNameFromOfferType(listing.offerType!)})', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold)), ), - largeSize: 26, - offset: const Offset(15, -5), - textColor: Colors.white, - backgroundColor: - getColorFromOfferType(listing.offerType!), - child: ClipRRect( - borderRadius: BorderRadius.circular(6.0), - child: Image.network( - '$apiAddress/logos/${listing.businessId}', - width: 48, - height: 48, errorBuilder: - (BuildContext context, Object exception, - StackTrace? stackTrace) { - return Icon( - getIconFromJobType( - listing.type ?? JobType.other), - size: 48); - }), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + Navigator.push(context, + MaterialPageRoute(builder: (context) { + return BusinessDetail( + id: widget.fromBusiness.id, + name: widget.fromBusiness.name!); + })); + }, + child: Text( + widget.fromBusiness.name!, + style: const TextStyle(fontSize: 18), + ), + ), + ), + Text( + listing.description, + ), + ], + ), + contentPadding: + const EdgeInsets.only(bottom: 8, left: 16), + leading: Badge( + label: Text( + getLetterFromOfferType(listing.offerType!), + style: const TextStyle(fontSize: 16), + ), + largeSize: 24, + offset: const Offset(12, -3), + textColor: Colors.white, + backgroundColor: + getColorFromOfferType(listing.offerType!), + child: ClipRRect( + borderRadius: BorderRadius.circular(6.0), + child: Image.network( + '$apiAddress/logos/${widget.fromBusiness.id}', + width: 48, + height: 48, errorBuilder: + (BuildContext context, Object exception, + StackTrace? stackTrace) { + return Icon( + getIconFromJobType( + listing.type ?? JobType.other), + size: 48); + }), + ), ), ), ), diff --git a/fbla_ui/lib/pages/listings_overview.dart b/fbla_ui/lib/pages/listings_overview.dart index 087fe88..b216cff 100644 --- a/fbla_ui/lib/pages/listings_overview.dart +++ b/fbla_ui/lib/pages/listings_overview.dart @@ -42,6 +42,7 @@ class _JobsOverviewState extends State { ScrollController controller = ScrollController(); bool _extended = true; double prevPixelPosition = 0; + bool _isRetrying = false; Map> _filterBySearch( Map> businesses, String query) { @@ -145,8 +146,13 @@ class _JobsOverviewState extends State { padding: const EdgeInsets.all(8.0), child: FilledButton( child: const Text('Retry'), - onPressed: () { - widget.updateBusinessesCallback(null, null); + onPressed: () async { + if (!_isRetrying) { + setState(() { + _isRetrying = true; + }); + await widget.updateBusinessesCallback(null, null); + } }, ), ), @@ -534,12 +540,8 @@ class _JobHeaderState extends State<_JobHeader> { business.listings![0].offerType!), style: const TextStyle(fontSize: 16), ), - largeSize: 26, - padding: business.listings![0].offerType! == - OfferType.internship - ? const EdgeInsets.symmetric(horizontal: 5) - : null, - offset: const Offset(13, -2), + largeSize: 24, + offset: const Offset(12, -3), textColor: Colors.white, backgroundColor: getColorFromOfferType( business.listings![0].offerType!), diff --git a/fbla_ui/lib/shared/api_logic.dart b/fbla_ui/lib/shared/api_logic.dart index ff7eecc..d8ff78e 100644 --- a/fbla_ui/lib/shared/api_logic.dart +++ b/fbla_ui/lib/shared/api_logic.dart @@ -6,8 +6,8 @@ import 'package:fbla_ui/shared/global_vars.dart'; import 'package:fbla_ui/shared/utils.dart'; import 'package:http/http.dart' as http; -var apiAddress = 'https://homelab.marinodev.com/fbla-api'; -// var apiAddress = 'http://192.168.0.114:8000/fbla-api'; +// var apiAddress = 'https://homelab.marinodev.com/fbla-api'; +var apiAddress = 'http://192.168.0.114:8000/fbla-api'; var client = http.Client(); @@ -67,21 +67,40 @@ Future fetchBusinessDataOverviewJobs( var response = await http.get(uri).timeout(const Duration(seconds: 20)); if (response.statusCode == 200) { - var decodedResponse = json.decode(response.body); + List> decodedResponse = + json.decode(response.body).cast>(); + + List initialBusinesses = + decodedResponse.map((element) => Business.fromJson(element)).toList(); + Map> groupedBusinesses = {}; - for (String stringType in decodedResponse.keys) { - List businesses = []; - - for (Map map in decodedResponse[stringType]) { - Business business = Business.fromJson(map); - businesses.add(business); + for (Business business in initialBusinesses) { + for (JobListing job in business.listings!) { + List newBusinesses = groupedBusinesses[job.type!] ?? []; + Business newBusiness = Business.copy(business); + newBusiness.listings = + newBusiness.listings!.where((element) => element == job).toList(); + newBusinesses.add(newBusiness); + groupedBusinesses.addAll({job.type!: newBusinesses}); } - - groupedBusinesses - .addAll({JobType.values.byName(stringType): businesses}); } + return groupedBusinesses; + // Map> groupedBusinesses = {}; + // + // for (String stringType in decodedResponse.keys) { + // List businesses = []; + // + // for (Map map in decodedResponse[stringType]) { + // Business business = Business.fromJson(map); + // businesses.add(business); + // } + // + // groupedBusinesses + // .addAll({JobType.values.byName(stringType): businesses}); + // } + // return groupedBusinesses; } else { return 'Error ${response.statusCode}! Please try again later!'; } @@ -89,6 +108,8 @@ Future fetchBusinessDataOverviewJobs( return 'Unable to connect to server (timeout).\nPlease try again later.'; } on SocketException { return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n'; + } catch (e) { + print(e); } } diff --git a/fbla_ui/lib/shared/utils.dart b/fbla_ui/lib/shared/utils.dart index 39af8bf..6ad09ee 100644 --- a/fbla_ui/lib/shared/utils.dart +++ b/fbla_ui/lib/shared/utils.dart @@ -32,7 +32,17 @@ enum BusinessType { other, } -enum JobType { cashier, server, mechanic, other } +enum JobType { + retail, + customerService, + foodService, + finance, + healthcare, + education, + maintenance, + manufacturing, + other, +} enum OfferType { job, internship, apprenticeship } @@ -169,12 +179,22 @@ IconData getIconFromBusinessType(BusinessType type) { IconData getIconFromJobType(JobType type) { switch (type) { - case JobType.cashier: + case JobType.retail: return Icons.shopping_bag; - case JobType.server: + case JobType.customerService: + return Icons.support_agent; + case JobType.foodService: return Icons.restaurant; - case JobType.mechanic: - return Icons.construction; + case JobType.finance: + return Icons.paid; + case JobType.healthcare: + return Icons.medical_services; + case JobType.education: + return Icons.school; + case JobType.maintenance: + return Icons.handyman; + case JobType.manufacturing: + return Icons.factory; case JobType.other: return Icons.work; } @@ -199,12 +219,22 @@ pw.IconData getPwIconFromBusinessType(BusinessType type) { pw.IconData getPwIconFromJobType(JobType type) { switch (type) { - case JobType.cashier: + case JobType.retail: return const pw.IconData(0xf1cc); - case JobType.server: + case JobType.customerService: + return const pw.IconData(0xf0e2); + case JobType.foodService: return const pw.IconData(0xe56c); - case JobType.mechanic: - return const pw.IconData(0xea3c); + case JobType.finance: + return const pw.IconData(0xf041); + case JobType.healthcare: + return const pw.IconData(0xf109); + case JobType.education: + return const pw.IconData(0xe80c); + case JobType.maintenance: + return const pw.IconData(0xf10b); + case JobType.manufacturing: + return const pw.IconData(0xebbc); case JobType.other: return const pw.IconData(0xe8f9); } @@ -229,12 +259,22 @@ String getNameFromBusinessType(BusinessType type) { String getNameFromJobType(JobType type) { switch (type) { - case JobType.cashier: - return 'Cashier'; - case JobType.server: - return 'Server'; - case JobType.mechanic: - return 'Mechanic'; + case JobType.retail: + return 'Retail'; + case JobType.customerService: + return 'Customer Service'; + case JobType.foodService: + return 'Food Service'; + case JobType.finance: + return 'Finance'; + case JobType.healthcare: + return 'Healthcare'; + case JobType.education: + return 'Education'; + case JobType.maintenance: + return 'Maintenance'; + case JobType.manufacturing: + return 'Manufacturing'; case JobType.other: return 'Other'; }