More fixes

This commit is contained in:
Drake Marino 2024-06-25 18:27:53 -05:00
parent 3cdf3b54ed
commit e1f8c15e9a
10 changed files with 454 additions and 239 deletions

View File

@ -1,10 +1,10 @@
# Job Link # Job Link
This is my app `Job Link` for the 2023-2024 FBLA Coding and Programming event.\ This is my app `Job Link` for the 2023-2024 FBLA Coding and Programming event.\
WI SLC Winner. 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 ## 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) - [open_filex](https://pub.dev/packages/open_filex)
- [postgres](https://pub.dev/packages/postgres) - [postgres](https://pub.dev/packages/postgres)
- [argon2](https://pub.dev/packages/argon2) - [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 ## Requirements
### Job Link ### 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. - Stable internet connection.
### API ### API
- **OS**: Windows, Linux, MacOS. - **OS**: Windows, Linux, MacOS.
- Stable internet connection. - Stable internet connection.
## Installation/Usage ## 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 ## 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 ### WI State Leadership Conference
@ -44,13 +52,24 @@ Used release 0.1.1\
Questions asked (with my answers): Questions asked (with my answers):
**Q**: Why did you decide to use the apps (framework and tools I assume) you used?\ **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)\ This one seems to just be a clarification for the `User input is validated` category of
**Q**: You mentioned that you validated that the description [of the business] is required, do you validate all the inputs or just that?\ the [scoring sheet](https://connect.fbla.org/headquarters/files/High%20School%20Competitive%20Events%20Resources/Individual%20Guidelines/Presentation%20Events/Coding--Programming.pdf) (
**A**: I validate that all of the required fields are inputted, and I also check for a valid email address and website format. 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:\ While looking through a binder of the presentation and source code:\
**Q**: Is your source code all properly commented?\ **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.

View File

@ -22,7 +22,17 @@ enum BusinessType {
other, other,
} }
enum JobType { cashier, server, mechanic, other } enum JobType {
retail,
customerService,
foodService,
finance,
healthcare,
education,
maintenance,
manufacturing,
other,
}
enum OfferType { job, internship, apprenticeship } enum OfferType { job, internship, apprenticeship }
@ -177,53 +187,46 @@ void main() async {
OfferType.values.asNameMap().keys; OfferType.values.asNameMap().keys;
var postgresResult = (await postgres.query(''' var postgresResult = (await postgres.query('''
WITH business_listings AS ( SELECT jsonb_agg(
SELECT b.id AS business_id, jsonb_build_object(
b.name AS business_name, 'id', b.id,
b."contactName" AS business_contactName, 'name', b.name,
b."contactEmail" AS business_contactEmail, 'contactName', b."contactName",
b."contactPhone" AS business_contactPhone, 'contactEmail', b."contactEmail",
b."locationName" AS business_locationName, 'contactPhone', b."contactPhone",
b."locationAddress" AS business_locationAddress, 'locationName', b."locationName",
l.type AS listing_type, 'locationAddress', b."locationAddress",
json_agg( 'listings', b.listings
json_build_object( )
'id', l.id, ) AS result
'businessId', l."businessId", FROM (
'name', l.name, SELECT
'description', l.description, businesses.id,
'type', l.type, businesses.name,
'offerType', l."offerType", businesses."contactName",
'wage', l.wage, businesses."contactEmail",
'link', l.link businesses."contactPhone",
) businesses."locationName",
) AS listings businesses."locationAddress",
FROM businesses b jsonb_agg(
JOIN listings l ON b.id = l."businessId" jsonb_build_object(
WHERE l.type IN (${typeFilters.map((element) => "'$element'").join(',')}) 'id', listings.id,
AND l."offerType" IN (${offerFilters.map((element) => "'$element'").join(',')}) 'name', listings.name,
GROUP BY b.id, b.name, b."contactName", b."contactEmail", b."contactPhone", 'description', listings.description,
b."locationName", b."locationAddress", l.type 'type', listings.type,
), 'wage', listings.wage,
aggregated_businesses AS ( 'link', listings.link,
SELECT listing_type, 'offerType', listings."offerType"
json_agg( )
json_build_object( ) AS listings
'id', business_id, FROM businesses
'name', business_name, JOIN listings ON businesses.id = listings."businessId"
'contactName', business_contactName, AND listings.type IN (${typeFilters.map((element) => "'$element'").join(',')})
'contactEmail', business_contactEmail, AND listings."offerType" IN (${offerFilters.map((element) => "'$element'").join(',')})
'contactPhone', business_contactPhone, GROUP BY businesses.id, businesses.name, businesses."contactName", businesses."contactEmail",
'locationName', business_locationName, businesses."contactPhone", businesses."locationName", businesses."locationAddress"
'locationAddress', business_locationAddress, ) b
'listings', listings WHERE b.listings IS NOT NULL;
)
) AS businesses
FROM business_listings
GROUP BY listing_type
)
SELECT jsonb_object_agg(listing_type, businesses) AS result
FROM aggregated_businesses;
''')); '''));
return Response.ok( return Response.ok(

View File

@ -22,6 +22,7 @@ class BusinessDetail extends StatefulWidget {
class _CreateBusinessDetailState extends State<BusinessDetail> { class _CreateBusinessDetailState extends State<BusinessDetail> {
late Future loadBusiness; late Future loadBusiness;
bool _isRetrying = false;
@override @override
void initState() { void initState() {
@ -60,11 +61,17 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: FilledButton( child: FilledButton(
child: const Text('Retry'), child: const Text('Retry'),
onPressed: () { onPressed: () async {
var refreshedData = fetchBusiness(widget.id); if (!_isRetrying) {
setState(() { setState(() {
loadBusiness = refreshedData; _isRetrying = true;
}); });
var refreshedData =
await fetchBusiness(widget.id);
setState(() {
loadBusiness = refreshedData;
});
}
}, },
), ),
), ),
@ -120,28 +127,32 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
children: [ children: [
ListTile( Padding(
titleAlignment: ListTileTitleAlignment.titleHeight, padding: const EdgeInsets.only(right: 8.0),
title: Text(business.name!, child: ListTile(
style: const TextStyle( titleAlignment: ListTileTitleAlignment.titleHeight,
fontSize: 24, fontWeight: FontWeight.bold)), title: Text(business.name!,
subtitle: Text( style: const TextStyle(
business.description!, fontSize: 24, fontWeight: FontWeight.bold)),
), subtitle: Text(
contentPadding: business.description!,
const EdgeInsets.only(bottom: 8, left: 16), ),
leading: ClipRRect( contentPadding:
borderRadius: BorderRadius.circular(6.0), const EdgeInsets.only(bottom: 8, left: 16),
child: Image.network( leading: ClipRRect(
'$apiAddress/logos/${business.id}', borderRadius: BorderRadius.circular(6.0),
width: 48, child: Image.network(
height: 48, errorBuilder: (BuildContext context, '$apiAddress/logos/${business.id}',
Object exception, StackTrace? stackTrace) { width: 48,
return Icon( height: 48, errorBuilder:
getIconFromBusinessType( (BuildContext context, Object exception,
business.type ?? BusinessType.other), StackTrace? stackTrace) {
size: 48); return Icon(
}), getIconFromBusinessType(
business.type ?? BusinessType.other),
size: 48);
}),
),
), ),
), ),
if (business.website != null) if (business.website != null)

View File

@ -40,6 +40,7 @@ class _BusinessesOverviewState extends State<BusinessesOverview> {
ScrollController controller = ScrollController(); ScrollController controller = ScrollController();
bool _extended = true; bool _extended = true;
double prevPixelPosition = 0; double prevPixelPosition = 0;
bool _isRetrying = false;
Map<BusinessType, List<Business>> _filterBySearch( Map<BusinessType, List<Business>> _filterBySearch(
Map<BusinessType, List<Business>> businesses, String query) { Map<BusinessType, List<Business>> businesses, String query) {
@ -138,9 +139,14 @@ class _BusinessesOverviewState extends State<BusinessesOverview> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: FilledButton( child: FilledButton(
child: const Text('Retry'), child: const Text('Retry'),
onPressed: () { onPressed: () async {
widget.updateBusinessesCallback( if (!_isRetrying) {
businessTypeFilters); setState(() {
_isRetrying = true;
});
await widget.updateBusinessesCallback(
businessTypeFilters);
}
}, },
), ),
), ),

View File

@ -103,7 +103,17 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
) )
: const Icon(Icons.save), : const Icon(Icons.save),
onPressed: () async { 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, : null,
@ -114,28 +124,40 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
width: 800, width: 800,
child: Column( child: Column(
children: [ children: [
ListTile( Padding(
titleAlignment: ListTileTitleAlignment.titleHeight, padding: const EdgeInsets.only(top: 4.0),
title: Text(business.name!, child: Card(
style: const TextStyle( child: Padding(
fontSize: 24, fontWeight: FontWeight.bold)), padding: const EdgeInsets.only(right: 8.0),
subtitle: Text( child: ListTile(
business.description!, titleAlignment:
), ListTileTitleAlignment.titleHeight,
contentPadding: title: Text(business.name!,
const EdgeInsets.only(bottom: 8, left: 16), style: const TextStyle(
leading: ClipRRect( fontSize: 24,
borderRadius: BorderRadius.circular(6.0), fontWeight: FontWeight.bold)),
child: Image.network( subtitle: Text(
'https://logo.clearbit.com/${business.website}', business.description!,
width: 48, ),
height: 48, errorBuilder: (BuildContext context, contentPadding:
Object exception, StackTrace? stackTrace) { const EdgeInsets.only(bottom: 8, left: 16),
return Icon( leading: ClipRRect(
getIconFromBusinessType( borderRadius: BorderRadius.circular(6.0),
business.type ?? BusinessType.other), child: Image.network(
size: 48); '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( Card(
@ -540,7 +562,18 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
], ],
), ),
onPressed: () async { 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.'),
),
);
}
}, },
), ),
), ),

View File

@ -38,6 +38,8 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
link: null, link: null,
offerType: null); offerType: null);
bool _isLoading = false; bool _isLoading = false;
late String businessName;
bool _isRetrying = false;
@override @override
void initState() { void initState() {
@ -57,6 +59,7 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
.replaceAll('http://', '') .replaceAll('http://', '')
.replaceAll('www.', '')); .replaceAll('www.', ''));
getBusinessNameMapping = fetchBusinessNames(); getBusinessNameMapping = fetchBusinessNames();
businessName = widget.inputBusiness?.name ?? 'Offering business';
} }
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
@ -92,7 +95,17 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
) )
: const Icon(Icons.save), : const Icon(Icons.save),
onPressed: () async { 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, : null,
body: FutureBuilder( body: FutureBuilder(
@ -112,11 +125,17 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
child: FilledButton( child: FilledButton(
child: const Text('Retry'), child: const Text('Retry'),
onPressed: () async { onPressed: () async {
var refreshedData = fetchBusinessNames(); if (!_isRetrying) {
await refreshedData; setState(() {
setState(() { _isRetrying = true;
getBusinessNameMapping = refreshedData; });
}); var refreshedData = fetchBusinessNames();
await refreshedData;
setState(() {
getBusinessNameMapping = refreshedData;
_isRetrying = false;
});
}
}, },
), ),
), ),
@ -135,43 +154,55 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
width: 800, width: 800,
child: Column( child: Column(
children: [ children: [
ListTile( Padding(
titleAlignment: padding: const EdgeInsets.only(top: 4.0),
ListTileTitleAlignment.titleHeight, child: Card(
title: Text(listing.name, child: Padding(
style: const TextStyle( padding:
fontSize: 24, const EdgeInsets.only(right: 8.0),
fontWeight: FontWeight.bold)), child: ListTile(
subtitle: Column( titleAlignment: ListTileTitleAlignment
crossAxisAlignment: .titleHeight,
CrossAxisAlignment.start, title: Text(listing.name,
children: [ style: const TextStyle(
Text( fontSize: 24,
getNameFromJobType( fontWeight: FontWeight.bold)),
listing.type ?? JobType.other), subtitle: Column(
style: const TextStyle(fontSize: 18), 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 // Business Type Dropdown
@ -286,6 +317,11 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
setState(() { setState(() {
listing.businessId = listing.businessId =
inputType!; inputType!;
businessName = nameMapping
.where((element) =>
element['id'] ==
listing.businessId)
.first['name'];
businessDropdownErrorText = businessDropdownErrorText =
null; null;
}); });
@ -435,7 +471,7 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: FilledButton( child: FilledButton(
child: const Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( Padding(
@ -443,13 +479,39 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
top: 8.0, top: 8.0,
right: 8.0, right: 8.0,
bottom: 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'), Text('Save'),
], ],
), ),
onPressed: () async { 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.'),
),
);
}
}, },
), ),
), ),

View File

@ -1,4 +1,5 @@
import 'package:fbla_ui/main.dart'; 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/pages/create_edit_listing.dart';
import 'package:fbla_ui/shared/api_logic.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/global_vars.dart'; import 'package:fbla_ui/shared/global_vars.dart';
@ -44,53 +45,70 @@ class _CreateBusinessDetailState extends State<JobListingDetail> {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
children: [ children: [
ListTile( Padding(
minVerticalPadding: 0, padding: const EdgeInsets.only(right: 8.0),
titleAlignment: ListTileTitleAlignment.titleHeight, child: ListTile(
title: Padding( minVerticalPadding: 0,
padding: const EdgeInsets.only(top: 8.0), titleAlignment: ListTileTitleAlignment.titleHeight,
child: Text( title: Padding(
'${listing.name} (${getNameFromOfferType(listing.offerType!)})', padding: const EdgeInsets.only(top: 8.0),
style: const TextStyle( child: Text(
fontSize: 24, fontWeight: FontWeight.bold)), '${listing.name} (${getNameFromOfferType(listing.offerType!)})',
), style: const TextStyle(
subtitle: Column( fontSize: 24,
crossAxisAlignment: CrossAxisAlignment.start, fontWeight: FontWeight.bold)),
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),
), ),
largeSize: 26, subtitle: Column(
offset: const Offset(15, -5), crossAxisAlignment: CrossAxisAlignment.start,
textColor: Colors.white, children: [
backgroundColor: MouseRegion(
getColorFromOfferType(listing.offerType!), cursor: SystemMouseCursors.click,
child: ClipRRect( child: GestureDetector(
borderRadius: BorderRadius.circular(6.0), onTap: () {
child: Image.network( Navigator.push(context,
'$apiAddress/logos/${listing.businessId}', MaterialPageRoute(builder: (context) {
width: 48, return BusinessDetail(
height: 48, errorBuilder: id: widget.fromBusiness.id,
(BuildContext context, Object exception, name: widget.fromBusiness.name!);
StackTrace? stackTrace) { }));
return Icon( },
getIconFromJobType( child: Text(
listing.type ?? JobType.other), widget.fromBusiness.name!,
size: 48); 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);
}),
),
), ),
), ),
), ),

View File

@ -42,6 +42,7 @@ class _JobsOverviewState extends State<JobsOverview> {
ScrollController controller = ScrollController(); ScrollController controller = ScrollController();
bool _extended = true; bool _extended = true;
double prevPixelPosition = 0; double prevPixelPosition = 0;
bool _isRetrying = false;
Map<JobType, List<Business>> _filterBySearch( Map<JobType, List<Business>> _filterBySearch(
Map<JobType, List<Business>> businesses, String query) { Map<JobType, List<Business>> businesses, String query) {
@ -145,8 +146,13 @@ class _JobsOverviewState extends State<JobsOverview> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: FilledButton( child: FilledButton(
child: const Text('Retry'), child: const Text('Retry'),
onPressed: () { onPressed: () async {
widget.updateBusinessesCallback(null, null); if (!_isRetrying) {
setState(() {
_isRetrying = true;
});
await widget.updateBusinessesCallback(null, null);
}
}, },
), ),
), ),
@ -534,12 +540,8 @@ class _JobHeaderState extends State<_JobHeader> {
business.listings![0].offerType!), business.listings![0].offerType!),
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
), ),
largeSize: 26, largeSize: 24,
padding: business.listings![0].offerType! == offset: const Offset(12, -3),
OfferType.internship
? const EdgeInsets.symmetric(horizontal: 5)
: null,
offset: const Offset(13, -2),
textColor: Colors.white, textColor: Colors.white,
backgroundColor: getColorFromOfferType( backgroundColor: getColorFromOfferType(
business.listings![0].offerType!), business.listings![0].offerType!),

View File

@ -6,8 +6,8 @@ import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart'; import 'package:fbla_ui/shared/utils.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
var apiAddress = 'https://homelab.marinodev.com/fbla-api'; // var apiAddress = 'https://homelab.marinodev.com/fbla-api';
// var apiAddress = 'http://192.168.0.114:8000/fbla-api'; var apiAddress = 'http://192.168.0.114:8000/fbla-api';
var client = http.Client(); var client = http.Client();
@ -67,21 +67,40 @@ Future fetchBusinessDataOverviewJobs(
var response = await http.get(uri).timeout(const Duration(seconds: 20)); var response = await http.get(uri).timeout(const Duration(seconds: 20));
if (response.statusCode == 200) { if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body); List<Map<String, dynamic>> decodedResponse =
json.decode(response.body).cast<Map<String, dynamic>>();
List<Business> initialBusinesses =
decodedResponse.map((element) => Business.fromJson(element)).toList();
Map<JobType, List<Business>> groupedBusinesses = {}; Map<JobType, List<Business>> groupedBusinesses = {};
for (String stringType in decodedResponse.keys) { for (Business business in initialBusinesses) {
List<Business> businesses = []; for (JobListing job in business.listings!) {
List<Business> newBusinesses = groupedBusinesses[job.type!] ?? [];
for (Map<String, dynamic> map in decodedResponse[stringType]) { Business newBusiness = Business.copy(business);
Business business = Business.fromJson(map); newBusiness.listings =
businesses.add(business); newBusiness.listings!.where((element) => element == job).toList();
newBusinesses.add(newBusiness);
groupedBusinesses.addAll({job.type!: newBusinesses});
} }
groupedBusinesses
.addAll({JobType.values.byName(stringType): businesses});
} }
return groupedBusinesses; return groupedBusinesses;
// Map<JobType, List<Business>> groupedBusinesses = {};
//
// for (String stringType in decodedResponse.keys) {
// List<Business> businesses = [];
//
// for (Map<String, dynamic> map in decodedResponse[stringType]) {
// Business business = Business.fromJson(map);
// businesses.add(business);
// }
//
// groupedBusinesses
// .addAll({JobType.values.byName(stringType): businesses});
// }
// return groupedBusinesses;
} else { } else {
return 'Error ${response.statusCode}! Please try again later!'; 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.'; return 'Unable to connect to server (timeout).\nPlease try again later.';
} on SocketException { } on SocketException {
return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n'; return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n';
} catch (e) {
print(e);
} }
} }

View File

@ -32,7 +32,17 @@ enum BusinessType {
other, other,
} }
enum JobType { cashier, server, mechanic, other } enum JobType {
retail,
customerService,
foodService,
finance,
healthcare,
education,
maintenance,
manufacturing,
other,
}
enum OfferType { job, internship, apprenticeship } enum OfferType { job, internship, apprenticeship }
@ -169,12 +179,22 @@ IconData getIconFromBusinessType(BusinessType type) {
IconData getIconFromJobType(JobType type) { IconData getIconFromJobType(JobType type) {
switch (type) { switch (type) {
case JobType.cashier: case JobType.retail:
return Icons.shopping_bag; return Icons.shopping_bag;
case JobType.server: case JobType.customerService:
return Icons.support_agent;
case JobType.foodService:
return Icons.restaurant; return Icons.restaurant;
case JobType.mechanic: case JobType.finance:
return Icons.construction; 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: case JobType.other:
return Icons.work; return Icons.work;
} }
@ -199,12 +219,22 @@ pw.IconData getPwIconFromBusinessType(BusinessType type) {
pw.IconData getPwIconFromJobType(JobType type) { pw.IconData getPwIconFromJobType(JobType type) {
switch (type) { switch (type) {
case JobType.cashier: case JobType.retail:
return const pw.IconData(0xf1cc); return const pw.IconData(0xf1cc);
case JobType.server: case JobType.customerService:
return const pw.IconData(0xf0e2);
case JobType.foodService:
return const pw.IconData(0xe56c); return const pw.IconData(0xe56c);
case JobType.mechanic: case JobType.finance:
return const pw.IconData(0xea3c); 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: case JobType.other:
return const pw.IconData(0xe8f9); return const pw.IconData(0xe8f9);
} }
@ -229,12 +259,22 @@ String getNameFromBusinessType(BusinessType type) {
String getNameFromJobType(JobType type) { String getNameFromJobType(JobType type) {
switch (type) { switch (type) {
case JobType.cashier: case JobType.retail:
return 'Cashier'; return 'Retail';
case JobType.server: case JobType.customerService:
return 'Server'; return 'Customer Service';
case JobType.mechanic: case JobType.foodService:
return 'Mechanic'; 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: case JobType.other:
return 'Other'; return 'Other';
} }