More fixes
This commit is contained in:
parent
3cdf3b54ed
commit
e1f8c15e9a
41
README.md
41
README.md
@ -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.
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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!),
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user