more fixes and features

This commit is contained in:
Drake Marino 2024-06-23 17:02:19 -05:00
parent 03abc1191d
commit b860ae52f6
7 changed files with 205 additions and 152 deletions

View File

@ -176,48 +176,58 @@ void main() async {
request.url.queryParameters['offerFilters']?.split(',') ?? request.url.queryParameters['offerFilters']?.split(',') ??
OfferType.values.asNameMap().keys; OfferType.values.asNameMap().keys;
Map<String, dynamic> output = {}; var postgresResult = (await postgres.query('''
WITH business_listings AS (
for (int i = 0; i < typeFilters.length; i++) { SELECT b.id AS business_id,
var postgresResult = (await postgres.query(''' b.name AS business_name,
SELECT json_agg( b."contactName" AS business_contactName,
json_build_object( b."contactEmail" AS business_contactEmail,
'id', b.id, b."contactPhone" AS business_contactPhone,
'name', b.name, b."locationName" AS business_locationName,
'contactName', b."contactName", b."locationAddress" AS business_locationAddress,
'contactEmail', b."contactEmail", l.type AS listing_type,
'contactPhone', b."contactPhone", json_agg(
'locationName', b."locationName", json_build_object(
'locationAddress', b."locationAddress", 'id', l.id,
'listings', ( 'businessId', l."businessId",
SELECT json_agg( 'name', l.name,
json_build_object( 'description', l.description,
'id', l.id, 'type', l.type,
'name', l.name, 'offerType', l."offerType",
'description', l.description, 'wage', l.wage,
'type', l.type, 'link', l.link
'offerType', l."offerType",
'wage', l.wage,
'link', l.link
)
) )
FROM listings l ) AS listings
WHERE l."businessId" = b.id AND l.type = '${typeFilters.elementAt(i)}' AND l."offerType" IN (${offerFilters.map((element) => "'$element'").join(',')}) 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 businesses b FROM aggregated_businesses;
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;
''')); '''));
if (postgresResult.isNotEmpty) {
output.addAll({typeFilters.elementAt(i): postgresResult[0][0]});
}
}
return Response.ok( return Response.ok(
json.encode(output), json.encode(postgresResult[0][0]),
headers: { headers: {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
@ -311,9 +321,9 @@ void main() async {
'name', l.name, 'name', l.name,
'description', l.description, 'description', l.description,
'type', l.type, 'type', l.type,
'offerType', l."offerType",
'wage', l.wage, 'wage', l.wage,
'link', l.link, 'link', l.link
'offerType', l."offerType"
) )
) )
END END

View File

@ -225,25 +225,22 @@ class _BusinessesOverviewState extends State<BusinessesOverview> {
}); });
} }
List<Padding> chips = []; List<Widget> chips = [];
for (var type in BusinessType.values) { for (var type in BusinessType.values) {
chips.add(Padding( chips.add(FilterChip(
padding: const EdgeInsets.all(4), showCheckmark: false,
child: FilterChip( shape: RoundedRectangleBorder(
showCheckmark: false, borderRadius: BorderRadius.circular(20)),
shape: RoundedRectangleBorder( label: Text(getNameFromBusinessType(type)),
borderRadius: BorderRadius.circular(20)), selected: selectedChips.contains(type),
label: Text(getNameFromBusinessType(type)), onSelected: (bool selected) {
selected: selectedChips.contains(type), if (selected) {
onSelected: (bool selected) { selectedChips.add(type);
if (selected) { } else {
selectedChips.add(type); selectedChips.remove(type);
} else { }
selectedChips.remove(type); setDialogState(filters);
} }));
setDialogState(filters);
}),
));
} }
return AlertDialog( return AlertDialog(
@ -251,6 +248,8 @@ class _BusinessesOverviewState extends State<BusinessesOverview> {
content: SizedBox( content: SizedBox(
width: 400, width: 400,
child: Wrap( child: Wrap(
spacing: 6,
runSpacing: 6,
children: chips, children: chips,
), ),
), ),

View File

@ -141,8 +141,18 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
subtitle: Text( subtitle: Column(
listing.description, crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
getNameFromJobType(listing.type!),
style: const TextStyle(fontSize: 18),
),
Text(
listing.description,
),
],
), ),
contentPadding: const EdgeInsets.only( contentPadding: const EdgeInsets.only(
bottom: 8, left: 16), bottom: 8, left: 16),

View File

@ -45,27 +45,53 @@ class _CreateBusinessDetailState extends State<JobListingDetail> {
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(
minVerticalPadding: 0,
titleAlignment: ListTileTitleAlignment.titleHeight, titleAlignment: ListTileTitleAlignment.titleHeight,
title: Text(listing.name, title: Padding(
style: const TextStyle( padding: const EdgeInsets.only(top: 8.0),
fontSize: 24, fontWeight: FontWeight.bold)), child: Text(
subtitle: Text( '${listing.name} (${getNameFromOfferType(listing.offerType!)})',
listing.description, 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: contentPadding:
const EdgeInsets.only(bottom: 8, left: 16), const EdgeInsets.only(bottom: 8, left: 16),
leading: ClipRRect( leading: Badge(
borderRadius: BorderRadius.circular(6.0), label: Text(
child: Image.network( getLetterFromOfferType(listing.offerType!),
'$apiAddress/logos/${listing.businessId}', style: const TextStyle(fontSize: 16),
width: 48, ),
height: 48, errorBuilder: (BuildContext context, largeSize: 26,
Object exception, StackTrace? stackTrace) { offset: const Offset(15, -5),
return Icon( textColor: Colors.white,
getIconFromJobType( backgroundColor:
listing.type ?? JobType.other), getColorFromOfferType(listing.offerType!),
size: 48); 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 != '') if (listing.link != null && listing.link != '')
@ -87,16 +113,14 @@ class _CreateBusinessDetailState extends State<JobListingDetail> {
), ),
), ),
// Wage // Wage
Visibility( if (listing.wage != null && listing.wage != '')
visible: listing.wage != null && listing.wage != '', Card(
child: Card(
child: ListTile( child: ListTile(
leading: const Icon(Icons.attach_money), leading: const Icon(Icons.attach_money),
subtitle: Text(listing.wage!), subtitle: Text(listing.wage!),
title: const Text('Wage Information'), title: const Text('Wage Information'),
), ),
), ),
),
Card( Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(

View File

@ -242,46 +242,40 @@ class _JobsOverviewState extends State<JobsOverview> {
} }
} }
List<Padding> jobTypeChips = []; List<Widget> jobTypeChips = [];
for (JobType type in JobType.values) { for (JobType type in JobType.values) {
jobTypeChips.add(Padding( jobTypeChips.add(FilterChip(
padding: const EdgeInsets.all(4), showCheckmark: false,
child: FilterChip( shape: RoundedRectangleBorder(
showCheckmark: false, borderRadius: BorderRadius.circular(20)),
shape: RoundedRectangleBorder( label: Text(getNameFromJobType(type)),
borderRadius: BorderRadius.circular(20)), selected: selectedJobTypeChips.contains(type),
label: Text(getNameFromJobType(type)), onSelected: (bool selected) {
selected: selectedJobTypeChips.contains(type), if (selected) {
onSelected: (bool selected) { selectedJobTypeChips.add(type);
if (selected) { } else {
selectedJobTypeChips.add(type); selectedJobTypeChips.remove(type);
} else { }
selectedJobTypeChips.remove(type); setDialogState(selectedJobTypeChips, null);
} }));
setDialogState(selectedJobTypeChips, null);
}),
));
} }
List<Padding> offerTypeChips = []; List<Widget> offerTypeChips = [];
for (OfferType type in OfferType.values) { for (OfferType type in OfferType.values) {
offerTypeChips.add(Padding( offerTypeChips.add(FilterChip(
padding: const EdgeInsets.all(4), showCheckmark: false,
child: FilterChip( shape: RoundedRectangleBorder(
showCheckmark: false, borderRadius: BorderRadius.circular(20)),
shape: RoundedRectangleBorder( label: Text(getNameFromOfferType(type)),
borderRadius: BorderRadius.circular(20)), selected: selectedOfferTypeChips.contains(type),
label: Text(getNameFromOfferType(type)), onSelected: (bool selected) {
selected: selectedOfferTypeChips.contains(type), if (selected) {
onSelected: (bool selected) { selectedOfferTypeChips.add(type);
if (selected) { } else {
selectedOfferTypeChips.add(type); selectedOfferTypeChips.remove(type);
} else { }
selectedOfferTypeChips.remove(type); setDialogState(null, selectedOfferTypeChips);
} }));
setDialogState(null, selectedOfferTypeChips);
}),
));
} }
return AlertDialog( return AlertDialog(
@ -295,6 +289,8 @@ class _JobsOverviewState extends State<JobsOverview> {
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Wrap( child: Wrap(
spacing: 6,
runSpacing: 6,
children: jobTypeChips, children: jobTypeChips,
), ),
), ),
@ -302,6 +298,8 @@ class _JobsOverviewState extends State<JobsOverview> {
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Wrap( child: Wrap(
spacing: 6,
runSpacing: 6,
children: offerTypeChips, children: offerTypeChips,
), ),
), ),
@ -515,8 +513,9 @@ class _JobHeaderState extends State<_JobHeader> {
), ),
largeSize: 26, largeSize: 26,
offset: const Offset(15, -5), offset: const Offset(15, -5),
textColor: Theme.of(context).colorScheme.onPrimary, textColor: Colors.white,
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: getColorFromOfferType(
business.listings![0].offerType!),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(6.0), borderRadius: BorderRadius.circular(6.0),
child: Image.network( child: Image.network(

View File

@ -87,31 +87,28 @@ class _FilterBusinessDataTypeChipsState
extends State<_FilterBusinessDataTypeChips> { extends State<_FilterBusinessDataTypeChips> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Padding> chips = []; List<Widget> chips = [];
for (var type in DataTypeBusiness.values) { for (var type in DataTypeBusiness.values) {
chips.add(Padding( chips.add(FilterChip(
padding: shape: RoundedRectangleBorder(
const EdgeInsets.only(left: 3.0, right: 3.0, bottom: 3.0, top: 3.0), borderRadius: BorderRadius.circular(20),
child: FilterChip( side: BorderSide(color: Theme.of(context).colorScheme.secondary)),
shape: RoundedRectangleBorder( label: Text(dataTypeFriendlyBusiness[type]!),
borderRadius: BorderRadius.circular(20), showCheckmark: false,
side: selected: widget.selectedDataTypesBusiness.contains(type),
BorderSide(color: Theme.of(context).colorScheme.secondary)), onSelected: (bool selected) {
label: Text(dataTypeFriendlyBusiness[type]!), setState(() {
showCheckmark: false, if (selected) {
selected: widget.selectedDataTypesBusiness.contains(type), widget.selectedDataTypesBusiness.add(type);
onSelected: (bool selected) { } else {
setState(() { widget.selectedDataTypesBusiness.remove(type);
if (selected) { }
widget.selectedDataTypesBusiness.add(type); });
} else { }));
widget.selectedDataTypesBusiness.remove(type);
}
});
}),
));
} }
return Wrap( return Wrap(
spacing: 6,
runSpacing: 6,
children: chips, children: chips,
); );
} }
@ -155,6 +152,8 @@ class _FilterJobDataTypeChipsState extends State<_FilterJobDataTypeChips> {
)); ));
} }
return Wrap( return Wrap(
spacing: 6,
runSpacing: 6,
children: chips, children: chips,
); );
} }

View File

@ -5,12 +5,12 @@ enum DataTypeBusiness {
logo, logo,
name, name,
description, description,
type,
website, website,
contactName, contactName,
contactEmail, contactEmail,
contactPhone, contactPhone,
notes, notes,
type,
} }
enum DataTypeJob { enum DataTypeJob {
@ -42,19 +42,20 @@ class JobListing {
String name; String name;
String description; String description;
JobType? type; JobType? type;
OfferType? offerType;
String? wage; String? wage;
String? link; String? link;
OfferType? offerType;
JobListing( JobListing({
{this.id, this.id,
this.businessId, this.businessId,
required this.name, required this.name,
required this.description, required this.description,
this.type, this.type,
this.wage, this.offerType,
this.link, this.wage,
this.offerType}); this.link,
});
factory JobListing.copy(JobListing input) { factory JobListing.copy(JobListing input) {
return JobListing( return JobListing(
@ -63,9 +64,9 @@ class JobListing {
name: input.name, name: input.name,
description: input.description, description: input.description,
type: input.type, type: input.type,
offerType: input.offerType,
wage: input.wage, wage: input.wage,
link: input.link, 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) { IconData getIconFromThemeMode(ThemeMode theme) {
switch (theme) { switch (theme) {
case ThemeMode.dark: case ThemeMode.dark: