v0.2.0 beta - Major screen changes

This commit is contained in:
Drake Marino 2024-06-20 13:18:54 -05:00
parent 95b2e0bf11
commit d72ee93f29
13 changed files with 2105 additions and 684 deletions

View File

@ -28,6 +28,7 @@ class Business {
int id; int id;
String name; String name;
String description; String description;
BusinessType? type;
String? website; String? website;
String? contactName; String? contactName;
String? contactEmail; String? contactEmail;
@ -40,6 +41,7 @@ class Business {
{required this.id, {required this.id,
required this.name, required this.name,
required this.description, required this.description,
this.type,
this.website, this.website,
this.contactName, this.contactName,
this.contactEmail, this.contactEmail,
@ -49,11 +51,21 @@ class Business {
this.locationAddress}); this.locationAddress});
factory Business.fromJson(Map<String, dynamic> json) { factory Business.fromJson(Map<String, dynamic> json) {
bool typeValid = true;
try {
BusinessType.values.byName(json['type']);
} catch (e) {
typeValid = false;
}
return Business( return Business(
id: json['id'], id: json['id'],
name: json['name'], name: json['name'],
description: json['description'], description: json['description'],
website: json['website'], website: json['website'],
type: typeValid
? BusinessType.values.byName(json['type'])
: BusinessType.other,
contactName: json['contactName'], contactName: json['contactName'],
contactEmail: json['contactEmail'], contactEmail: json['contactEmail'],
contactPhone: json['contactPhone'], contactPhone: json['contactPhone'],
@ -151,12 +163,64 @@ void main() async {
headers: {'Access-Control-Allow-Origin': '*'}, headers: {'Access-Control-Allow-Origin': '*'},
); );
}); });
app.get('/fbla-api/businessdata/overview', (Request request) async { app.get('/fbla-api/businessdata/overview/jobs', (Request request) async {
print('business overview request received'); print('business overview request received');
var filters = request.url.queryParameters['filters']?.split(',') ?? var filters = request.url.queryParameters['filters']?.split(',') ??
JobType.values.asNameMap().keys; JobType.values.asNameMap().keys;
Map<String, dynamic> output = {};
for (int i = 0; i < filters.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",
'listings', (
SELECT json_agg(
json_build_object(
'id', l.id,
'name', l.name,
'description', l.description,
'type', l.type,
'wage', l.wage,
'link', l.link
)
)
FROM listings l
WHERE l."businessId" = b.id AND l.type = '${filters.elementAt(i)}'
)
)
)
FROM businesses b
WHERE b.id IN (SELECT "businessId" FROM public.listings WHERE type='${filters.elementAt(i)}')
GROUP BY b.id;
'''));
if (postgresResult.isNotEmpty) {
output.addAll({filters.elementAt(i): postgresResult[0][0]});
}
}
return Response.ok(
json.encode(output),
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain'
},
);
});
app.get('/fbla-api/businessdata/overview/types', (Request request) async {
print('business overview request received');
var filters = request.url.queryParameters['filters']?.split(',') ??
BusinessType.values.asNameMap().keys;
// List<Map<String, List<Map<String, dynamic>>>> this is the real type lol // List<Map<String, List<Map<String, dynamic>>>> this is the real type lol
Map<String, dynamic> output = {}; Map<String, dynamic> output = {};
@ -172,7 +236,7 @@ void main() async {
'contactPhone', "contactPhone", 'contactPhone', "contactPhone",
'locationName', "locationName" 'locationName', "locationName"
) )
) FROM public.businesses WHERE id IN (SELECT "businessId" FROM public.listings WHERE type='${filters.elementAt(i)}') ) FROM public.businesses WHERE type='${filters.elementAt(i)}'
'''))[0][0]; '''))[0][0];
if (postgresResult != null) { if (postgresResult != null) {
@ -180,6 +244,7 @@ void main() async {
} }
} }
// await Future.delayed(Duration(seconds: 5));
return Response.ok( return Response.ok(
json.encode(output), json.encode(output),
headers: { headers: {
@ -218,6 +283,7 @@ void main() async {
'id', b.id, 'id', b.id,
'name', b.name, 'name', b.name,
'description', b.description, 'description', b.description,
'type', b.type,
'website', b.website, 'website', b.website,
'contactName', b."contactName", 'contactName', b."contactName",
'contactEmail', b."contactEmail", 'contactEmail', b."contactEmail",
@ -226,7 +292,9 @@ void main() async {
'locationName', b."locationName", 'locationName', b."locationName",
'locationAddress', b."locationAddress", 'locationAddress', b."locationAddress",
'listings', 'listings',
json_agg( CASE
WHEN COUNT(l.id) = 0 THEN 'null'
ELSE json_agg(
json_build_object( json_build_object(
'id', l.id, 'id', l.id,
'businessId', l."businessId", 'businessId', l."businessId",
@ -237,6 +305,7 @@ void main() async {
'link', l.link 'link', l.link
) )
) )
END
) )
FROM businesses b FROM businesses b
LEFT JOIN listings l ON b.id = l."businessId" LEFT JOIN listings l ON b.id = l."businessId"
@ -273,14 +342,16 @@ void main() async {
'name', b.name, 'name', b.name,
'description', b.description, 'description', b.description,
'website', b.website, 'website', b.website,
'type', b.type,
'contactName', b."contactName", 'contactName', b."contactName",
'contactEmail', b."contactEmail", 'contactEmail', b."contactEmail",
'contactPhone', b."contactPhone", 'contactPhone', b."contactPhone",
'notes', b.notes, 'notes', b.notes,
'locationName', b."locationName", 'locationName', b."locationName",
'locationAddress', b."locationAddress", 'locationAddress', b."locationAddress",
'listings', 'listings', CASE
json_agg( WHEN COUNT(l.id) = 0 THEN 'null'
ELSE json_agg(
json_build_object( json_build_object(
'id', l.id, 'id', l.id,
'businessId', l."businessId", 'businessId', l."businessId",
@ -291,6 +362,7 @@ void main() async {
'link', l.link 'link', l.link
) )
) )
END
) )
FROM businesses b FROM businesses b
LEFT JOIN listings l ON b.id = l."businessId" LEFT JOIN listings l ON b.id = l."businessId"
@ -362,9 +434,10 @@ void main() async {
Business business = Business.fromJson(json); Business business = Business.fromJson(json);
await postgres.query(''' await postgres.query('''
INSERT INTO businesses (name, description, website, "contactName", "contactPhone", "contactEmail", notes, "locationName", "locationAddress") INSERT INTO businesses (name, description, website, type, "contactName", "contactPhone", "contactEmail", notes, "locationName", "locationAddress")
VALUES ('${business.name.replaceAll("'", "''")}', '${business.description.replaceAll("'", "''")}', '${business.website ?? 'NULL'}', '${business.contactName?.replaceAll("'", "''") ?? 'NULL'}', '${business.contactPhone ?? 'NULL'}', '${business.contactEmail ?? 'NULL'}', '${business.notes?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationName?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationAddress?.replaceAll("'", "''") ?? 'NULL'}') VALUES ('${business.name.replaceAll("'", "''")}', '${business.description.replaceAll("'", "''")}', '${business.website ?? 'NULL'}', '${business.type?.name}', '${business.contactName?.replaceAll("'", "''") ?? 'NULL'}', '${business.contactPhone ?? 'NULL'}', '${business.contactEmail ?? 'NULL'}', '${business.notes?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationName?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationAddress?.replaceAll("'", "''") ?? 'NULL'}')
'''); '''
.replaceAll("'null'", 'NULL'));
final dbBusiness = await postgres.query('''SELECT * FROM public.businesses final dbBusiness = await postgres.query('''SELECT * FROM public.businesses
ORDER BY id DESC LIMIT 1'''); ORDER BY id DESC LIMIT 1''');
@ -403,8 +476,9 @@ void main() async {
await postgres.query(''' await postgres.query('''
INSERT INTO listings ("businessId", name, description, type, wage, link) INSERT INTO listings ("businessId", name, description, type, wage, link)
VALUES ('${listing.businessId}' '${listing.name.replaceAll("'", "''")}', '${listing.description.replaceAll("'", "''")}', '${listing.type.name}', '${listing.wage ?? 'NULL'}', '${listing.link?.replaceAll("'", "''") ?? 'NULL'}') VALUES ('${listing.businessId}', '${listing.name.replaceAll("'", "''")}', '${listing.description.replaceAll("'", "''")}', '${listing.type.name}', '${listing.wage ?? 'NULL'}', '${listing.link?.replaceAll("'", "''") ?? 'NULL'}')
'''); '''
.replaceAll("'null'", 'NULL'));
final dbListing = await postgres.query('''SELECT id FROM public.listings final dbListing = await postgres.query('''SELECT id FROM public.listings
ORDER BY id DESC LIMIT 1'''); ORDER BY id DESC LIMIT 1''');
@ -500,7 +574,8 @@ void main() async {
UPDATE businesses SET UPDATE businesses SET
name = '${business.name.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, description = '${business.description.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, website = '${business.website!}'::text, "contactName" = '${business.contactName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "contactPhone" = '${business.contactPhone!}'::text, "contactEmail" = '${business.contactEmail!}'::text, notes = '${business.notes!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationName" = '${business.locationName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationAddress" = '${business.locationAddress!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text WHERE name = '${business.name.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, description = '${business.description.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, website = '${business.website!}'::text, "contactName" = '${business.contactName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "contactPhone" = '${business.contactPhone!}'::text, "contactEmail" = '${business.contactEmail!}'::text, notes = '${business.notes!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationName" = '${business.locationName!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationAddress" = '${business.locationAddress!.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text WHERE
id = ${business.id}; id = ${business.id};
'''); '''
.replaceAll("'null'", 'NULL'));
var logoResponse = await http.get( var logoResponse = await http.get(
Uri.http('logo.clearbit.com', '/${business.website}'), Uri.http('logo.clearbit.com', '/${business.website}'),
@ -546,7 +621,8 @@ void main() async {
UPDATE listings SET UPDATE listings SET
"businessId" = ${listing.businessId}, name = '${listing.name.replaceAll("'", "''")}'::text, description = '${listing.description.replaceAll("'", "''")}'::text, type = '${listing.type.name}'::text, wage = '${listing.wage ?? 'NULL'}'::text, link = '${listing.link?.replaceAll("'", "''") ?? 'NULL'}'::text WHERE "businessId" = ${listing.businessId}, name = '${listing.name.replaceAll("'", "''")}'::text, description = '${listing.description.replaceAll("'", "''")}'::text, type = '${listing.type.name}'::text, wage = '${listing.wage ?? 'NULL'}'::text, link = '${listing.link?.replaceAll("'", "''") ?? 'NULL'}'::text WHERE
id = ${listing.id}; id = ${listing.id};
'''); '''
.replaceAll("'null'", 'NULL'));
return Response.ok( return Response.ok(
listing.id.toString(), listing.id.toString(),

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200mm"
height="70mm"
viewBox="0 0 200 70"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="MDEVLogoFull.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.75544332"
inkscape:cx="47.654138"
inkscape:cy="97.955728"
inkscape:window-width="1920"
inkscape:window-height="1046"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1"><rect
x="164.6723"
y="176.95522"
width="237.7821"
height="119.97466"
id="rect1047" /><rect
x="170.3439"
y="136.68353"
width="277.49832"
height="196.87138"
id="rect9139" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><path
style="font-size:48px;font-family:HeadlineNEWS;-inkscape-font-specification:HeadlineNEWS;white-space:pre;shape-padding:0.133618;fill:#000000;fill-opacity:1;stroke-width:0.608723"
d="m 103.58306,22.62203 h -7.380754 q 0,-2.975635 0,-5.95127 0,-4.113378 -0.233384,-5.74706 l -4.113377,8.489312 -3.617439,-0.08752 -3.82165,-8.401793 q -0.262556,1.779547 -0.291729,5.74706 0,2.975635 0,5.95127 h -7.380745 l 1.10857,-21.90884035 h 7.205708 l 5.105257,9.91878235 5.134429,-9.91878235 h 7.147374 z m 22.60912,0 h -5.68875 l -0.90436,-2.683906 h -7.20575 l -0.93354,2.683906 h -5.71792 L 112.7434,5.1182952 h 6.47642 z m -7.52665,-5.54285 -2.71309,-8.4601379 -2.65475,8.4601379 z m 27.62688,5.54285 h -6.12634 q -1.72121,-3.442402 -1.80873,-3.617439 -1.40031,-2.479696 -2.45054,-3.033981 l -1.72121,-0.904359 V 22.62203 H 128.4385 V 5.1182952 h 9.71463 q 2.77144,0 4.43431,1.050224 2.01294,1.2836072 2.01294,3.8799948 0,1.808719 -1.16692,2.888116 -0.81685,0.787668 -2.77145,1.51699 1.25445,0.583458 3.23821,3.850822 1.1961,2.158794 2.39219,4.317588 z m -7.29326,-12.194269 q 0,-2.4505225 -3.034,-2.4505225 h -1.77956 V 12.93663 h 1.77956 q 1.31279,0 2.15881,-0.612631 0.87519,-0.641804 0.87519,-1.896238 z m 15.66591,12.194269 h -6.068 V 5.1182952 h 6.068 z m 22.40489,0 h -5.33867 l -7.67251,-9.743746 0.20421,9.743746 h -5.74709 V 5.1182952 h 5.36784 l 7.76003,9.6562268 -0.26256,-9.6562268 h 5.68875 z m 22.02554,-8.839386 q 0,4.230069 -2.59639,6.709764 -2.59639,2.450523 -6.85563,2.450523 -4.23007,0 -6.85563,-2.42135 -2.62556,-2.42135 -2.62556,-6.622246 0,-4.1717228 2.71308,-6.6805917 2.62556,-2.4505228 6.85563,-2.4505228 4.11338,0 6.70976,2.4213499 2.65474,2.5088688 2.65474,6.5930736 z m -5.74706,0.02917 q 0,-1.808719 -0.96271,-2.946462 -0.9627,-1.1669148 -2.74225,-1.1669148 -1.83789,0 -2.77143,1.2252608 -0.87518,1.10857 -0.87518,2.975635 0,1.837892 0.87518,2.917289 0.93354,1.196089 2.71308,1.196089 3.76331,0 3.76331,-4.200897 z"
id="text9137"
inkscape:label="Marino"
aria-label="Marino" /><path
d="m 104.7848,29.612184 q 8.86861,0 14.46983,5.4456 5.60122,5.393746 5.60122,14.210437 0,8.505515 -5.18631,13.899254 -5.13446,5.341879 -13.58816,5.341879 H 86.114135 v -38.89717 z m 6.37918,19.500449 q 0,-4.200897 -2.1264,-6.690317 -2.28198,-2.696869 -6.43104,-2.696869 h -2.956187 v 18.774366 h 3.059917 q 4.25279,0 6.43104,-2.645003 2.02267,-2.437559 2.02267,-6.742177 z m 45.19311,19.396721 H 128.71401 V 29.560317 h 27.53935 v 8.92042 H 142.2503 v 5.964233 h 12.65462 v 8.868553 H 142.2503 v 6.327278 h 14.10679 z m 43.5333,-38.949037 -15.40328,38.949037 H 173.18098 L 157.31084,29.560317 h 14.41797 l 7.62387,23.234579 q 0.15559,-0.20745 7.31267,-23.234579 z"
id="text1045"
style="font-size:85.3333px;font-family:HeadlineNEWS;-inkscape-font-specification:HeadlineNEWS;letter-spacing:-4.66px;white-space:pre;fill:#000000;fill-opacity:1;stroke-width:0.608723"
inkscape:label="Dev"
aria-label="DEV" /><path
d="m 39.967467,0 c -1.24713,0 -2.07854,1.440059 -2.07854,1.440059 l -4.57281,7.920321 17.81611,30.858404 c 4.37185,-0.99615 6.86321,4.80385 3.14145,7.29302 -3.72178,2.48917 -8.13084,-2.02826 -5.54068,-5.68838 v 0 L 31.653287,12.240495 0.47509223,66.242704 c -0.55431396,0.96004 -0.62353496,1.80008 -0.20783896,2.52011 0.41568996,0.72005 1.17784883,1.08004 2.28640583,1.08004 h 9.1456049 l 17.816103,-30.8584 c -3.04862,-3.28806 0.72867,-8.34565 4.74522,-6.36708 4.01656,1.97857 2.30888,8.05564 -2.15596,7.64256 v 0 l -17.079704,29.58292 h 62.356384 c 0,0 1.66283,0 2.2864,-1.08004 0.62353,-1.08005 -0.20784,-2.52011 -0.20784,-2.52011 l -4.57281,-7.92032 h -35.63222 c -1.32322,4.28422 -7.59186,3.5418 -7.88666,-0.92594 -0.29484,-4.46773 5.82195,-6.02738 7.69662,-1.95418 v 0 h 34.15941 L 42.046017,1.440059 c 0,0 -0.83138,-1.440059 -2.07855,-1.440059"
style="display:inline;fill:#2096f3;fill-opacity:1;stroke:none;stroke-width:2.37548"
id="path395"
inkscape:label="triangle"
inkscape:transform-center-y="-11.666667" /></g></svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -1,80 +1,50 @@
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:fbla_ui/pages/businesses_overview.dart';
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/pages/create_edit_business.dart'; import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/pages/export_data.dart'; import 'package:fbla_ui/pages/create_edit_listing.dart';
import 'package:fbla_ui/pages/signin_page.dart'; import 'package:fbla_ui/pages/listings_overview.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
class Home extends StatefulWidget { class Home extends StatefulWidget {
final void Function() themeCallback; final void Function() themeCallback;
final int? initialPage;
const Home({super.key, required this.themeCallback}); const Home({super.key, required this.themeCallback, this.initialPage});
@override @override
State<Home> createState() => _HomeState(); State<Home> createState() => _HomeState();
} }
class _HomeState extends State<Home> { class _HomeState extends State<Home> {
late Future refreshBusinessDataOverviewFuture;
bool _isPreviousData = false;
late Map<JobType, List<Business>> overviewBusinesses;
Set<JobType> jobTypeFilters = <JobType>{}; Set<JobType> jobTypeFilters = <JobType>{};
Set<BusinessType> businessTypeFilters = <BusinessType>{};
String searchQuery = ''; String searchQuery = '';
Set<DataTypeJob> selectedDataTypesJob = <DataTypeJob>{}; late Future refreshBusinessDataOverviewJobFuture;
Set<DataTypeBusiness> selectedDataTypesBusiness = <DataTypeBusiness>{}; late Future refreshBusinessDataOverviewBusinessFuture;
int currentPageIndex = 0;
late dynamic previousJobData;
ScrollController scrollControllerBusinesses = ScrollController();
ScrollController scrollControllerJobs = ScrollController();
Future<void> _setFilters(Set<JobType> filters) async { void _updateLoggedIn(bool updated) {
setState(() { setState(() {
jobTypeFilters = filters; loggedIn = updated;
}); });
_updateOverviewBusinesses();
}
Future<void> _updateOverviewBusinesses() async {
var refreshedData =
fetchBusinessDataOverview(typeFilters: jobTypeFilters.toList());
await refreshedData;
setState(() {
refreshBusinessDataOverviewFuture = refreshedData;
});
}
Map<JobType, List<Business>> _filterBySearch(
Map<JobType, List<Business>> businesses) {
Map<JobType, List<Business>> filteredBusinesses = businesses;
for (JobType jobType in businesses.keys) {
filteredBusinesses[jobType]!.removeWhere((tmpBusiness) => !tmpBusiness
.name
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.contains(searchQuery
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.trim()));
}
filteredBusinesses.removeWhere((key, value) => value.isEmpty);
return filteredBusinesses;
}
Future<void> _setSearch(String search) async {
setState(() {
searchQuery = search;
});
_updateOverviewBusinesses();
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
refreshBusinessDataOverviewFuture = fetchBusinessDataOverview(); currentPageIndex = widget.initialPage ?? 0;
initialLogin(); initialLogin();
refreshBusinessDataOverviewJobFuture = fetchBusinessDataOverviewJobs();
refreshBusinessDataOverviewBusinessFuture =
fetchBusinessDataOverviewTypes();
} }
Future<void> initialLogin() async { Future<void> initialLogin() async {
@ -94,319 +64,321 @@ class _HomeState extends State<Home> {
} }
} }
void setStateCallback() { Future<void> _updateOverviewBusinessesJobsCallback(
Set<JobType>? newFilters) async {
if (newFilters != null) {
jobTypeFilters = Set.from(newFilters);
}
var refreshedData =
fetchBusinessDataOverviewJobs(typeFilters: jobTypeFilters.toList());
await refreshedData;
setState(() { setState(() {
loggedIn = loggedIn; refreshBusinessDataOverviewJobFuture = refreshedData;
});
}
Future<void> _updateOverviewBusinessesBusinessCallback(
Set<BusinessType>? newFilters) async {
if (newFilters != null) {
businessTypeFilters = Set.from(newFilters);
}
var refreshedData = fetchBusinessDataOverviewTypes(
typeFilters: businessTypeFilters.toList());
await refreshedData;
setState(() {
refreshBusinessDataOverviewBusinessFuture = refreshedData;
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool widescreen = MediaQuery.sizeOf(context).width >= 1000; bool widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
return Scaffold( return Scaffold(
// backgroundColor: Theme.of(context).scaffoldBackgroundColor, // floatingActionButton: _getFAB(widescreen, scrollControllerBusinesses,
floatingActionButton: _getFAB(), // scrollControllerJobs, currentPageIndex),
bottomNavigationBar: _getNavigationBar(widescreen),
body: RefreshIndicator( body: RefreshIndicator(
edgeOffset: 120, edgeOffset: 145,
onRefresh: () async { onRefresh: () async {
_updateOverviewBusinesses(); _updateOverviewBusinessesJobsCallback(null);
_updateOverviewBusinessesBusinessCallback(null);
}, },
child: CustomScrollView( child: widescreen
slivers: [ ? Row(
SliverAppBar( children: [
title: widescreen _getNavigationRail(),
? BusinessSearchBar( Expanded(
filters: jobTypeFilters, child: _ContentPane(
setFiltersCallback: _setFilters, themeCallback: widget.themeCallback,
setSearchCallback: _setSearch) searchQuery: searchQuery,
: const Text('Job Link'), currentPageIndex: currentPageIndex,
toolbarHeight: 70, refreshBusinessDataOverviewBusinessFuture:
pinned: true, refreshBusinessDataOverviewBusinessFuture,
scrolledUnderElevation: 0, refreshBusinessDataOverviewJobFuture:
centerTitle: true, refreshBusinessDataOverviewJobFuture,
expandedHeight: widescreen ? 70 : 120, updateOverviewBusinessesBusinessCallback:
bottom: _getBottom(), _updateOverviewBusinessesBusinessCallback,
leading: IconButton( updateOverviewBusinessesJobsCallback:
icon: getIconFromThemeMode(themeMode), _updateOverviewBusinessesJobsCallback,
updateLoggedIn: _updateLoggedIn,
),
)
],
)
: _ContentPane(
themeCallback: widget.themeCallback,
searchQuery: searchQuery,
currentPageIndex: currentPageIndex,
refreshBusinessDataOverviewBusinessFuture:
refreshBusinessDataOverviewBusinessFuture,
refreshBusinessDataOverviewJobFuture:
refreshBusinessDataOverviewJobFuture,
updateOverviewBusinessesBusinessCallback:
_updateOverviewBusinessesBusinessCallback,
updateOverviewBusinessesJobsCallback:
_updateOverviewBusinessesJobsCallback,
updateLoggedIn: _updateLoggedIn,
),
),
);
}
Widget? _getNavigationBar(bool widescreen) {
if (!widescreen) {
return NavigationBar(
selectedIndex: currentPageIndex,
indicatorColor: Theme.of(context).colorScheme.primary.withOpacity(0.5),
onDestinationSelected: (int index) {
setState(() {
currentPageIndex = index;
});
},
destinations: <NavigationDestination>[
NavigationDestination(
icon: const Icon(Icons.business_outlined),
selectedIcon: Icon(
Icons.business,
color: Theme.of(context).colorScheme.onSurface,
),
label: 'Businesses'),
NavigationDestination(
icon: const Icon(Icons.work_outline),
selectedIcon: Icon(
Icons.work,
color: Theme.of(context).colorScheme.onSurface,
),
label: 'Job Listings'),
// NavigationDestination(
// icon: const Icon(Icons.description_outlined),
// selectedIcon: Icon(
// Icons.description,
// color: Theme.of(context).colorScheme.onSurface,
// ),
// label: 'Export Data')
],
);
}
return null;
}
Widget _getNavigationRail() {
return Row(
children: [
NavigationRail(
selectedIndex: currentPageIndex,
groupAlignment: -1,
indicatorColor:
Theme.of(context).colorScheme.primary.withOpacity(0.5),
trailing: Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(16),
child: IconButton(
iconSize: 30,
icon: Icon(
getIconFromThemeMode(themeMode),
),
onPressed: () { onPressed: () {
setState(() { setState(() {
widget.themeCallback(); widget.themeCallback();
}); });
}, },
), ),
actions: [ ),
IconButton( ),
icon: const Icon(Icons.help), ),
leading: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 2.0, bottom: 8.0),
child: Image.asset(
'assets/Triangle256.png',
height: 50,
),
),
if (loggedIn)
FloatingActionButton(
child: Icon(Icons.add),
heroTag: 'Homepage',
onPressed: () { onPressed: () {
showDialog( if (currentPageIndex == 0) {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('About'),
backgroundColor:
Theme.of(context).colorScheme.surface,
content: SizedBox(
width: 500,
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Welcome to my FBLA 2024 Coding and Programming submission!\n\n'
'MarinoDev Job Link aims to provide comprehensive details of businesses and community partners'
' for Waukesha West High School\'s Career and Technical Education Department.\n\n'),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: const Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('Git Repo:'),
Text(
'https://git.marinodev.com/MarinoDev/FBLA24\n',
style: TextStyle(
color: Colors.blue)),
],
),
onTap: () {
launchUrl(Uri.https(
'git.marinodev.com',
'/MarinoDev/FBLA24'));
},
),
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: const Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Please direct any questions to'),
Text('drake@marinodev.com',
style: TextStyle(
color: Colors.blue)),
],
),
onTap: () {
launchUrl(Uri.parse(
'mailto:drake@marinodev.com'));
},
),
)
],
),
),
),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
}),
],
);
});
},
),
IconButton(
icon: const Icon(Icons.picture_as_pdf),
onPressed: () async {
if (!_isPreviousData) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text('There is no data!'),
duration: Duration(seconds: 2),
),
);
} else {
selectedDataTypesBusiness = <DataTypeBusiness>{};
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ExportData( builder: (context) =>
groupedBusinesses: overviewBusinesses))); const CreateEditBusiness()));
} else if (currentPageIndex == 1) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const CreateEditJobListing()));
} }
}, },
)
],
), ),
Padding( onDestinationSelected: (int index) {
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: loggedIn
? const Icon(Icons.account_circle)
: const Icon(Icons.login),
onPressed: () {
if (loggedIn) {
var payload = JWT.decode(jwt).payload;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.surface,
title: Text('Hi, ${payload['username']}!'),
content: Text(
'You are logged in as an admin with username ${payload['username']}.'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Logout'),
onPressed: () async {
final prefs = await SharedPreferences
.getInstance();
prefs.setBool('rememberMe', false);
prefs.setString('username', '');
prefs.setString('password', '');
setState(() { setState(() {
loggedIn = false; currentPageIndex = index;
}); });
Navigator.of(context).pop(); },
}), labelType: NavigationRailLabelType.all,
destinations: <NavigationRailDestination>[
NavigationRailDestination(
icon: const Icon(Icons.business_outlined),
selectedIcon: Icon(
Icons.business,
color: Theme.of(context).colorScheme.onSurface,
),
label: const Text('Businesses')),
NavigationRailDestination(
icon: const Icon(Icons.work_outline),
selectedIcon: Icon(
Icons.work,
color: Theme.of(context).colorScheme.onSurface,
),
label: const Text('Job Listings')),
// NavigationRailDestination(
// icon: const Icon(Icons.description_outlined),
// selectedIcon: Icon(
// Icons.description,
// color: Theme.of(context).colorScheme.onSurface,
// ),
// label: const Text('Export Data'))
],
),
// children.first
], ],
); );
// }
// return children.first;
}
// Widget _contentPane() {
// return IndexedStack(
// index: currentPageIndex,
// children: [
// BusinessesOverview(
// searchQuery: searchQuery,
// refreshBusinessDataOverviewFuture:
// refreshBusinessDataOverviewBusinessFuture,
// updateBusinessesCallback: _updateOverviewBusinessesBusinessCallback,
// themeCallback: widget.themeCallback,
// updateLoggedIn: _updateLoggedIn,
// ),
// JobsOverview(
// searchQuery: searchQuery,
// refreshJobDataOverviewFuture: refreshBusinessDataOverviewJobFuture,
// updateBusinessesCallback: _updateOverviewBusinessesJobsCallback,
// themeCallback: widget.themeCallback, updateLoggedIn: _updateLoggedIn),
// ExportData(
// searchQuery: searchQuery,
// refreshBusinessDataOverviewFuture:
// refreshBusinessDataOverviewBusinessFuture,
// updateBusinessesWithJobCallback:
// _updateOverviewBusinessesJobsCallback,
// themeCallback: widget.themeCallback,
// refreshJobDataOverviewFuture: refreshBusinessDataOverviewJobFuture,
// updateBusinessesCallback: _updateOverviewBusinessesBusinessCallback)
// ],
// );
// }
}
class _ContentPane extends StatelessWidget {
final String searchQuery;
final Future refreshBusinessDataOverviewBusinessFuture;
final Future<void> Function(Set<BusinessType>)
updateOverviewBusinessesBusinessCallback;
final void Function() themeCallback;
final Future refreshBusinessDataOverviewJobFuture;
final Future<void> Function(Set<JobType>)
updateOverviewBusinessesJobsCallback;
final int currentPageIndex;
final void Function(bool) updateLoggedIn;
const _ContentPane({
required this.searchQuery,
required this.refreshBusinessDataOverviewBusinessFuture,
required this.updateOverviewBusinessesBusinessCallback,
required this.themeCallback,
required this.refreshBusinessDataOverviewJobFuture,
required this.updateOverviewBusinessesJobsCallback,
required this.currentPageIndex,
required this.updateLoggedIn,
}); });
} else {
Navigator.push( @override
context, Widget build(BuildContext context) {
MaterialPageRoute( return IndexedStack(
builder: (context) => SignInPage( index: currentPageIndex,
refreshAccount: setStateCallback))); children: [
} BusinessesOverview(
}, searchQuery: searchQuery,
refreshBusinessDataOverviewFuture:
refreshBusinessDataOverviewBusinessFuture,
updateBusinessesCallback: updateOverviewBusinessesBusinessCallback,
themeCallback: themeCallback,
updateLoggedIn: updateLoggedIn,
), ),
JobsOverview(
searchQuery: searchQuery,
refreshJobDataOverviewFuture: refreshBusinessDataOverviewJobFuture,
updateBusinessesCallback: updateOverviewBusinessesJobsCallback,
themeCallback: themeCallback,
updateLoggedIn: updateLoggedIn,
), ),
// ExportData(
// searchQuery: searchQuery,
// refreshBusinessDataOverviewFuture:
// refreshBusinessDataOverviewBusinessFuture,
// updateBusinessesWithJobCallback:
// updateOverviewBusinessesJobsCallback,
// themeCallback: themeCallback,
// refreshJobDataOverviewFuture: refreshBusinessDataOverviewJobFuture,
// updateBusinessesCallback: updateOverviewBusinessesBusinessCallback)
], ],
),
FutureBuilder(
future: refreshBusinessDataOverviewFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType == String) {
_isPreviousData = false;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child: Text(snapshot.data,
textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
_updateOverviewBusinesses();
},
),
),
]),
));
}
overviewBusinesses = snapshot.data;
_isPreviousData = true;
return BusinessDisplayPanel(
groupedBusinesses:
_filterBySearch(overviewBusinesses),
widescreen: widescreen,
selectable: false);
} else if (snapshot.hasError) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
));
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
if (_isPreviousData) {
return BusinessDisplayPanel(
groupedBusinesses:
_filterBySearch(overviewBusinesses),
widescreen: widescreen,
selectable: false);
} else {
return SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child: RiveAnimation.asset(
'assets/mdev_triangle_loading.riv'),
),
));
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
}),
const SliverToBoxAdapter(
child: SizedBox(
height: 80,
),
)
],
),
),
); );
} }
Widget? _getFAB() {
if (loggedIn) {
return FloatingActionButton(
child: const Icon(Icons.add_business),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CreateEditBusiness()));
},
);
}
return null;
} }
PreferredSizeWidget? _getBottom() { // class FABAnimator extends FloatingActionButtonAnimator {
if (MediaQuery.sizeOf(context).width <= 1000) { // @override
return PreferredSize( // Offset getOffset({Offset begin, Offset end, double progress}) {
preferredSize: const Size.fromHeight(0), // return end;
child: SizedBox( // }
// color: Theme.of(context).colorScheme.background, //
height: 70, // @override
child: Padding( // Animation<double> getRotationAnimation({required Animation<double> parent}) {
padding: const EdgeInsets.all(10), // return Tween<double>(begin: 0.0, end: 1.0).animate(parent);
child: BusinessSearchBar( // throw UnimplementedError();
filters: jobTypeFilters, // }
setFiltersCallback: _setFilters, //
setSearchCallback: _setSearch), // @override
), // Animation<double> getScaleAnimation({required Animation<double> parent}) {
), // return Tween<double>(begin: 0.0, end: 1.0).animate(parent);
); // throw UnimplementedError();
} // }
return null; // }
}
}

View File

@ -1,10 +1,9 @@
import 'package:fbla_ui/home.dart'; import 'package:fbla_ui/home.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
ThemeMode themeMode = ThemeMode.system;
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -23,9 +22,9 @@ void main() async {
} }
class MainApp extends StatefulWidget { class MainApp extends StatefulWidget {
final bool? isDark; final int? initialPage;
const MainApp({super.key, this.isDark}); const MainApp({super.key, this.initialPage});
@override @override
State<MainApp> createState() => _MainAppState(); State<MainApp> createState() => _MainAppState();
@ -72,7 +71,7 @@ class _MainAppState extends State<MainApp> {
darkTheme: ThemeData( darkTheme: ThemeData(
colorScheme: ColorScheme.dark( colorScheme: ColorScheme.dark(
brightness: Brightness.dark, brightness: Brightness.dark,
primary: Colors.blue, primary: Colors.blue.shade700,
onPrimary: Colors.white, onPrimary: Colors.white,
secondary: Colors.blue.shade900, secondary: Colors.blue.shade900,
surface: const Color.fromARGB(255, 31, 31, 31), surface: const Color.fromARGB(255, 31, 31, 31),
@ -86,7 +85,7 @@ class _MainAppState extends State<MainApp> {
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.light( colorScheme: ColorScheme.light(
brightness: Brightness.light, brightness: Brightness.light,
primary: Colors.blue, primary: Colors.blue.shade700,
onPrimary: Colors.white, onPrimary: Colors.white,
secondary: Colors.blue.shade200, secondary: Colors.blue.shade200,
surface: Colors.grey.shade200, surface: Colors.grey.shade200,
@ -98,7 +97,7 @@ class _MainAppState extends State<MainApp> {
const InputDecorationTheme(border: UnderlineInputBorder()), const InputDecorationTheme(border: UnderlineInputBorder()),
useMaterial3: true, useMaterial3: true,
), ),
home: Home(themeCallback: _switchTheme), home: Home(themeCallback: _switchTheme, initialPage: widget.initialPage),
); );
} }
} }

View File

@ -1,24 +1,20 @@
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/main.dart'; import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/pages/create_edit_business.dart'; import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/pages/create_edit_listing.dart'; import 'package:fbla_ui/pages/create_edit_listing.dart';
import 'package:fbla_ui/pages/listing_detail.dart'; import 'package:fbla_ui/pages/listing_detail.dart';
import 'package:fbla_ui/pages/signin_page.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/shared/global_vars.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rive/rive.dart'; import 'package:rive/rive.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../shared/utils.dart';
class BusinessDetail extends StatefulWidget { class BusinessDetail extends StatefulWidget {
final int id; final int id;
final String name; final String name;
final JobType clickFromType;
const BusinessDetail( const BusinessDetail({super.key, required this.id, required this.name});
{super.key,
required this.id,
required this.name,
required this.clickFromType});
@override @override
State<BusinessDetail> createState() => _CreateBusinessDetailState(); State<BusinessDetail> createState() => _CreateBusinessDetailState();
@ -45,7 +41,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(snapshot.data.name), title: Text(snapshot.data.name),
actions: _getActions(snapshot.data, widget.clickFromType), actions: _getActions(snapshot.data),
), ),
body: _detailBody(snapshot.data), body: _detailBody(snapshot.data),
); );
@ -120,12 +116,12 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(
title: Text(business.name, title: Text(business.name!,
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)), fontSize: 24, fontWeight: FontWeight.bold)),
subtitle: Text( subtitle: Text(
business.description, business.description!,
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
leading: ClipRRect( leading: ClipRRect(
@ -134,8 +130,8 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
width: 48, width: 48,
height: 48, errorBuilder: (BuildContext context, height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) { Object exception, StackTrace? stackTrace) {
return getIconFromJobType(widget.clickFromType, 48, return Icon(getIconFromBusinessType(business.type!),
Theme.of(context).colorScheme.onSurface); size: 48);
}), }),
), ),
), ),
@ -143,13 +139,13 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
leading: const Icon(Icons.link), leading: const Icon(Icons.link),
title: const Text('Website'), title: const Text('Website'),
subtitle: Text( subtitle: Text(
business.website business.website!
.replaceAll('https://', '') .replaceAll('https://', '')
.replaceAll('http://', '') .replaceAll('http://', '')
.replaceAll('www.', ''), .replaceAll('www.', ''),
style: const TextStyle(color: Colors.blue)), style: const TextStyle(color: Colors.blue)),
onTap: () { onTap: () {
launchUrl(Uri.parse(business.website)); launchUrl(Uri.parse(business.website!));
}, },
), ),
], ],
@ -157,6 +153,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
), ),
), ),
// Available positions // Available positions
if (business.listings != null)
Card( Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: child:
@ -185,9 +182,8 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
), ),
], ],
), ),
Visibility( if (business.contactPhone != null)
visible: business.contactPhone != null, ListTile(
child: ListTile(
leading: const Icon(Icons.phone), leading: const Icon(Icons.phone),
title: Text(business.contactPhone!), title: Text(business.contactPhone!),
// maybe replace ! with ?? ''. same is true for below // maybe replace ! with ?? ''. same is true for below
@ -221,10 +217,10 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
}); });
}, },
), ),
), if (business.contactEmail != null)
ListTile( ListTile(
leading: const Icon(Icons.email), leading: const Icon(Icons.email),
title: Text(business.contactEmail), title: Text(business.contactEmail!),
onTap: () { onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}')); launchUrl(Uri.parse('mailto:${business.contactEmail}'));
}, },
@ -233,8 +229,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
), ),
), ),
// Location // Location
Visibility( Card(
child: Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: ListTile( child: ListTile(
leading: const Icon(Icons.location_on), leading: const Icon(Icons.location_on),
@ -246,11 +241,9 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
}, },
), ),
), ),
),
// Notes // Notes
Visibility( if (business.notes != null && business.notes != '')
visible: business.notes != null && business.notes != '', Card(
child: Card(
child: ListTile( child: ListTile(
leading: const Icon(Icons.notes), leading: const Icon(Icons.notes),
title: const Text( title: const Text(
@ -260,12 +253,11 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
subtitle: Text(business.notes!), subtitle: Text(business.notes!),
), ),
), ),
),
], ],
); );
} }
List<Widget>? _getActions(Business business, JobType clickFromType) { List<Widget>? _getActions(Business business) {
if (loggedIn) { if (loggedIn) {
return [ return [
IconButton( IconButton(
@ -274,7 +266,6 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CreateEditBusiness( builder: (context) => CreateEditBusiness(
inputBusiness: business, inputBusiness: business,
clickFromType: clickFromType,
))); )));
}, },
), ),
@ -354,8 +345,7 @@ class _JobListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
leading: getIconFromJobType( leading: Icon(getIconFromJobType(jobListing.type!)),
jobListing.type, 24, Theme.of(context).colorScheme.onSurface),
title: Text(jobListing.name), title: Text(jobListing.name),
subtitle: Text( subtitle: Text(
jobListing.description, jobListing.description,

View File

@ -0,0 +1,581 @@
import 'package:fbla_ui/pages/business_detail.dart';
import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/export.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:fbla_ui/shared/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:rive/rive.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:url_launcher/url_launcher.dart';
class BusinessesOverview extends StatefulWidget {
final String searchQuery;
final Future refreshBusinessDataOverviewFuture;
final Future<void> Function(Set<BusinessType>) updateBusinessesCallback;
final void Function() themeCallback;
final void Function(bool) updateLoggedIn;
const BusinessesOverview({
super.key,
required this.searchQuery,
required this.refreshBusinessDataOverviewFuture,
required this.updateBusinessesCallback,
required this.themeCallback,
required this.updateLoggedIn,
});
@override
State<BusinessesOverview> createState() => _BusinessesOverviewState();
}
class _BusinessesOverviewState extends State<BusinessesOverview> {
bool _isPreviousData = false;
late Map<BusinessType, List<Business>> overviewBusinesses;
Set<BusinessType> businessTypeFilters = <BusinessType>{};
String searchQuery = '';
ScrollController controller = ScrollController();
bool _extended = true;
double prevPixelPosition = 0;
Map<BusinessType, List<Business>> _filterBySearch(
Map<BusinessType, List<Business>> businesses, String query) {
Map<BusinessType, List<Business>> filteredBusinesses = {};
for (BusinessType businessType in businesses.keys) {
filteredBusinesses[businessType] = List.from(businesses[businessType]!
.where((element) => element.name!
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.contains(query
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.trim())));
}
filteredBusinesses.removeWhere((key, value) => value.isEmpty);
return filteredBusinesses;
}
void _setSearch(String search) async {
setState(() {
searchQuery = search;
});
}
void _setFilters(Set<BusinessType> filters) async {
businessTypeFilters = Set.from(filters);
widget.updateBusinessesCallback(businessTypeFilters);
}
void _scrollListener() {
if ((prevPixelPosition - controller.position.pixels).abs() > 10) {
setState(() {
_extended =
controller.position.userScrollDirection == ScrollDirection.forward;
});
}
prevPixelPosition = controller.position.pixels;
}
void _generatePDF() {
List<Business> allBusinesses = [];
for (List<Business> businessList
in _filterBySearch(overviewBusinesses, searchQuery).values) {
allBusinesses.addAll(businessList);
}
generatePDF(
context: context,
documentTypeIndex: 0,
selectedBusinesses: Set.from(allBusinesses));
}
@override
void initState() {
super.initState();
controller.addListener(_scrollListener);
}
@override
Widget build(BuildContext context) {
bool widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
return Scaffold(
floatingActionButton: _getFAB(widescreen),
body: CustomScrollView(
controller: controller,
slivers: [
MainSliverAppBar(
widescreen: widescreen,
setSearch: _setSearch,
searchHintText: 'Search Businesses',
themeCallback: widget.themeCallback,
filterIconButton: _filterIconButton(
businessTypeFilters,
),
updateLoggedIn: widget.updateLoggedIn,
generatePDF: _generatePDF,
),
FutureBuilder(
future: widget.refreshBusinessDataOverviewFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType == String) {
_isPreviousData = false;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child: Text(snapshot.data,
textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
widget.updateBusinessesCallback(
businessTypeFilters);
},
),
),
]),
));
}
overviewBusinesses = snapshot.data;
_isPreviousData = true;
return BusinessDisplayPanel(
groupedBusinesses:
_filterBySearch(overviewBusinesses, searchQuery),
widescreen: widescreen,
);
} else if (snapshot.hasError) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
));
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
if (_isPreviousData) {
return BusinessDisplayPanel(
groupedBusinesses:
_filterBySearch(overviewBusinesses, searchQuery),
widescreen: widescreen,
);
} else {
return SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child: RiveAnimation.asset(
'assets/mdev_triangle_loading.riv'),
),
));
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
}),
],
),
);
}
Widget _filterIconButton(Set<BusinessType> filters) {
Set<BusinessType> selectedChips = Set.from(filters);
return IconButton(
icon: Icon(
Icons.filter_list,
color: filters.isNotEmpty
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
void setDialogState(Set<BusinessType> newFilters) {
setState(() {
filters = newFilters;
});
}
List<Padding> 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);
}),
));
}
return AlertDialog(
title: const Text('Filter Options'),
content: SizedBox(
width: 400,
child: Wrap(
children: chips,
),
),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
_setFilters(<BusinessType>{});
// selectedChips = <BusinessType>{};
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Cancel'),
onPressed: () {
// selectedChips = Set.from(filters);
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Apply'),
onPressed: () {
_setFilters(selectedChips);
Navigator.of(context).pop();
},
)
],
);
});
});
});
}
Widget? _getFAB(bool widescreen) {
if (!widescreen && loggedIn) {
return FloatingActionButton.extended(
extendedIconLabelSpacing: _extended ? 8.0 : 0,
extendedPadding: const EdgeInsets.symmetric(horizontal: 16),
icon: const Icon(Icons.add),
label: AnimatedSize(
curve: Easing.standard,
duration: const Duration(milliseconds: 300),
child: _extended ? const Text('Add Business') : Container(),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CreateEditBusiness()));
},
);
}
return null;
}
}
class BusinessDisplayPanel extends StatefulWidget {
final Map<BusinessType, List<Business>> groupedBusinesses;
final bool widescreen;
const BusinessDisplayPanel({
super.key,
required this.groupedBusinesses,
required this.widescreen,
});
@override
State<BusinessDisplayPanel> createState() => _BusinessDisplayPanelState();
}
class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
@override
Widget build(BuildContext context) {
if (widget.groupedBusinesses.keys.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'No results found!\nPlease change your search filters.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
),
),
);
}
List<BusinessHeader> headers = [];
for (BusinessType businessType in widget.groupedBusinesses.keys) {
headers.add(BusinessHeader(
businessType: businessType,
widescreen: widget.widescreen,
businesses: widget.groupedBusinesses[businessType]!));
}
headers
.sort((a, b) => a.businessType.index.compareTo(b.businessType.index));
return MultiSliver(children: headers);
}
}
class BusinessHeader extends StatefulWidget {
final BusinessType businessType;
final List<Business> businesses;
final bool widescreen;
const BusinessHeader({
super.key,
required this.businessType,
required this.businesses,
required this.widescreen,
});
@override
State<BusinessHeader> createState() => _BusinessHeaderState();
}
class _BusinessHeaderState extends State<BusinessHeader> {
@override
Widget build(BuildContext context) {
return SliverStickyHeader(
header: Container(
height: 55.0,
color: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: _getHeaderRow(),
),
sliver: _getChildSliver(widget.businesses, widget.widescreen),
);
}
Widget _getHeaderRow() {
return Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: Icon(
getIconFromBusinessType(widget.businessType),
color: Theme.of(context).colorScheme.onPrimary,
)),
Text(getNameFromBusinessType(widget.businessType)),
],
);
}
Widget _getChildSliver(List<Business> businesses, bool widescreen) {
if (widescreen) {
return SliverPadding(
padding: const EdgeInsets.all(4),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisExtent: 250.0,
maxCrossAxisExtent: 400.0,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
),
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return _businessTile(
businesses[index],
widget.businessType,
);
},
),
),
);
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return _businessListItem(
businesses[index],
widget.businessType,
);
},
),
);
}
}
Widget _businessTile(Business business, BusinessType jobType) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(
id: business.id,
name: business.name!,
)));
},
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 48,
width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromBusinessType(business.type!),
size: 48);
}),
)),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.name!,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.description!,
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.link),
onPressed: () {
launchUrl(Uri.parse('https://${business.website}'));
},
),
IconButton(
icon: const Icon(Icons.location_on),
onPressed: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
if (business.contactPhone != null)
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.surface,
title:
Text('Contact ${business.contactName}'),
content: Text(
'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${business.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (business.contactEmail != null)
IconButton(
icon: const Icon(Icons.email),
onPressed: () {
launchUrl(
Uri.parse('mailto:${business.contactEmail}'));
},
),
],
)),
],
),
),
),
);
}
Widget _businessListItem(Business business, BusinessType? jobType) {
return Card(
child: ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(3.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromBusinessType(business.type!));
})),
title: Text(business.name!),
subtitle: Text(business.description!,
maxLines: 2, overflow: TextOverflow.ellipsis),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(
id: business.id,
name: business.name!,
)));
},
),
);
}
}

View File

@ -1,14 +1,13 @@
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/main.dart'; import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class CreateEditBusiness extends StatefulWidget { class CreateEditBusiness extends StatefulWidget {
final Business? inputBusiness; final Business? inputBusiness;
final JobType? clickFromType;
const CreateEditBusiness({super.key, this.inputBusiness, this.clickFromType}); const CreateEditBusiness({super.key, this.inputBusiness});
@override @override
State<CreateEditBusiness> createState() => _CreateEditBusinessState(); State<CreateEditBusiness> createState() => _CreateEditBusinessState();
@ -25,19 +24,23 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
late TextEditingController _locationNameController; late TextEditingController _locationNameController;
late TextEditingController _locationAddressController; late TextEditingController _locationAddressController;
// late TextEditingController _businessTypeController;
Business business = Business( Business business = Business(
id: 0, id: 0,
name: 'Business', name: 'Business',
description: 'Add details about the business below.', description: 'Add details about the business below.',
type: null,
website: '', website: '',
contactName: '', contactName: null,
contactEmail: '', contactEmail: null,
contactPhone: '', contactPhone: null,
notes: '', notes: null,
locationName: '', locationName: '',
locationAddress: '', locationAddress: null,
); );
bool _isLoading = false; bool _isLoading = false;
String? dropDownErrorText;
@override @override
void initState() { void initState() {
@ -47,11 +50,16 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
_nameController = TextEditingController(text: business.name); _nameController = TextEditingController(text: business.name);
_descriptionController = _descriptionController =
TextEditingController(text: business.description); TextEditingController(text: business.description);
business.type = widget.inputBusiness?.type;
} else { } else {
_nameController = TextEditingController(); _nameController = TextEditingController();
_descriptionController = TextEditingController(); _descriptionController = TextEditingController();
} }
_websiteController = TextEditingController(text: business.website); _websiteController = TextEditingController(
text: business.website!
.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''));
_contactNameController = TextEditingController(text: business.contactName); _contactNameController = TextEditingController(text: business.contactName);
_contactPhoneController = _contactPhoneController =
TextEditingController(text: business.contactPhone); TextEditingController(text: business.contactPhone);
@ -65,7 +73,6 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
} }
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final TextEditingController businessTypeController = TextEditingController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -91,15 +98,21 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
) )
: const Icon(Icons.save), : const Icon(Icons.save),
onPressed: () async { onPressed: () async {
if (business.type == null) {
setState(() {
dropDownErrorText = 'Business type is required';
});
formKey.currentState!.validate();
} else {
setState(() {
dropDownErrorText = null;
});
if (formKey.currentState!.validate()) { if (formKey.currentState!.validate()) {
formKey.currentState?.save(); formKey.currentState?.save();
setState(() { setState(() {
_isLoading = true; _isLoading = true;
}); });
String? result; String? result;
// if (business.contactName == '') {
// business.contactName = 'Contact ${business.name}';
// }
if (widget.inputBusiness != null) { if (widget.inputBusiness != null) {
result = await editBusiness(business); result = await editBusiness(business);
} else { } else {
@ -130,22 +143,23 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
), ),
); );
} }
}
}, },
), ),
body: ListView( body: ListView(
children: [ children: [
Center( Center(
child: SizedBox( child: SizedBox(
width: 1000, width: 800,
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(
title: Text(business.name, title: Text(business.name!,
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)), fontSize: 24, fontWeight: FontWeight.bold)),
subtitle: Text( subtitle: Text(
business.description, business.description!,
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
leading: ClipRRect( leading: ClipRRect(
@ -156,10 +170,11 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
'https://logo.clearbit.com/${business.website}', 'https://logo.clearbit.com/${business.website}',
errorBuilder: (BuildContext context, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) { Object exception, StackTrace? stackTrace) {
return getIconFromJobType( return Icon(
widget.clickFromType ?? JobType.other, getIconFromBusinessType(business.type != null
48, ? business.type!
Theme.of(context).colorScheme.onSurface); : BusinessType.other),
size: 48);
}), }),
), ),
), ),
@ -186,44 +201,13 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
labelText: 'Business Name (required)', labelText: 'Business Name (required)',
), ),
validator: (value) { validator: (value) {
if (value != null && value.isEmpty) { if (value != null && value.trim().isEmpty) {
return 'Name is required'; return 'Name is required';
} }
return null; return null;
}, },
), ),
), ),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _websiteController,
autovalidateMode:
AutovalidateMode.onUserInteraction,
keyboardType: TextInputType.url,
onChanged: (inputUrl) {
business.website = Uri.encodeFull(inputUrl);
if (!business.website.contains('http://') &&
!business.website
.contains('https://')) {
business.website =
'https://${business.website}';
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Website (required)',
),
validator: (value) {
if (value != null && value.isEmpty) {
return 'Website is required';
}
return null;
},
),
),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8.0, right: 8.0), left: 8.0, right: 8.0),
@ -246,13 +230,82 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
'Business Description (required)', 'Business Description (required)',
), ),
validator: (value) { validator: (value) {
if (value != null && value.isEmpty) { if (value != null && value.trim().isEmpty) {
return 'Description is required'; return 'Description is required';
} }
return null; return null;
}, },
), ),
), ),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _websiteController,
autovalidateMode:
AutovalidateMode.onUserInteraction,
keyboardType: TextInputType.url,
onChanged: (inputUrl) {
business.website = Uri.encodeFull(inputUrl);
if (!business.website!
.contains('http://') &&
!business.website!
.contains('https://')) {
business.website =
'https://${business.website}';
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Website (required)',
),
validator: (value) {
if (value != null &&
!RegExp(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:\/[^\/\s]*)*')
.hasMatch(value)) {
return 'Enter a valid Website';
}
if (value != null && value.trim().isEmpty) {
return 'Website is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text('Type of Business',
style: TextStyle(fontSize: 16)),
DropdownMenu<BusinessType>(
initialSelection: business.type,
label: const Text('Business Type'),
errorText: dropDownErrorText,
dropdownMenuEntries: [
for (BusinessType type
in BusinessType.values)
DropdownMenuEntry(
value: type,
label: getNameFromBusinessType(
type)),
],
onSelected: (inputType) {
setState(() {
business.type = inputType!;
dropDownErrorText = null;
});
},
),
],
),
),
// Padding( // Padding(
// padding: const EdgeInsets.only( // padding: const EdgeInsets.only(
// left: 8.0, right: 8.0, bottom: 16.0), // left: 8.0, right: 8.0, bottom: 16.0),
@ -325,43 +378,6 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
// ], // ],
// ), // ),
// ), // ),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _locationNameController,
onChanged: (inputName) {
setState(() {
business.locationName = inputName;
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Location Name',
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _locationAddressController,
onChanged: (inputAddr) {
setState(() {
business.locationAddress = inputAddr;
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Location Address',
),
),
),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0), left: 8.0, right: 8.0, bottom: 8.0),
@ -374,8 +390,17 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}, },
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Contact Information Name', labelText:
'Contact Information Name (required)',
), ),
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Contact name is required';
}
return null;
},
), ),
), ),
Padding( Padding(
@ -385,15 +410,31 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
controller: _contactPhoneController, controller: _contactPhoneController,
inputFormatters: [PhoneFormatter()], inputFormatters: [PhoneFormatter()],
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
onSaved: (inputText) { autovalidateMode:
business.contactPhone = inputText!; AutovalidateMode.onUserInteraction,
onChanged: (inputText) {
if (inputText.trim().isEmpty) {
business.contactPhone = null;
} else {
business.contactPhone = inputText.trim();
}
}, },
onTapOutside: (PointerDownEvent event) { onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}, },
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Contact Phone # (optional)', labelText: 'Contact Phone #',
), ),
validator: (value) {
if (business.contactEmail == null &&
(value == null || value.isEmpty)) {
return 'At least one contact method is required';
}
if (value != null && value.length != 14) {
return 'Enter a valid phone number';
}
return null;
},
), ),
), ),
Padding( Padding(
@ -402,9 +443,15 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
child: TextFormField( child: TextFormField(
controller: _contactEmailController, controller: _contactEmailController,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
onSaved: (inputText) { onChanged: (inputText) {
business.contactEmail = inputText!; if (inputText.trim().isEmpty) {
business.contactEmail = null;
} else {
business.contactEmail = inputText.trim();
}
}, },
autovalidateMode:
AutovalidateMode.onUserInteraction,
onTapOutside: (PointerDownEvent event) { onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}, },
@ -412,10 +459,16 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
labelText: 'Contact Email', labelText: 'Contact Email',
), ),
validator: (value) { validator: (value) {
value = value?.trim();
if (value != null && value.isEmpty) {
value = null;
}
if (value == null &&
business.contactPhone == null) {
return 'At least one contact method is required';
}
if (value != null) { if (value != null) {
if (value.isEmpty) { if (!RegExp(
return null;
} else if (!RegExp(
r'^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$') r'^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
.hasMatch(value)) { .hasMatch(value)) {
return 'Enter a valid Email'; return 'Enter a valid Email';
@ -427,6 +480,58 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
}, },
), ),
), ),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _locationNameController,
onChanged: (inputName) {
setState(() {
business.locationName = inputName.trim();
});
},
autovalidateMode:
AutovalidateMode.onUserInteraction,
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Location Name (required)',
),
validator: (value) {
if (value != null && value.trim().isEmpty) {
return 'Location name is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _locationAddressController,
onChanged: (inputAddr) {
setState(() {
business.locationAddress = inputAddr;
});
},
autovalidateMode:
AutovalidateMode.onUserInteraction,
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Location Address (required)',
),
validator: (value) {
if (value != null && value.trim().isEmpty) {
return 'Location Address is required';
}
return null;
},
),
),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0), left: 8.0, right: 8.0, bottom: 8.0),
@ -435,7 +540,12 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
maxLength: 300, maxLength: 300,
maxLines: null, maxLines: null,
onSaved: (inputText) { onSaved: (inputText) {
business.notes = inputText!; if (inputText == null ||
inputText.trim().isEmpty) {
business.notes = null;
} else {
business.notes = inputText.trim();
}
}, },
onTapOutside: (PointerDownEvent event) { onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();

View File

@ -1,15 +1,15 @@
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/main.dart'; import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rive/rive.dart'; import 'package:rive/rive.dart';
class CreateEditJobListing extends StatefulWidget { class CreateEditJobListing extends StatefulWidget {
final JobListing? inputJobListing; final JobListing? inputJobListing;
final Business inputBusiness; final Business? inputBusiness;
const CreateEditJobListing( const CreateEditJobListing(
{super.key, this.inputJobListing, required this.inputBusiness}); {super.key, this.inputJobListing, this.inputBusiness});
@override @override
State<CreateEditJobListing> createState() => _CreateEditJobListingState(); State<CreateEditJobListing> createState() => _CreateEditJobListingState();
@ -22,14 +22,15 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
late TextEditingController _wageController; late TextEditingController _wageController;
late TextEditingController _linkController; late TextEditingController _linkController;
List nameMapping = []; List nameMapping = [];
String? businessErrorText; String? typeDropdownErrorText;
String? businessDropdownErrorText;
JobListing listing = JobListing( JobListing listing = JobListing(
id: null, id: null,
businessId: null, businessId: null,
name: 'Job Listing', name: 'Job Listing',
description: 'Add details about the business below.', description: 'Add details about the business below.',
type: JobType.other, type: null,
wage: null, wage: null,
link: null); link: null);
bool _isLoading = false; bool _isLoading = false;
@ -46,17 +47,21 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
_descriptionController = TextEditingController(); _descriptionController = TextEditingController();
} }
_wageController = TextEditingController(text: listing.wage); _wageController = TextEditingController(text: listing.wage);
_linkController = TextEditingController(text: listing.link); _linkController = TextEditingController(
text: listing.link
?.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''));
getBusinessNameMapping = fetchBusinessNames(); getBusinessNameMapping = fetchBusinessNames();
} }
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final TextEditingController jobTypeController = TextEditingController();
final TextEditingController businessController = TextEditingController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
listing.businessId = widget.inputBusiness.id; if (widget.inputBusiness != null) {
listing.businessId = widget.inputBusiness!.id;
}
return PopScope( return PopScope(
canPop: !_isLoading, canPop: !_isLoading,
onPopInvoked: _handlePop, onPopInvoked: _handlePop,
@ -79,6 +84,24 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
) )
: const Icon(Icons.save), : const Icon(Icons.save),
onPressed: () async { onPressed: () async {
if (listing.type == null || listing.businessId == null) {
if (listing.type == null) {
setState(() {
typeDropdownErrorText = 'Job type is required';
});
formKey.currentState!.validate();
}
if (listing.businessId == null) {
setState(() {
businessDropdownErrorText = 'Business is required';
});
formKey.currentState!.validate();
}
} else {
setState(() {
typeDropdownErrorText = null;
businessDropdownErrorText = null;
});
if (formKey.currentState!.validate()) { if (formKey.currentState!.validate()) {
formKey.currentState?.save(); formKey.currentState?.save();
setState(() { setState(() {
@ -102,7 +125,9 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const MainApp())); builder: (context) => const MainApp(
initialPage: 1,
)));
} }
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -115,8 +140,8 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
), ),
); );
} }
}, }
), }),
body: FutureBuilder( body: FutureBuilder(
future: getBusinessNameMapping, future: getBusinessNameMapping,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -152,7 +177,7 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
children: [ children: [
Center( Center(
child: SizedBox( child: SizedBox(
width: 1000, width: 800,
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(
@ -176,12 +201,11 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
errorBuilder: (BuildContext context, errorBuilder: (BuildContext context,
Object exception, Object exception,
StackTrace? stackTrace) { StackTrace? stackTrace) {
return getIconFromJobType( return Icon(
listing.type, getIconFromJobType(
48, listing.type ?? JobType.other,
Theme.of(context) ),
.colorScheme size: 48);
.onSurface);
}), }),
), ),
), ),
@ -204,8 +228,9 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
TextStyle(fontSize: 16)), TextStyle(fontSize: 16)),
DropdownMenu<JobType>( DropdownMenu<JobType>(
initialSelection: listing.type, initialSelection: listing.type,
controller: jobTypeController,
label: const Text('Job Type'), label: const Text('Job Type'),
errorText:
typeDropdownErrorText,
dropdownMenuEntries: [ dropdownMenuEntries: [
for (JobType type for (JobType type
in JobType.values) in JobType.values)
@ -218,6 +243,8 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
onSelected: (inputType) { onSelected: (inputType) {
setState(() { setState(() {
listing.type = inputType!; listing.type = inputType!;
typeDropdownErrorText =
null;
}); });
}, },
), ),
@ -239,9 +266,10 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
style: style:
TextStyle(fontSize: 16)), TextStyle(fontSize: 16)),
DropdownMenu<int>( DropdownMenu<int>(
errorText:
businessDropdownErrorText,
initialSelection: initialSelection:
widget.inputBusiness.id, widget.inputBusiness?.id,
controller: businessController,
label: const Text('Business'), label: const Text('Business'),
dropdownMenuEntries: [ dropdownMenuEntries: [
for (Map<String, dynamic> map for (Map<String, dynamic> map
@ -254,6 +282,8 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
setState(() { setState(() {
listing.businessId = listing.businessId =
inputType!; inputType!;
businessDropdownErrorText =
null;
}); });
}, },
), ),
@ -353,8 +383,7 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
.onUserInteraction, .onUserInteraction,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
onChanged: (inputUrl) { onChanged: (inputUrl) {
if (listing.link != null && if (inputUrl != '') {
listing.link != '') {
listing.link = listing.link =
Uri.encodeFull(inputUrl); Uri.encodeFull(inputUrl);
if (!listing.link! if (!listing.link!
@ -365,6 +394,16 @@ class _CreateEditJobListingState extends State<CreateEditJobListing> {
'https://${listing.link}'; 'https://${listing.link}';
} }
} }
listing.link = null;
},
validator: (value) {
if (value != null &&
value.isNotEmpty &&
!RegExp(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:\/[^\/\s]*)*')
.hasMatch(value)) {
return 'Enter a valid Website';
}
return null;
}, },
onTapOutside: onTapOutside:
(PointerDownEvent event) { (PointerDownEvent event) {

View File

@ -1,8 +1,8 @@
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/main.dart'; import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/pages/create_edit_listing.dart'; import 'package:fbla_ui/pages/create_edit_listing.dart';
import 'package:fbla_ui/pages/signin_page.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -39,30 +39,43 @@ class _CreateBusinessDetailState extends State<JobListingDetail> {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
children: [ children: [
ListTile( Padding(
title: Text(listing.name, padding: const EdgeInsets.all(16.0),
textAlign: TextAlign.left, child: Row(
style: const TextStyle( crossAxisAlignment: CrossAxisAlignment.start,
fontSize: 24, fontWeight: FontWeight.bold)), children: [
subtitle: Text( Padding(
listing.description, padding: const EdgeInsets.only(right: 16.0),
textAlign: TextAlign.left, child: ClipRRect(
),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0), borderRadius: BorderRadius.circular(6.0),
child: Image.network( child: Image.network(
'$apiAddress/logos/${listing.businessId}', '$apiAddress/logos/${listing.businessId}',
width: 48, width: 48,
height: 48, errorBuilder: (BuildContext context, height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) { Object exception, StackTrace? stackTrace) {
return getIconFromJobType(listing.type, 48, return Icon(getIconFromJobType(listing.type!),
Theme.of(context).colorScheme.onSurface); size: 48);
}), }),
), ),
), ),
Visibility( Column(
visible: listing.link != null && listing.link != '', crossAxisAlignment: CrossAxisAlignment.start,
child: ListTile( children: [
Text(listing.name,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold)),
Text(widget.fromBusiness.name!,
style: const TextStyle(fontSize: 16)),
Text(
listing.description,
),
],
),
],
),
),
if (listing.link != null && listing.link != '')
ListTile(
leading: const Icon(Icons.link), leading: const Icon(Icons.link),
title: const Text('More Information'), title: const Text('More Information'),
subtitle: Text( subtitle: Text(
@ -75,7 +88,6 @@ class _CreateBusinessDetailState extends State<JobListingDetail> {
launchUrl(Uri.parse(listing.link!)); launchUrl(Uri.parse(listing.link!));
}, },
), ),
),
], ],
), ),
), ),
@ -108,9 +120,8 @@ class _CreateBusinessDetailState extends State<JobListingDetail> {
), ),
], ],
), ),
Visibility( if (widget.fromBusiness.contactPhone != null)
visible: widget.fromBusiness.contactPhone != null, ListTile(
child: ListTile(
leading: const Icon(Icons.phone), leading: const Icon(Icons.phone),
title: Text(widget.fromBusiness.contactPhone!), title: Text(widget.fromBusiness.contactPhone!),
// maybe replace ! with ?? ''. same is true for below // maybe replace ! with ?? ''. same is true for below
@ -145,13 +156,13 @@ class _CreateBusinessDetailState extends State<JobListingDetail> {
}); });
}, },
), ),
), if (widget.fromBusiness.contactEmail != null)
ListTile( ListTile(
leading: const Icon(Icons.email), leading: const Icon(Icons.email),
title: Text(widget.fromBusiness.contactEmail), title: Text(widget.fromBusiness.contactEmail!),
onTap: () { onTap: () {
launchUrl( launchUrl(Uri.parse(
Uri.parse('mailto:${widget.fromBusiness.contactEmail}')); 'mailto:${widget.fromBusiness.contactEmail}'));
}, },
), ),
], ],

View File

@ -0,0 +1,582 @@
import 'package:fbla_ui/pages/create_edit_listing.dart';
import 'package:fbla_ui/pages/listing_detail.dart';
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/export.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:fbla_ui/shared/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:rive/rive.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:url_launcher/url_launcher.dart';
class JobsOverview extends StatefulWidget {
final String searchQuery;
final Future refreshJobDataOverviewFuture;
final Future<void> Function(Set<JobType>) updateBusinessesCallback;
final void Function() themeCallback;
final void Function(bool) updateLoggedIn;
const JobsOverview({
super.key,
required this.searchQuery,
required this.refreshJobDataOverviewFuture,
required this.updateBusinessesCallback,
required this.themeCallback,
required this.updateLoggedIn,
});
@override
State<JobsOverview> createState() => _JobsOverviewState();
}
class _JobsOverviewState extends State<JobsOverview> {
bool _isPreviousData = false;
late Map<JobType, List<Business>> overviewBusinesses;
Set<JobType> jobTypeFilters = <JobType>{};
String searchQuery = '';
ScrollController controller = ScrollController();
bool _extended = true;
double prevPixelPosition = 0;
Map<JobType, List<Business>> _filterBySearch(
Map<JobType, List<Business>> businesses, String query) {
Map<JobType, List<Business>> filteredBusinesses = {};
for (JobType jobType in businesses.keys) {
filteredBusinesses[jobType] = List.from(businesses[jobType]!.where(
(element) => element.listings![0].name
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.contains(query
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
.toLowerCase()
.trim())));
}
filteredBusinesses.removeWhere((key, value) => value.isEmpty);
return filteredBusinesses;
}
void _setSearch(String search) async {
setState(() {
searchQuery = search;
});
}
void _setFilters(Set<JobType> filters) async {
jobTypeFilters = Set.from(filters);
widget.updateBusinessesCallback(jobTypeFilters);
}
void _scrollListener() {
if ((prevPixelPosition - controller.position.pixels).abs() > 10) {
setState(() {
_extended =
controller.position.userScrollDirection == ScrollDirection.forward;
});
}
prevPixelPosition = controller.position.pixels;
}
void _generatePDF() {
List<Business> allJobs = [];
for (List<Business> businesses
in _filterBySearch(overviewBusinesses, searchQuery).values) {
allJobs.addAll(businesses);
}
generatePDF(
context: context,
documentTypeIndex: 1,
selectedJobs: Set.from(allJobs));
}
@override
void initState() {
super.initState();
controller.addListener(_scrollListener);
}
@override
Widget build(BuildContext context) {
bool widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
return Scaffold(
floatingActionButton: _getFAB(widescreen),
body: CustomScrollView(
controller: controller,
slivers: [
MainSliverAppBar(
widescreen: widescreen,
setSearch: _setSearch,
searchHintText: 'Search Job Listings',
themeCallback: widget.themeCallback,
filterIconButton: _filterIconButton(
jobTypeFilters,
),
updateLoggedIn: widget.updateLoggedIn,
generatePDF: _generatePDF,
),
FutureBuilder(
future: widget.refreshJobDataOverviewFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType == String) {
_isPreviousData = false;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child: Text(snapshot.data,
textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () {
widget.updateBusinessesCallback(jobTypeFilters);
},
),
),
]),
));
}
overviewBusinesses = snapshot.data;
_isPreviousData = true;
return JobDisplayPanel(
jobGroupedBusinesses:
_filterBySearch(overviewBusinesses, searchQuery),
widescreen: widescreen,
);
} else if (snapshot.hasError) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
));
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
if (_isPreviousData) {
return JobDisplayPanel(
jobGroupedBusinesses:
_filterBySearch(overviewBusinesses, searchQuery),
widescreen: widescreen,
);
} else {
return SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child: RiveAnimation.asset(
'assets/mdev_triangle_loading.riv'),
),
));
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
}),
],
),
);
}
Widget _filterIconButton(Set<JobType> filters) {
Set<JobType> selectedChips = Set.from(filters);
return IconButton(
icon: Icon(
Icons.filter_list,
color: filters.isNotEmpty
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
void setDialogState(Set<JobType> newFilters) {
setState(() {
filters = newFilters;
});
}
List<Padding> chips = [];
for (var type in JobType.values) {
chips.add(Padding(
padding: const EdgeInsets.all(4),
child: FilterChip(
showCheckmark: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
label: Text(getNameFromJobType(type)),
selected: selectedChips.contains(type),
onSelected: (bool selected) {
if (selected) {
selectedChips.add(type);
} else {
selectedChips.remove(type);
}
setDialogState(filters);
}),
));
}
return AlertDialog(
title: const Text('Filter Options'),
content: SizedBox(
width: 400,
child: Wrap(
children: chips,
),
),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
_setFilters(<JobType>{});
// selectedChips = <BusinessType>{};
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Cancel'),
onPressed: () {
// selectedChips = Set.from(filters);
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Apply'),
onPressed: () {
_setFilters(selectedChips);
Navigator.of(context).pop();
},
)
],
);
});
});
});
}
Widget? _getFAB(bool widescreen) {
if (!widescreen && loggedIn) {
return FloatingActionButton.extended(
extendedIconLabelSpacing: _extended ? 8.0 : 0,
extendedPadding: const EdgeInsets.symmetric(horizontal: 16),
icon: const Icon(Icons.add),
label: AnimatedSize(
curve: Easing.standard,
duration: const Duration(milliseconds: 300),
child: _extended ? const Text('Add Job Listing') : Container(),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CreateEditJobListing()));
},
);
}
return null;
}
}
class JobDisplayPanel extends StatefulWidget {
final Map<JobType, List<Business>> jobGroupedBusinesses;
final bool widescreen;
const JobDisplayPanel({
super.key,
required this.jobGroupedBusinesses,
required this.widescreen,
});
@override
State<JobDisplayPanel> createState() => _JobDisplayPanelState();
}
class _JobDisplayPanelState extends State<JobDisplayPanel> {
@override
Widget build(BuildContext context) {
if (widget.jobGroupedBusinesses.keys.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'No results found!\nPlease change your search filters.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
),
),
);
}
List<BusinessHeader> headers = [];
for (JobType jobType in widget.jobGroupedBusinesses.keys) {
headers.add(BusinessHeader(
jobType: jobType,
widescreen: widget.widescreen,
businesses: widget.jobGroupedBusinesses[jobType]!));
}
headers.sort((a, b) => a.jobType.index.compareTo(b.jobType.index));
return MultiSliver(children: headers);
}
}
class BusinessHeader extends StatefulWidget {
final JobType jobType;
final List<Business> businesses;
final bool widescreen;
const BusinessHeader({
super.key,
required this.jobType,
required this.businesses,
required this.widescreen,
});
@override
State<BusinessHeader> createState() => _BusinessHeaderState();
}
class _BusinessHeaderState extends State<BusinessHeader> {
@override
Widget build(BuildContext context) {
return SliverStickyHeader(
header: Container(
height: 55.0,
color: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: _getHeaderRow(),
),
sliver: _getChildSliver(widget.businesses, widget.widescreen),
);
}
Widget _getHeaderRow() {
return Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: Icon(
getIconFromJobType(widget.jobType),
color: Theme.of(context).colorScheme.onPrimary,
)),
Text(getNameFromJobType(widget.jobType)),
],
);
}
Widget _getChildSliver(List<Business> businesses, bool widescreen) {
if (widescreen) {
return SliverPadding(
padding: const EdgeInsets.all(4),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisExtent: 250.0,
maxCrossAxisExtent: 400.0,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
),
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return _businessTile(
businesses[index],
widget.jobType,
);
},
),
),
);
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return _businessListItem(
businesses[index],
widget.jobType,
);
},
),
);
}
}
Widget _businessTile(Business business, JobType jobType) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => JobListingDetail(
listing: business.listings![0],
fromBusiness: business,
)));
},
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 48,
width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromBusinessType(business.type!),
size: 48);
}),
)),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.listings![0].name,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.listings![0].description,
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (business.listings![0].link != null &&
business.listings![0].link!.isNotEmpty)
IconButton(
icon: const Icon(Icons.link),
onPressed: () {
launchUrl(Uri.parse(
'https://${business.listings![0].link!}'));
},
),
IconButton(
icon: const Icon(Icons.location_on),
onPressed: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
if (business.contactPhone != null)
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.surface,
title:
Text('Contact ${business.contactName}'),
content: Text(
'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${business.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (business.contactEmail != null)
IconButton(
icon: const Icon(Icons.email),
onPressed: () {
launchUrl(
Uri.parse('mailto:${business.contactEmail}'));
},
),
],
)),
],
),
),
),
);
}
Widget _businessListItem(Business business, JobType? jobType) {
return Card(
child: ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(3.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return Icon(getIconFromBusinessType(business.type!));
})),
title: Text(business.listings![0].name),
subtitle: Text(business.listings![0].description,
maxLines: 2, overflow: TextOverflow.ellipsis),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => JobListingDetail(
listing: business.listings![0],
fromBusiness: business,
)));
},
),
);
}
}

View File

@ -1,12 +1,10 @@
import 'package:fbla_ui/api_logic.dart'; import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared.dart'; import 'package:fbla_ui/shared/global_vars.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
bool loggedIn = false;
class SignInPage extends StatefulWidget { class SignInPage extends StatefulWidget {
final void Function() refreshAccount; final void Function(bool) refreshAccount;
const SignInPage({super.key, required this.refreshAccount}); const SignInPage({super.key, required this.refreshAccount});
@ -96,8 +94,7 @@ class _SignInPageState extends State<SignInPage> {
await prefs.setString('username', username); await prefs.setString('username', username);
await prefs.setString('password', password); await prefs.setString('password', password);
await prefs.setBool('rememberMe', rememberMe); await prefs.setBool('rememberMe', rememberMe);
loggedIn = true; widget.refreshAccount(true);
widget.refreshAccount();
Navigator.of(context).pop(); Navigator.of(context).pop();
} else { } else {
setState(() { setState(() {
@ -182,8 +179,7 @@ class _SignInPageState extends State<SignInPage> {
await prefs.setString('username', username); await prefs.setString('username', username);
await prefs.setString('password', password); await prefs.setString('password', password);
await prefs.setBool('rememberMe', rememberMe); await prefs.setBool('rememberMe', rememberMe);
loggedIn = true; widget.refreshAccount(true);
widget.refreshAccount();
Navigator.of(context).pop(); Navigator.of(context).pop();
} else { } else {
setState(() { setState(() {

View File

@ -73,6 +73,8 @@ flutter:
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
assets: assets:
- assets/mdev_triangle_loading.riv - assets/mdev_triangle_loading.riv
- assets/MarinoDev.svg
- assets/Triangle256.png
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg