From b860ae52f6e5fbff20800515cb45147e446e1246 Mon Sep 17 00:00:00 2001 From: drake Date: Sun, 23 Jun 2024 17:02:19 -0500 Subject: [PATCH] more fixes and features --- fbla-api/lib/fbla_api.dart | 88 ++++++++++++---------- fbla_ui/lib/pages/businesses_overview.dart | 35 +++++---- fbla_ui/lib/pages/create_edit_listing.dart | 14 +++- fbla_ui/lib/pages/listing_detail.dart | 66 ++++++++++------ fbla_ui/lib/pages/listings_overview.dart | 75 +++++++++--------- fbla_ui/lib/shared/export.dart | 43 ++++++----- fbla_ui/lib/shared/utils.dart | 36 ++++++--- 7 files changed, 205 insertions(+), 152 deletions(-) diff --git a/fbla-api/lib/fbla_api.dart b/fbla-api/lib/fbla_api.dart index bc71659..083ae73 100644 --- a/fbla-api/lib/fbla_api.dart +++ b/fbla-api/lib/fbla_api.dart @@ -176,48 +176,58 @@ void main() async { request.url.queryParameters['offerFilters']?.split(',') ?? OfferType.values.asNameMap().keys; - Map output = {}; - - for (int i = 0; i < typeFilters.length; i++) { - var postgresResult = (await postgres.query(''' - SELECT json_agg( - json_build_object( - 'id', b.id, - 'name', b.name, - 'contactName', b."contactName", - 'contactEmail', b."contactEmail", - 'contactPhone', b."contactPhone", - 'locationName', b."locationName", - 'locationAddress', b."locationAddress", - 'listings', ( - SELECT json_agg( - json_build_object( - 'id', l.id, - 'name', l.name, - 'description', l.description, - 'type', l.type, - 'offerType', l."offerType", - 'wage', l.wage, - 'link', l.link - ) + 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 ) - FROM listings l - WHERE l."businessId" = b.id AND l.type = '${typeFilters.elementAt(i)}' AND l."offerType" IN (${offerFilters.map((element) => "'$element'").join(',')}) - ) + ) 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 ) - ) - FROM businesses b - WHERE b.id IN (SELECT "businessId" FROM public.listings WHERE type='${typeFilters.elementAt(i)}' AND "offerType" IN (${offerFilters.map((element) => "'$element'").join(',')})) - GROUP BY b.id; + SELECT jsonb_object_agg(listing_type, businesses) AS result + FROM aggregated_businesses; ''')); - if (postgresResult.isNotEmpty) { - output.addAll({typeFilters.elementAt(i): postgresResult[0][0]}); - } - } - return Response.ok( - json.encode(output), + json.encode(postgresResult[0][0]), headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain' @@ -311,9 +321,9 @@ void main() async { 'name', l.name, 'description', l.description, 'type', l.type, + 'offerType', l."offerType", 'wage', l.wage, - 'link', l.link, - 'offerType', l."offerType" + 'link', l.link ) ) END diff --git a/fbla_ui/lib/pages/businesses_overview.dart b/fbla_ui/lib/pages/businesses_overview.dart index 7560d12..0c93094 100644 --- a/fbla_ui/lib/pages/businesses_overview.dart +++ b/fbla_ui/lib/pages/businesses_overview.dart @@ -225,25 +225,22 @@ class _BusinessesOverviewState extends State { }); } - List chips = []; + List chips = []; for (var type in BusinessType.values) { - chips.add(Padding( - padding: const EdgeInsets.all(4), - child: FilterChip( - showCheckmark: false, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)), - label: Text(getNameFromBusinessType(type)), - selected: selectedChips.contains(type), - onSelected: (bool selected) { - if (selected) { - selectedChips.add(type); - } else { - selectedChips.remove(type); - } - setDialogState(filters); - }), - )); + chips.add(FilterChip( + showCheckmark: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + label: Text(getNameFromBusinessType(type)), + selected: selectedChips.contains(type), + onSelected: (bool selected) { + if (selected) { + selectedChips.add(type); + } else { + selectedChips.remove(type); + } + setDialogState(filters); + })); } return AlertDialog( @@ -251,6 +248,8 @@ class _BusinessesOverviewState extends State { content: SizedBox( width: 400, child: Wrap( + spacing: 6, + runSpacing: 6, children: chips, ), ), diff --git a/fbla_ui/lib/pages/create_edit_listing.dart b/fbla_ui/lib/pages/create_edit_listing.dart index 1ff25e6..164649c 100644 --- a/fbla_ui/lib/pages/create_edit_listing.dart +++ b/fbla_ui/lib/pages/create_edit_listing.dart @@ -141,8 +141,18 @@ class _CreateEditJobListingState extends State { style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold)), - subtitle: Text( - listing.description, + 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), diff --git a/fbla_ui/lib/pages/listing_detail.dart b/fbla_ui/lib/pages/listing_detail.dart index 108383e..0b313a1 100644 --- a/fbla_ui/lib/pages/listing_detail.dart +++ b/fbla_ui/lib/pages/listing_detail.dart @@ -45,27 +45,53 @@ class _CreateBusinessDetailState extends State { child: Column( children: [ ListTile( + minVerticalPadding: 0, titleAlignment: ListTileTitleAlignment.titleHeight, - title: Text(listing.name, - style: const TextStyle( - fontSize: 24, fontWeight: FontWeight.bold)), - subtitle: Text( - listing.description, + 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: 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); - }), + leading: Badge( + label: Text( + getLetterFromOfferType(listing.offerType!), + style: const TextStyle(fontSize: 16), + ), + 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); + }), + ), ), ), if (listing.link != null && listing.link != '') @@ -87,16 +113,14 @@ class _CreateBusinessDetailState extends State { ), ), // Wage - Visibility( - visible: listing.wage != null && listing.wage != '', - child: Card( + if (listing.wage != null && listing.wage != '') + Card( child: ListTile( leading: const Icon(Icons.attach_money), subtitle: Text(listing.wage!), title: const Text('Wage Information'), ), ), - ), Card( clipBehavior: Clip.antiAlias, child: Column( diff --git a/fbla_ui/lib/pages/listings_overview.dart b/fbla_ui/lib/pages/listings_overview.dart index 7683474..097db9d 100644 --- a/fbla_ui/lib/pages/listings_overview.dart +++ b/fbla_ui/lib/pages/listings_overview.dart @@ -242,46 +242,40 @@ class _JobsOverviewState extends State { } } - List jobTypeChips = []; + List jobTypeChips = []; for (JobType type in JobType.values) { - jobTypeChips.add(Padding( - padding: const EdgeInsets.all(4), - child: FilterChip( - showCheckmark: false, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)), - label: Text(getNameFromJobType(type)), - selected: selectedJobTypeChips.contains(type), - onSelected: (bool selected) { - if (selected) { - selectedJobTypeChips.add(type); - } else { - selectedJobTypeChips.remove(type); - } - setDialogState(selectedJobTypeChips, null); - }), - )); + jobTypeChips.add(FilterChip( + showCheckmark: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + label: Text(getNameFromJobType(type)), + selected: selectedJobTypeChips.contains(type), + onSelected: (bool selected) { + if (selected) { + selectedJobTypeChips.add(type); + } else { + selectedJobTypeChips.remove(type); + } + setDialogState(selectedJobTypeChips, null); + })); } - List offerTypeChips = []; + List offerTypeChips = []; for (OfferType type in OfferType.values) { - offerTypeChips.add(Padding( - padding: const EdgeInsets.all(4), - child: FilterChip( - showCheckmark: false, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)), - label: Text(getNameFromOfferType(type)), - selected: selectedOfferTypeChips.contains(type), - onSelected: (bool selected) { - if (selected) { - selectedOfferTypeChips.add(type); - } else { - selectedOfferTypeChips.remove(type); - } - setDialogState(null, selectedOfferTypeChips); - }), - )); + offerTypeChips.add(FilterChip( + showCheckmark: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + label: Text(getNameFromOfferType(type)), + selected: selectedOfferTypeChips.contains(type), + onSelected: (bool selected) { + if (selected) { + selectedOfferTypeChips.add(type); + } else { + selectedOfferTypeChips.remove(type); + } + setDialogState(null, selectedOfferTypeChips); + })); } return AlertDialog( @@ -295,6 +289,8 @@ class _JobsOverviewState extends State { Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Wrap( + spacing: 6, + runSpacing: 6, children: jobTypeChips, ), ), @@ -302,6 +298,8 @@ class _JobsOverviewState extends State { Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Wrap( + spacing: 6, + runSpacing: 6, children: offerTypeChips, ), ), @@ -515,8 +513,9 @@ class _JobHeaderState extends State<_JobHeader> { ), largeSize: 26, offset: const Offset(15, -5), - textColor: Theme.of(context).colorScheme.onPrimary, - backgroundColor: Theme.of(context).colorScheme.primary, + textColor: Colors.white, + backgroundColor: getColorFromOfferType( + business.listings![0].offerType!), child: ClipRRect( borderRadius: BorderRadius.circular(6.0), child: Image.network( diff --git a/fbla_ui/lib/shared/export.dart b/fbla_ui/lib/shared/export.dart index 716f017..38f4a92 100644 --- a/fbla_ui/lib/shared/export.dart +++ b/fbla_ui/lib/shared/export.dart @@ -87,31 +87,28 @@ class _FilterBusinessDataTypeChipsState extends State<_FilterBusinessDataTypeChips> { @override Widget build(BuildContext context) { - List chips = []; + List chips = []; for (var type in DataTypeBusiness.values) { - chips.add(Padding( - padding: - const EdgeInsets.only(left: 3.0, right: 3.0, bottom: 3.0, top: 3.0), - child: FilterChip( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: - BorderSide(color: Theme.of(context).colorScheme.secondary)), - label: Text(dataTypeFriendlyBusiness[type]!), - showCheckmark: false, - selected: widget.selectedDataTypesBusiness.contains(type), - onSelected: (bool selected) { - setState(() { - if (selected) { - widget.selectedDataTypesBusiness.add(type); - } else { - widget.selectedDataTypesBusiness.remove(type); - } - }); - }), - )); + chips.add(FilterChip( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Theme.of(context).colorScheme.secondary)), + label: Text(dataTypeFriendlyBusiness[type]!), + showCheckmark: false, + selected: widget.selectedDataTypesBusiness.contains(type), + onSelected: (bool selected) { + setState(() { + if (selected) { + widget.selectedDataTypesBusiness.add(type); + } else { + widget.selectedDataTypesBusiness.remove(type); + } + }); + })); } return Wrap( + spacing: 6, + runSpacing: 6, children: chips, ); } @@ -155,6 +152,8 @@ class _FilterJobDataTypeChipsState extends State<_FilterJobDataTypeChips> { )); } return Wrap( + spacing: 6, + runSpacing: 6, children: chips, ); } diff --git a/fbla_ui/lib/shared/utils.dart b/fbla_ui/lib/shared/utils.dart index 320bbdc..39af8bf 100644 --- a/fbla_ui/lib/shared/utils.dart +++ b/fbla_ui/lib/shared/utils.dart @@ -5,12 +5,12 @@ enum DataTypeBusiness { logo, name, description, + type, website, contactName, contactEmail, contactPhone, notes, - type, } enum DataTypeJob { @@ -42,19 +42,20 @@ class JobListing { String name; String description; JobType? type; + OfferType? offerType; String? wage; String? link; - OfferType? offerType; - JobListing( - {this.id, - this.businessId, - required this.name, - required this.description, - this.type, - this.wage, - this.link, - this.offerType}); + JobListing({ + this.id, + this.businessId, + required this.name, + required this.description, + this.type, + this.offerType, + this.wage, + this.link, + }); factory JobListing.copy(JobListing input) { return JobListing( @@ -63,9 +64,9 @@ class JobListing { name: input.name, description: input.description, type: input.type, + offerType: input.offerType, wage: input.wage, link: input.link, - offerType: input.offerType, ); } } @@ -261,6 +262,17 @@ String getLetterFromOfferType(OfferType type) { } } +Color getColorFromOfferType(OfferType type) { + switch (type) { + case OfferType.job: + return Colors.blue; + case OfferType.internship: + return Colors.green.shade800; + case OfferType.apprenticeship: + return Colors.red; + } +} + IconData getIconFromThemeMode(ThemeMode theme) { switch (theme) { case ThemeMode.dark: