diff --git a/fbla-api/lib/create_first_user.dart b/fbla-api/lib/create_first_user.dart index ce5124d..85eb416 100644 --- a/fbla-api/lib/create_first_user.dart +++ b/fbla-api/lib/create_first_user.dart @@ -1,14 +1,17 @@ +import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; + import 'package:argon2/argon2.dart'; -import 'dart:io'; import 'package:postgres/postgres.dart'; +// Set these to the desired username and password of your user String username = 'admin'; -String password = 'password'; +String password = 'adminPassword'; var r = Random.secure(); -String randomSalt = String.fromCharCodes(List.generate(32, (index) => r.nextInt(33) + 89)); +String randomSalt = + String.fromCharCodes(List.generate(32, (index) => r.nextInt(33) + 89)); final salt = randomSalt.toBytesLatin1(); var parameters = Argon2Parameters( @@ -37,10 +40,8 @@ Future main() async { argon2.generateBytes(passwordBytes, result); var resultHex = result.toHexString(); - postgres.query( - ''' + postgres.query(''' INSERT INTO public.users (username, password_hash, salt) VALUES ('$username', '$resultHex', '$randomSalt') - ''' - ); + '''); } diff --git a/fbla-api/lib/fbla_api.dart b/fbla-api/lib/fbla_api.dart index 2424a61..bc71659 100644 --- a/fbla-api/lib/fbla_api.dart +++ b/fbla-api/lib/fbla_api.dart @@ -24,6 +24,8 @@ enum BusinessType { enum JobType { cashier, server, mechanic, other } +enum OfferType { job, internship, apprenticeship } + class Business { int id; String name; @@ -84,6 +86,7 @@ class JobListing { JobType type; String? wage; String? link; + OfferType offerType; JobListing( {this.id, @@ -92,7 +95,8 @@ class JobListing { required this.description, required this.type, this.wage, - this.link}); + this.link, + required this.offerType}); factory JobListing.fromJson(Map json) { bool typeValid = true; @@ -103,14 +107,14 @@ class JobListing { } return JobListing( - id: json['id'], - businessId: json['businessId'], - name: json['name'], - description: json['description'], - type: typeValid ? JobType.values.byName(json['type']) : JobType.other, - wage: json['wage'], - link: json['link'], - ); + id: json['id'], + businessId: json['businessId'], + name: json['name'], + description: json['description'], + type: typeValid ? JobType.values.byName(json['type']) : JobType.other, + wage: json['wage'], + link: json['link'], + offerType: OfferType.values.byName(json['offerType'])); } } @@ -166,12 +170,15 @@ void main() async { app.get('/fbla-api/businessdata/overview/jobs', (Request request) async { print('business overview request received'); - var filters = request.url.queryParameters['filters']?.split(',') ?? + var typeFilters = request.url.queryParameters['typeFilters']?.split(',') ?? JobType.values.asNameMap().keys; + var offerFilters = + request.url.queryParameters['offerFilters']?.split(',') ?? + OfferType.values.asNameMap().keys; Map output = {}; - for (int i = 0; i < filters.length; i++) { + for (int i = 0; i < typeFilters.length; i++) { var postgresResult = (await postgres.query(''' SELECT json_agg( json_build_object( @@ -181,6 +188,7 @@ void main() async { 'contactEmail', b."contactEmail", 'contactPhone', b."contactPhone", 'locationName', b."locationName", + 'locationAddress', b."locationAddress", 'listings', ( SELECT json_agg( json_build_object( @@ -188,22 +196,23 @@ void main() async { 'name', l.name, 'description', l.description, 'type', l.type, + 'offerType', l."offerType", 'wage', l.wage, 'link', l.link ) ) FROM listings l - WHERE l."businessId" = b.id AND l.type = '${filters.elementAt(i)}' + WHERE l."businessId" = b.id AND l.type = '${typeFilters.elementAt(i)}' AND l."offerType" IN (${offerFilters.map((element) => "'$element'").join(',')}) ) ) ) FROM businesses b - WHERE b.id IN (SELECT "businessId" FROM public.listings WHERE type='${filters.elementAt(i)}') + WHERE b.id IN (SELECT "businessId" FROM public.listings WHERE type='${typeFilters.elementAt(i)}' AND "offerType" IN (${offerFilters.map((element) => "'$element'").join(',')})) GROUP BY b.id; ''')); if (postgresResult.isNotEmpty) { - output.addAll({filters.elementAt(i): postgresResult[0][0]}); + output.addAll({typeFilters.elementAt(i): postgresResult[0][0]}); } } @@ -234,7 +243,8 @@ void main() async { 'website', website, 'contactEmail', "contactEmail", 'contactPhone', "contactPhone", - 'locationName', "locationName" + 'locationName', "locationName", + 'locationAddress', "locationAddress" ) ) FROM public.businesses WHERE type='${filters.elementAt(i)}' '''))[0][0]; @@ -302,7 +312,8 @@ void main() async { 'description', l.description, 'type', l.type, 'wage', l.wage, - 'link', l.link + 'link', l.link, + 'offerType', l."offerType" ) ) END @@ -348,25 +359,11 @@ void main() async { 'contactPhone', b."contactPhone", 'notes', b.notes, 'locationName', b."locationName", - 'locationAddress', b."locationAddress", - 'listings', CASE - WHEN COUNT(l.id) = 0 THEN 'null' - ELSE json_agg( - json_build_object( - 'id', l.id, - 'businessId', l."businessId", - 'name', l.name, - 'description', l.description, - 'type', l.type, - 'wage', l.wage, - 'link', l.link - ) - ) - END + 'locationAddress', b."locationAddress" ) FROM businesses b LEFT JOIN listings l ON b.id = l."businessId" - WHERE b.id IN ${'$filters'.replaceAll('[', '(').replaceAll(']', ')')} + WHERE b.id IN (${filters.join(',')}) GROUP BY b.id; ''')); @@ -413,15 +410,25 @@ void main() async { print('business logo request received'); var logo = File('logos/$logoId.png'); - List content = logo.readAsBytesSync(); - - return Response.ok( - content, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'image/png' - }, - ); + try { + List content = logo.readAsBytesSync(); + return Response.ok( + content, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'image/png' + }, + ); + } catch (e) { + print('Error reading logo!'); + return Response.notFound( + 'logo not found', + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'image/png' + }, + ); + } }); app.post('/fbla-api/createbusiness', (Request request) async { print('create business request received'); @@ -475,8 +482,8 @@ void main() async { JobListing listing = JobListing.fromJson(json); await postgres.query(''' - 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'}') + INSERT INTO listings ("businessId", name, description, type, wage, link, "offerType") + VALUES ('${listing.businessId}', '${listing.name.replaceAll("'", "''")}', '${listing.description.replaceAll("'", "''")}', '${listing.type.name}', '${listing.wage ?? 'NULL'}', '${listing.link?.replaceAll("'", "''") ?? 'NULL'}', '${listing.offerType.name}') ''' .replaceAll("'null'", 'NULL')); @@ -619,7 +626,7 @@ void main() async { await postgres.query(''' 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, "offerType"='${listing.offerType.name}'::text WHERE id = ${listing.id}; ''' .replaceAll("'null'", 'NULL')); diff --git a/fbla_ui/analysis_options.yaml b/fbla_ui/analysis_options.yaml index 1caa1fa..0b7e083 100644 --- a/fbla_ui/analysis_options.yaml +++ b/fbla_ui/analysis_options.yaml @@ -7,6 +7,9 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + errors: + use_build_context_synchronously: ignore include: package:flutter_lints/flutter.yaml linter: diff --git a/fbla_ui/lib/home.dart b/fbla_ui/lib/home.dart index e4ac913..27f8f12 100644 --- a/fbla_ui/lib/home.dart +++ b/fbla_ui/lib/home.dart @@ -20,6 +20,7 @@ class Home extends StatefulWidget { class _HomeState extends State { Set jobTypeFilters = {}; + Set offerTypeFilters = {}; Set businessTypeFilters = {}; String searchQuery = ''; late Future refreshBusinessDataOverviewJobFuture; @@ -65,12 +66,16 @@ class _HomeState extends State { } Future _updateOverviewBusinessesJobsCallback( - Set? newFilters) async { - if (newFilters != null) { - jobTypeFilters = Set.from(newFilters); + Set? newJobTypeFilters, + Set? newOfferTypeFilters) async { + if (newJobTypeFilters != null) { + jobTypeFilters = Set.from(newJobTypeFilters); } - var refreshedData = - fetchBusinessDataOverviewJobs(typeFilters: jobTypeFilters.toList()); + if (newOfferTypeFilters != null) { + offerTypeFilters = Set.from(newOfferTypeFilters); + } + var refreshedData = fetchBusinessDataOverviewJobs( + typeFilters: jobTypeFilters, offerFilters: offerTypeFilters); await refreshedData; setState(() { refreshBusinessDataOverviewJobFuture = refreshedData; @@ -100,7 +105,7 @@ class _HomeState extends State { body: RefreshIndicator( edgeOffset: 145, onRefresh: () async { - _updateOverviewBusinessesJobsCallback(null); + _updateOverviewBusinessesJobsCallback(null, null); _updateOverviewBusinessesBusinessCallback(null); }, child: widescreen @@ -186,7 +191,6 @@ class _HomeState extends State { children: [ NavigationRail( selectedIndex: currentPageIndex, - groupAlignment: -1, indicatorColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), trailing: Expanded( @@ -219,7 +223,6 @@ class _HomeState extends State { ), if (loggedIn) FloatingActionButton( - child: Icon(Icons.add), heroTag: 'Homepage', onPressed: () { if (currentPageIndex == 0) { @@ -236,6 +239,7 @@ class _HomeState extends State { const CreateEditJobListing())); } }, + child: const Icon(Icons.add), ) ], ), @@ -314,7 +318,7 @@ class _ContentPane extends StatelessWidget { updateOverviewBusinessesBusinessCallback; final void Function() themeCallback; final Future refreshBusinessDataOverviewJobFuture; - final Future Function(Set) + final Future Function(Set?, Set?) updateOverviewBusinessesJobsCallback; final int currentPageIndex; final void Function(bool) updateLoggedIn; diff --git a/fbla_ui/lib/main.dart b/fbla_ui/lib/main.dart index 4ae9554..f5bb597 100644 --- a/fbla_ui/lib/main.dart +++ b/fbla_ui/lib/main.dart @@ -69,32 +69,53 @@ class _MainAppState extends State { title: 'Job Link', themeMode: themeMode, darkTheme: ThemeData( + scaffoldBackgroundColor: const Color(0xFF121212), colorScheme: ColorScheme.dark( brightness: Brightness.dark, primary: Colors.blue.shade700, onPrimary: Colors.white, secondary: Colors.blue.shade900, surface: const Color.fromARGB(255, 31, 31, 31), - surfaceContainer: const Color.fromARGB(255, 40, 40, 40), + surfaceContainer: const Color.fromARGB(255, 46, 46, 46), tertiary: Colors.green.shade900, ), iconTheme: const IconThemeData(color: Colors.white), - inputDecorationTheme: const InputDecorationTheme(), useMaterial3: true, + inputDecorationTheme: InputDecorationTheme( + // border: OutlineInputBorder(), + filled: true, + fillColor: Colors.grey.withOpacity(0.1), + labelStyle: const TextStyle(color: Colors.grey), + ), + dropdownMenuTheme: const DropdownMenuThemeData( + inputDecorationTheme: InputDecorationTheme( + filled: true, + ), + ), ), theme: ThemeData( + scaffoldBackgroundColor: Colors.grey.shade300, colorScheme: ColorScheme.light( brightness: Brightness.light, primary: Colors.blue.shade700, onPrimary: Colors.white, - secondary: Colors.blue.shade200, - surface: Colors.grey.shade200, - surfaceContainer: Colors.grey.shade300, + secondary: Colors.blue.shade300, + surface: Colors.grey.shade100, + surfaceContainer: Colors.grey.shade200, tertiary: Colors.green, ), iconTheme: const IconThemeData(color: Colors.black), - inputDecorationTheme: - const InputDecorationTheme(border: UnderlineInputBorder()), + inputDecorationTheme: InputDecorationTheme( + // border: OutlineInputBorder(), + filled: true, + fillColor: Colors.blue.withOpacity(0.1), + labelStyle: const TextStyle(color: Colors.grey), + ), + dropdownMenuTheme: const DropdownMenuThemeData( + inputDecorationTheme: InputDecorationTheme( + filled: true, + ), + ), useMaterial3: true, ), home: Home(themeCallback: _switchTheme, initialPage: widget.initialPage), diff --git a/fbla_ui/lib/pages/business_detail.dart b/fbla_ui/lib/pages/business_detail.dart index 83fa969..34ffb7d 100644 --- a/fbla_ui/lib/pages/business_detail.dart +++ b/fbla_ui/lib/pages/business_detail.dart @@ -105,154 +105,174 @@ class _CreateBusinessDetailState extends State { }); } - ListView _detailBody(Business business) { + Widget _detailBody(Business business) { return ListView( children: [ // Title, logo, desc, website - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Card( - clipBehavior: Clip.antiAlias, + Center( + child: SizedBox( + width: 800, child: Column( children: [ - ListTile( - title: Text(business.name!, - textAlign: TextAlign.left, - style: const TextStyle( - fontSize: 24, fontWeight: FontWeight.bold)), - subtitle: Text( - business.description!, - textAlign: TextAlign.left, - ), - leading: ClipRRect( - borderRadius: BorderRadius.circular(6.0), - child: Image.network('$apiAddress/logos/${business.id}', - width: 48, - height: 48, errorBuilder: (BuildContext context, - Object exception, StackTrace? stackTrace) { - return Icon(getIconFromBusinessType(business.type!), - size: 48); - }), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Card( + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + ListTile( + titleAlignment: ListTileTitleAlignment.titleHeight, + title: Text(business.name!, + style: const TextStyle( + fontSize: 24, fontWeight: FontWeight.bold)), + subtitle: Text( + business.description!, + ), + contentPadding: + const EdgeInsets.only(bottom: 8, left: 16), + leading: ClipRRect( + borderRadius: BorderRadius.circular(6.0), + child: Image.network( + '$apiAddress/logos/${business.id}', + width: 48, + height: 48, errorBuilder: (BuildContext context, + Object exception, StackTrace? stackTrace) { + return Icon( + getIconFromBusinessType( + business.type ?? BusinessType.other), + size: 48); + }), + ), + ), + if (business.website != null) + ListTile( + leading: const Icon(Icons.link), + title: const Text('Website'), + subtitle: Text( + business.website! + .replaceAll('https://', '') + .replaceAll('http://', '') + .replaceAll('www.', ''), + style: const TextStyle(color: Colors.blue)), + onTap: () { + launchUrl( + Uri.parse('https://${business.website}')); + }, + ), + ], + ), ), ), - ListTile( - leading: const Icon(Icons.link), - title: const Text('Website'), - subtitle: Text( - business.website! - .replaceAll('https://', '') - .replaceAll('http://', '') - .replaceAll('www.', ''), - style: const TextStyle(color: Colors.blue)), - onTap: () { - launchUrl(Uri.parse(business.website!)); - }, + // Available positions + if (business.listings != null) + Card( + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, top: 4), + child: _GetListingsTitle(business)), + _JobList(business: business) + ]), + ), + // Contact info + Card( + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + Row( + children: [ + Padding( + padding: + const EdgeInsets.only(left: 16.0, top: 8.0), + child: Text( + business.contactName!, + textAlign: TextAlign.left, + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ], + ), + if (business.contactPhone != null) + ListTile( + leading: const Icon(Icons.phone), + title: Text(business.contactPhone!), + // maybe replace ! with ?? ''. same is true for below + onTap: () { + 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) + ListTile( + leading: const Icon(Icons.email), + title: Text(business.contactEmail!), + onTap: () { + launchUrl( + Uri.parse('mailto:${business.contactEmail}')); + }, + ), + ], + ), ), + // Location + Card( + clipBehavior: Clip.antiAlias, + child: ListTile( + leading: const Icon(Icons.location_on), + title: Text(business.locationName), + subtitle: Text(business.locationAddress!), + onTap: () { + launchUrl(Uri.parse(Uri.encodeFull( + 'https://www.google.com/maps/search/?api=1&query=${business.locationName} ${business.locationAddress}'))); + }, + ), + ), + // Notes + if (business.notes != null && business.notes != '') + Card( + child: ListTile( + leading: const Icon(Icons.notes), + title: const Text( + 'Additional Notes', + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + subtitle: Text(business.notes!), + ), + ), ], ), ), ), - // Available positions - if (business.listings != null) - Card( - clipBehavior: Clip.antiAlias, - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(left: 16, top: 4), - child: _GetListingsTitle(business)), - _JobList(business: business) - ]), - ), - // Contact info - Card( - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: Text( - business.contactName!, - textAlign: TextAlign.left, - style: const TextStyle( - fontSize: 20, fontWeight: FontWeight.bold), - ), - ), - ], - ), - if (business.contactPhone != null) - ListTile( - leading: const Icon(Icons.phone), - title: Text(business.contactPhone!), - // maybe replace ! with ?? ''. same is true for below - onTap: () { - 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) - ListTile( - leading: const Icon(Icons.email), - title: Text(business.contactEmail!), - onTap: () { - launchUrl(Uri.parse('mailto:${business.contactEmail}')); - }, - ), - ], - ), - ), - // Location - Card( - clipBehavior: Clip.antiAlias, - child: ListTile( - leading: const Icon(Icons.location_on), - title: Text(business.locationName), - subtitle: Text(business.locationAddress!), - onTap: () { - launchUrl(Uri.parse(Uri.encodeFull( - 'https://www.google.com/maps/search/?api=1&query=${business.locationName}'))); - }, - ), - ), - // Notes - if (business.notes != null && business.notes != '') - Card( - child: ListTile( - leading: const Icon(Icons.notes), - title: const Text( - 'Additional Notes', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - subtitle: Text(business.notes!), - ), - ), ], ); } diff --git a/fbla_ui/lib/pages/businesses_overview.dart b/fbla_ui/lib/pages/businesses_overview.dart index 9820af9..7560d12 100644 --- a/fbla_ui/lib/pages/businesses_overview.dart +++ b/fbla_ui/lib/pages/businesses_overview.dart @@ -213,6 +213,7 @@ class _BusinessesOverviewState extends State { : Theme.of(context).colorScheme.onSurface, ), onPressed: () { + selectedChips = Set.from(businessTypeFilters); showDialog( context: context, builder: (BuildContext context) { @@ -338,9 +339,9 @@ class _BusinessDisplayPanelState extends State { ); } - List headers = []; + List<_BusinessHeader> headers = []; for (BusinessType businessType in widget.groupedBusinesses.keys) { - headers.add(BusinessHeader( + headers.add(_BusinessHeader( businessType: businessType, widescreen: widget.widescreen, businesses: widget.groupedBusinesses[businessType]!)); @@ -351,23 +352,22 @@ class _BusinessDisplayPanelState extends State { } } -class BusinessHeader extends StatefulWidget { +class _BusinessHeader extends StatefulWidget { final BusinessType businessType; final List businesses; final bool widescreen; - const BusinessHeader({ - super.key, + const _BusinessHeader({ required this.businessType, required this.businesses, required this.widescreen, }); @override - State createState() => _BusinessHeaderState(); + State<_BusinessHeader> createState() => _BusinessHeaderState(); } -class _BusinessHeaderState extends State { +class _BusinessHeaderState extends State<_BusinessHeader> { @override Widget build(BuildContext context) { return SliverStickyHeader( @@ -391,7 +391,8 @@ class _BusinessHeaderState extends State { getIconFromBusinessType(widget.businessType), color: Theme.of(context).colorScheme.onPrimary, )), - Text(getNameFromBusinessType(widget.businessType)), + Text(getNameFromBusinessType(widget.businessType), + style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)), ], ); } @@ -491,17 +492,18 @@ class _BusinessHeaderState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - IconButton( - icon: const Icon(Icons.link), - onPressed: () { - launchUrl(Uri.parse('https://${business.website}')); - }, - ), + if (business.website != null) + 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}'))); + 'https://www.google.com/maps/search/?api=1&query=${business.locationName} ${business.locationAddress}'))); }, ), if (business.contactPhone != null) diff --git a/fbla_ui/lib/pages/create_edit_business.dart b/fbla_ui/lib/pages/create_edit_business.dart index b7a443c..112f4be 100644 --- a/fbla_ui/lib/pages/create_edit_business.dart +++ b/fbla_ui/lib/pages/create_edit_business.dart @@ -4,6 +4,8 @@ import 'package:fbla_ui/shared/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../shared/global_vars.dart'; + class CreateEditBusiness extends StatefulWidget { final Business? inputBusiness; @@ -23,15 +25,14 @@ class _CreateEditBusinessState extends State { late TextEditingController _notesController; late TextEditingController _locationNameController; late TextEditingController _locationAddressController; - - // late TextEditingController _businessTypeController; + late bool widescreen; Business business = Business( id: 0, name: 'Business', description: 'Add details about the business below.', type: null, - website: '', + website: null, contactName: null, contactEmail: null, contactPhone: null, @@ -56,8 +57,8 @@ class _CreateEditBusinessState extends State { _descriptionController = TextEditingController(); } _websiteController = TextEditingController( - text: business.website! - .replaceAll('https://', '') + text: business.website + ?.replaceAll('https://', '') .replaceAll('http://', '') .replaceAll('www.', '')); _contactNameController = TextEditingController(text: business.contactName); @@ -76,6 +77,7 @@ class _CreateEditBusinessState extends State { @override Widget build(BuildContext context) { + widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth; return PopScope( canPop: !_isLoading, onPopInvoked: _handlePop, @@ -87,65 +89,23 @@ class _CreateEditBusinessState extends State { ? Text('Edit ${widget.inputBusiness?.name}', maxLines: 1) : const Text('Add New Business'), ), - floatingActionButton: FloatingActionButton( - child: _isLoading - ? const Padding( - padding: EdgeInsets.all(16.0), - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 3.0, - ), - ) - : const Icon(Icons.save), - onPressed: () async { - if (business.type == null) { - setState(() { - dropDownErrorText = 'Business type is required'; - }); - formKey.currentState!.validate(); - } else { - setState(() { - dropDownErrorText = null; - }); - if (formKey.currentState!.validate()) { - formKey.currentState?.save(); - setState(() { - _isLoading = true; - }); - String? result; - if (widget.inputBusiness != null) { - result = await editBusiness(business); - } else { - result = await createBusiness(business); - } - setState(() { - _isLoading = false; - }); - if (result != null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - width: 400, - behavior: SnackBarBehavior.floating, - content: Text(result))); - } else { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => const MainApp())); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Check field inputs!'), - width: 200, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - ), - ); - } - } - }, - ), + floatingActionButton: !widescreen + ? FloatingActionButton.extended( + label: const Text('Save'), + icon: _isLoading + ? const Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 3.0, + ), + ) + : const Icon(Icons.save), + onPressed: () async { + await _saveBusiness(context); + }, + ) + : null, body: ListView( children: [ Center( @@ -154,26 +114,25 @@ class _CreateEditBusinessState extends State { child: Column( children: [ ListTile( + titleAlignment: ListTileTitleAlignment.titleHeight, title: Text(business.name!, - textAlign: TextAlign.left, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold)), subtitle: Text( business.description!, - textAlign: TextAlign.left, ), + contentPadding: + const EdgeInsets.only(bottom: 8, left: 16), leading: ClipRRect( borderRadius: BorderRadius.circular(6.0), child: Image.network( + '$apiAddress/logos/${business.id}', width: 48, - height: 48, - 'https://logo.clearbit.com/${business.website}', - errorBuilder: (BuildContext context, + height: 48, errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Icon( - getIconFromBusinessType(business.type != null - ? business.type! - : BusinessType.other), + getIconFromBusinessType( + business.type ?? BusinessType.other), size: 48); }), ), @@ -183,7 +142,10 @@ class _CreateEditBusinessState extends State { children: [ Padding( padding: const EdgeInsets.only( - left: 8.0, right: 8.0), + top: 8.0, + bottom: 8.0, + left: 8.0, + right: 8.0), child: TextFormField( controller: _nameController, autovalidateMode: @@ -210,7 +172,7 @@ class _CreateEditBusinessState extends State { ), Padding( padding: const EdgeInsets.only( - left: 8.0, right: 8.0), + bottom: 8.0, left: 8.0, right: 8.0), child: TextFormField( controller: _descriptionController, autovalidateMode: @@ -247,62 +209,59 @@ class _CreateEditBusinessState extends State { keyboardType: TextInputType.url, onChanged: (inputUrl) { business.website = Uri.encodeFull(inputUrl); - if (!business.website! - .contains('http://') && - !business.website! - .contains('https://')) { - business.website = - 'https://${business.website}'; + if (inputUrl.trim().isEmpty) { + business.website = null; + } else { + if (!business.website! + .contains('http://') && + !business.website! + .contains('https://')) { + business.website = + 'https://${business.website!.trim()}'; + } } }, onTapOutside: (PointerDownEvent event) { FocusScope.of(context).unfocus(); }, decoration: const InputDecoration( - labelText: 'Website (required)', + labelText: 'Website', ), 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]*)*') + !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( - 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; - }); - }, - ), - ], + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 8.0, right: 8.0, bottom: 16.0), + child: DropdownMenu( + initialSelection: business.type, + // width: 776, + 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; + }); + }, + ), ), ), @@ -380,7 +339,7 @@ class _CreateEditBusinessState extends State { // ), Padding( padding: const EdgeInsets.only( - left: 8.0, right: 8.0, bottom: 8.0), + left: 8.0, right: 8.0, bottom: 16.0), child: TextFormField( controller: _contactNameController, onSaved: (inputText) { @@ -405,7 +364,7 @@ class _CreateEditBusinessState extends State { ), Padding( padding: const EdgeInsets.only( - left: 8.0, right: 8.0, bottom: 8.0), + left: 8.0, right: 8.0, bottom: 16.0), child: TextFormField( controller: _contactPhoneController, inputFormatters: [PhoneFormatter()], @@ -439,7 +398,7 @@ class _CreateEditBusinessState extends State { ), Padding( padding: const EdgeInsets.only( - left: 8.0, right: 8.0, bottom: 8.0), + left: 8.0, right: 8.0, bottom: 16.0), child: TextFormField( controller: _contactEmailController, keyboardType: TextInputType.emailAddress, @@ -482,7 +441,7 @@ class _CreateEditBusinessState extends State { ), Padding( padding: const EdgeInsets.only( - left: 8.0, right: 8.0, bottom: 8.0), + left: 8.0, right: 8.0, bottom: 16.0), child: TextFormField( controller: _locationNameController, onChanged: (inputName) { @@ -558,9 +517,33 @@ class _CreateEditBusinessState extends State { ], ), ), - SizedBox( - height: 75, - ) + if (!widescreen) + const SizedBox( + height: 75, + ) + else + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: FilledButton( + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.only( + top: 8.0, right: 8.0, bottom: 8.0), + child: Icon(Icons.save), + ), + Text('Save'), + ], + ), + onPressed: () async { + await _saveBusiness(context); + }, + ), + ), + ) ], ), ), @@ -582,6 +565,53 @@ class _CreateEditBusinessState extends State { ); } } + + Future _saveBusiness(BuildContext context) async { + if (business.type == null) { + setState(() { + dropDownErrorText = 'Business type is required'; + }); + formKey.currentState!.validate(); + } else { + setState(() { + dropDownErrorText = null; + }); + if (formKey.currentState!.validate()) { + formKey.currentState?.save(); + setState(() { + _isLoading = true; + }); + String? result; + if (widget.inputBusiness != null) { + result = await editBusiness(business); + } else { + result = await createBusiness(business); + } + setState(() { + _isLoading = false; + }); + if (result != null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + width: 400, + behavior: SnackBarBehavior.floating, + content: Text(result))); + } else { + Navigator.pushReplacement(context, + MaterialPageRoute(builder: (context) => const MainApp())); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Check field inputs!'), + width: 200, + behavior: SnackBarBehavior.floating, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ); + } + } + } } class PhoneFormatter extends TextInputFormatter { diff --git a/fbla_ui/lib/pages/create_edit_listing.dart b/fbla_ui/lib/pages/create_edit_listing.dart index 0078e40..1ff25e6 100644 --- a/fbla_ui/lib/pages/create_edit_listing.dart +++ b/fbla_ui/lib/pages/create_edit_listing.dart @@ -1,9 +1,11 @@ -import 'package:fbla_ui/main.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:rive/rive.dart'; +import '../main.dart'; + class CreateEditJobListing extends StatefulWidget { final JobListing? inputJobListing; final Business? inputBusiness; @@ -21,9 +23,10 @@ class _CreateEditJobListingState extends State { late TextEditingController _descriptionController; late TextEditingController _wageController; late TextEditingController _linkController; - List nameMapping = []; + List> nameMapping = []; String? typeDropdownErrorText; String? businessDropdownErrorText; + late bool widescreen; JobListing listing = JobListing( id: null, @@ -32,7 +35,8 @@ class _CreateEditJobListingState extends State { description: 'Add details about the business below.', type: null, wage: null, - link: null); + link: null, + offerType: null); bool _isLoading = false; @override @@ -59,6 +63,7 @@ class _CreateEditJobListingState extends State { @override Widget build(BuildContext context) { + widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth; if (widget.inputBusiness != null) { listing.businessId = widget.inputBusiness!.id; } @@ -73,75 +78,22 @@ class _CreateEditJobListingState extends State { ? Text('Edit ${widget.inputJobListing?.name}', maxLines: 1) : const Text('Add New Job Listing'), ), - floatingActionButton: FloatingActionButton( - child: _isLoading - ? const Padding( - padding: EdgeInsets.all(16.0), - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 3.0, - ), - ) - : const Icon(Icons.save), - 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()) { - formKey.currentState?.save(); - setState(() { - _isLoading = true; - }); - String? result; - if (widget.inputJobListing != null) { - result = await editListing(listing); - } else { - result = await createListing(listing); - } - setState(() { - _isLoading = false; - }); - if (result != null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - width: 400, - behavior: SnackBarBehavior.floating, - content: Text(result))); - } else { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => const MainApp( - initialPage: 1, - ))); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Check field inputs!'), - width: 200, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - ), - ); - } - } - }), + floatingActionButton: !widescreen + ? FloatingActionButton.extended( + label: const Text('Save'), + icon: _isLoading + ? const Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 3.0, + ), + ) + : const Icon(Icons.save), + onPressed: () async { + await _saveListing(context); + }) + : null, body: FutureBuilder( future: getBusinessNameMapping, builder: (context, snapshot) { @@ -172,6 +124,8 @@ class _CreateEditJobListingState extends State { } nameMapping = snapshot.data; + nameMapping.sort((a, b) => + a['name'].toString().compareTo(b['name'].toString())); return ListView( children: [ @@ -181,30 +135,29 @@ class _CreateEditJobListingState extends State { child: Column( children: [ ListTile( + titleAlignment: + ListTileTitleAlignment.titleHeight, title: Text(listing.name, - textAlign: TextAlign.left, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold)), subtitle: Text( listing.description, - textAlign: TextAlign.left, ), + contentPadding: const EdgeInsets.only( + bottom: 8, left: 16), leading: ClipRRect( borderRadius: BorderRadius.circular(6.0), child: Image.network( + '$apiAddress/logos/${listing.businessId}', width: 48, - height: 48, - listing.businessId != null - ? '$apiAddress/logos/${listing.businessId}' - : '', - errorBuilder: (BuildContext context, - Object exception, - StackTrace? stackTrace) { + height: 48, errorBuilder: + (BuildContext context, + Object exception, + StackTrace? stackTrace) { return Icon( getIconFromJobType( - listing.type ?? JobType.other, - ), + listing.type ?? JobType.other), size: 48); }), ), @@ -213,86 +166,126 @@ class _CreateEditJobListingState extends State { Card( child: Column( children: [ - Padding( - padding: const EdgeInsets.only( - left: 8.0, - right: 8.0, - bottom: 8.0, - top: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + Align( + alignment: Alignment.centerLeft, + child: Wrap( children: [ - const Text('Type of Job', - style: - TextStyle(fontSize: 16)), - DropdownMenu( - initialSelection: listing.type, - label: const Text('Job Type'), - errorText: - typeDropdownErrorText, - dropdownMenuEntries: [ - for (JobType type - in JobType.values) - DropdownMenuEntry( - value: type, - label: - getNameFromJobType( - type)) - ], - onSelected: (inputType) { - setState(() { - listing.type = inputType!; - typeDropdownErrorText = - null; - }); - }, + Padding( + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + bottom: 8.0, + top: 8.0), + child: DropdownMenu( + initialSelection: + listing.type, + label: const Text('Job Type'), + errorText: + typeDropdownErrorText, + width: calculateDropdownWidth( + context), + dropdownMenuEntries: [ + for (JobType type + in JobType.values) + DropdownMenuEntry( + value: type, + label: + getNameFromJobType( + type)) + ], + onSelected: (inputType) { + setState(() { + listing.type = inputType!; + typeDropdownErrorText = + null; + }); + }, + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + bottom: 8.0, + top: 8.0), + child: DropdownMenu( + initialSelection: + listing.offerType, + label: + const Text('Offer Type'), + errorText: + typeDropdownErrorText, + width: calculateDropdownWidth( + context), + dropdownMenuEntries: [ + for (OfferType type + in OfferType.values) + DropdownMenuEntry( + value: type, + label: + getNameFromOfferType( + type)) + ], + onSelected: (inputType) { + setState(() { + listing.offerType = + inputType!; + typeDropdownErrorText = + null; + }); + }, + ), ), ], ), ), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + bottom: 16.0, + top: 8.0), + child: DropdownMenu( + menuHeight: 300, + width: (MediaQuery.sizeOf(context) + .width - + 24) < + 776 + ? MediaQuery.sizeOf(context) + .width - + 24 + : 776, + errorText: + businessDropdownErrorText, + initialSelection: + widget.inputBusiness?.id, + label: const Text( + 'Offering Business'), + dropdownMenuEntries: [ + for (Map map + in nameMapping) + DropdownMenuEntry( + value: map['id']!, + label: map['name']) + ], + onSelected: (inputType) { + setState(() { + listing.businessId = + inputType!; + businessDropdownErrorText = + null; + }); + }, + ), + ), + ), Padding( padding: const EdgeInsets.only( left: 8.0, right: 8.0, - bottom: 8.0, - top: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Business that has the job', - style: - TextStyle(fontSize: 16)), - DropdownMenu( - errorText: - businessDropdownErrorText, - initialSelection: - widget.inputBusiness?.id, - label: const Text('Business'), - dropdownMenuEntries: [ - for (Map map - in nameMapping) - DropdownMenuEntry( - value: map['id']!, - label: map['name']) - ], - onSelected: (inputType) { - setState(() { - listing.businessId = - inputType!; - businessDropdownErrorText = - null; - }); - }, - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 8.0, right: 8.0), + bottom: 8.0), child: TextFormField( controller: _nameController, autovalidateMode: AutovalidateMode @@ -322,7 +315,9 @@ class _CreateEditJobListingState extends State { ), Padding( padding: const EdgeInsets.only( - left: 8.0, right: 8.0), + left: 8.0, + right: 8.0, + bottom: 8.0), child: TextFormField( controller: _descriptionController, autovalidateMode: AutovalidateMode @@ -355,7 +350,7 @@ class _CreateEditJobListingState extends State { padding: const EdgeInsets.only( left: 8.0, right: 8.0, - bottom: 8.0), + bottom: 16.0), child: TextFormField( controller: _wageController, onChanged: (input) { @@ -376,7 +371,7 @@ class _CreateEditJobListingState extends State { padding: const EdgeInsets.only( left: 8.0, right: 8.0, - bottom: 8.0), + bottom: 16.0), child: TextFormField( controller: _linkController, autovalidateMode: AutovalidateMode @@ -399,7 +394,7 @@ class _CreateEditJobListingState extends State { 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]*)*') + !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'; } @@ -418,9 +413,35 @@ class _CreateEditJobListingState extends State { ], ), ), - const SizedBox( - height: 75, - ) + if (!widescreen) + const SizedBox( + height: 75, + ) + else + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: FilledButton( + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.only( + top: 8.0, + right: 8.0, + bottom: 8.0), + child: Icon(Icons.save), + ), + Text('Save'), + ], + ), + onPressed: () async { + await _saveListing(context); + }, + ), + ), + ) ], ), ), @@ -456,6 +477,18 @@ class _CreateEditJobListingState extends State { ); } + double calculateDropdownWidth(BuildContext context) { + double screenWidth = MediaQuery.sizeOf(context).width; + + if ((screenWidth - 40) / 2 < 200) { + return screenWidth - 24; + } else if ((screenWidth - 40) / 2 < 380) { + return (screenWidth - 40) / 2; + } else { + return 380; + } + } + void _handlePop(bool didPop) { if (!didPop) { ScaffoldMessenger.of(context).showSnackBar( @@ -467,4 +500,64 @@ class _CreateEditJobListingState extends State { ); } } + + Future _saveListing(BuildContext context) 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()) { + formKey.currentState?.save(); + setState(() { + _isLoading = true; + }); + String? result; + if (widget.inputJobListing != null) { + result = await editListing(listing); + } else { + result = await createListing(listing); + } + setState(() { + _isLoading = false; + }); + if (result != null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + width: 400, + behavior: SnackBarBehavior.floating, + content: Text(result))); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const MainApp( + initialPage: 1, + ))); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Check field inputs!'), + width: 200, + behavior: SnackBarBehavior.floating, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ); + } + } + } } diff --git a/fbla_ui/lib/pages/listing_detail.dart b/fbla_ui/lib/pages/listing_detail.dart index ba13176..108383e 100644 --- a/fbla_ui/lib/pages/listing_detail.dart +++ b/fbla_ui/lib/pages/listing_detail.dart @@ -29,145 +29,144 @@ class _CreateBusinessDetailState extends State { ); } - ListView _detailBody(JobListing listing) { + Widget _detailBody(JobListing listing) { return ListView( children: [ // Title, logo, desc, website - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Card( - clipBehavior: Clip.antiAlias, + Center( + child: SizedBox( + width: 800, child: Column( children: [ Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(6.0), - child: Image.network( - '$apiAddress/logos/${listing.businessId}', - width: 48, - height: 48, errorBuilder: (BuildContext context, - Object exception, StackTrace? stackTrace) { - return Icon(getIconFromJobType(listing.type!), - size: 48); - }), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(listing.name, + padding: const EdgeInsets.only(top: 4.0), + child: Card( + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + ListTile( + titleAlignment: ListTileTitleAlignment.titleHeight, + title: Text(listing.name, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold)), - Text(widget.fromBusiness.name!, - style: const TextStyle(fontSize: 16)), - Text( + subtitle: Text( listing.description, ), + contentPadding: + const EdgeInsets.only(bottom: 8, left: 16), + leading: ClipRRect( + borderRadius: BorderRadius.circular(6.0), + child: Image.network( + '$apiAddress/logos/${listing.businessId}', + width: 48, + height: 48, errorBuilder: (BuildContext context, + Object exception, StackTrace? stackTrace) { + return Icon( + getIconFromJobType( + listing.type ?? JobType.other), + size: 48); + }), + ), + ), + if (listing.link != null && listing.link != '') + ListTile( + leading: const Icon(Icons.link), + title: const Text('More Information'), + subtitle: Text( + listing.link! + .replaceAll('https://', '') + .replaceAll('http://', '') + .replaceAll('www.', ''), + style: const TextStyle(color: Colors.blue)), + onTap: () { + launchUrl(Uri.parse('https://${listing.link!}')); + }, + ), + ], + ), + ), + ), + // Wage + Visibility( + visible: listing.wage != null && listing.wage != '', + child: Card( + child: ListTile( + leading: const Icon(Icons.attach_money), + subtitle: Text(listing.wage!), + title: const Text('Wage Information'), + ), + ), + ), + Card( + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + Row( + children: [ + Padding( + padding: + const EdgeInsets.only(left: 16.0, top: 8.0), + child: Text( + widget.fromBusiness.contactName!, + textAlign: TextAlign.left, + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + ), ], ), + if (widget.fromBusiness.contactPhone != null) + ListTile( + leading: const Icon(Icons.phone), + title: Text(widget.fromBusiness.contactPhone!), + // maybe replace ! with ?? ''. same is true for below + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: + Theme.of(context).colorScheme.surface, + title: Text( + 'Contact ${widget.fromBusiness.contactName}'), + content: Text( + 'Would you like to call or text ${widget.fromBusiness.contactName}?'), + actions: [ + TextButton( + child: const Text('Text'), + onPressed: () { + launchUrl(Uri.parse( + 'sms:${widget.fromBusiness.contactPhone}')); + Navigator.of(context).pop(); + }), + TextButton( + child: const Text('Call'), + onPressed: () async { + launchUrl(Uri.parse( + 'tel:${widget.fromBusiness.contactPhone}')); + Navigator.of(context).pop(); + }), + ], + ); + }); + }, + ), + if (widget.fromBusiness.contactEmail != null) + ListTile( + leading: const Icon(Icons.email), + title: Text(widget.fromBusiness.contactEmail!), + onTap: () { + launchUrl(Uri.parse( + 'mailto:${widget.fromBusiness.contactEmail}')); + }, + ), ], ), ), - if (listing.link != null && listing.link != '') - ListTile( - leading: const Icon(Icons.link), - title: const Text('More Information'), - subtitle: Text( - listing.link! - .replaceAll('https://', '') - .replaceAll('http://', '') - .replaceAll('www.', ''), - style: const TextStyle(color: Colors.blue)), - onTap: () { - launchUrl(Uri.parse(listing.link!)); - }, - ), ], ), ), ), - // Wage - Visibility( - visible: listing.wage != null && listing.wage != '', - child: Card( - child: ListTile( - leading: const Icon(Icons.attach_money), - subtitle: Text(listing.wage!), - title: const Text('Wage Information'), - ), - ), - ), - Card( - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: Text( - widget.fromBusiness.contactName!, - textAlign: TextAlign.left, - style: const TextStyle( - fontSize: 20, fontWeight: FontWeight.bold), - ), - ), - ], - ), - if (widget.fromBusiness.contactPhone != null) - ListTile( - leading: const Icon(Icons.phone), - title: Text(widget.fromBusiness.contactPhone!), - // maybe replace ! with ?? ''. same is true for below - onTap: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: - Theme.of(context).colorScheme.surface, - title: Text( - 'Contact ${widget.fromBusiness.contactName}'), - content: Text( - 'Would you like to call or text ${widget.fromBusiness.contactName}?'), - actions: [ - TextButton( - child: const Text('Text'), - onPressed: () { - launchUrl(Uri.parse( - 'sms:${widget.fromBusiness.contactPhone}')); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Call'), - onPressed: () async { - launchUrl(Uri.parse( - 'tel:${widget.fromBusiness.contactPhone}')); - Navigator.of(context).pop(); - }), - ], - ); - }); - }, - ), - if (widget.fromBusiness.contactEmail != null) - ListTile( - leading: const Icon(Icons.email), - title: Text(widget.fromBusiness.contactEmail!), - onTap: () { - launchUrl(Uri.parse( - 'mailto:${widget.fromBusiness.contactEmail}')); - }, - ), - ], - ), - ), ], ); } diff --git a/fbla_ui/lib/pages/listings_overview.dart b/fbla_ui/lib/pages/listings_overview.dart index 682280f..7683474 100644 --- a/fbla_ui/lib/pages/listings_overview.dart +++ b/fbla_ui/lib/pages/listings_overview.dart @@ -15,7 +15,8 @@ import 'package:url_launcher/url_launcher.dart'; class JobsOverview extends StatefulWidget { final String searchQuery; final Future refreshJobDataOverviewFuture; - final Future Function(Set) updateBusinessesCallback; + final Future Function(Set?, Set?) + updateBusinessesCallback; final void Function() themeCallback; final void Function(bool) updateLoggedIn; @@ -36,6 +37,7 @@ class _JobsOverviewState extends State { bool _isPreviousData = false; late Map> overviewBusinesses; Set jobTypeFilters = {}; + Set offerTypeFilters = {}; String searchQuery = ''; ScrollController controller = ScrollController(); bool _extended = true; @@ -66,9 +68,15 @@ class _JobsOverviewState extends State { }); } - void _setFilters(Set filters) async { - jobTypeFilters = Set.from(filters); - widget.updateBusinessesCallback(jobTypeFilters); + void _setFilters(Set? newJobTypeFilters, + Set? newOfferTypeFilters) async { + if (newJobTypeFilters != null) { + jobTypeFilters = Set.from(newJobTypeFilters); + } + if (newOfferTypeFilters != null) { + offerTypeFilters = Set.from(newOfferTypeFilters); + } + widget.updateBusinessesCallback(jobTypeFilters, offerTypeFilters); } void _scrollListener() { @@ -114,9 +122,8 @@ class _JobsOverviewState extends State { setSearch: _setSearch, searchHintText: 'Search Job Listings', themeCallback: widget.themeCallback, - filterIconButton: _filterIconButton( - jobTypeFilters, - ), + filterIconButton: + _filterIconButton(jobTypeFilters, offerTypeFilters), updateLoggedIn: widget.updateLoggedIn, generatePDF: _generatePDF, ), @@ -139,7 +146,7 @@ class _JobsOverviewState extends State { child: FilledButton( child: const Text('Retry'), onPressed: () { - widget.updateBusinessesCallback(jobTypeFilters); + widget.updateBusinessesCallback(null, null); }, ), ), @@ -201,45 +208,78 @@ class _JobsOverviewState extends State { ); } - Widget _filterIconButton(Set filters) { - Set selectedChips = Set.from(filters); + Widget _filterIconButton( + Set jobTypeFilters, Set offerTypeFilters) { + Set selectedJobTypeChips = Set.from(jobTypeFilters); + Set selectedOfferTypeChips = Set.from(offerTypeFilters); return IconButton( icon: Icon( Icons.filter_list, - color: filters.isNotEmpty + color: jobTypeFilters.isNotEmpty ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), onPressed: () { + selectedJobTypeChips = Set.from(jobTypeFilters); + selectedOfferTypeChips = Set.from(offerTypeFilters); showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - void setDialogState(Set newFilters) { - setState(() { - filters = newFilters; - }); + void setDialogState(Set? newJobTypeFilters, + Set? newOfferTypeFilters) { + if (newJobTypeFilters != null) { + setState(() { + selectedJobTypeChips = newJobTypeFilters; + }); + } + if (newOfferTypeFilters != null) { + setState(() { + selectedOfferTypeChips = newOfferTypeFilters; + }); + } } - List chips = []; - for (var type in JobType.values) { - chips.add(Padding( + List jobTypeChips = []; + for (JobType type in JobType.values) { + jobTypeChips.add(Padding( padding: const EdgeInsets.all(4), child: FilterChip( showCheckmark: false, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20)), label: Text(getNameFromJobType(type)), - selected: selectedChips.contains(type), + selected: selectedJobTypeChips.contains(type), onSelected: (bool selected) { if (selected) { - selectedChips.add(type); + selectedJobTypeChips.add(type); } else { - selectedChips.remove(type); + selectedJobTypeChips.remove(type); } - setDialogState(filters); + setDialogState(selectedJobTypeChips, null); + }), + )); + } + + List offerTypeChips = []; + for (OfferType type in OfferType.values) { + offerTypeChips.add(Padding( + padding: const EdgeInsets.all(4), + child: FilterChip( + showCheckmark: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + label: Text(getNameFromOfferType(type)), + selected: selectedOfferTypeChips.contains(type), + onSelected: (bool selected) { + if (selected) { + selectedOfferTypeChips.add(type); + } else { + selectedOfferTypeChips.remove(type); + } + setDialogState(null, selectedOfferTypeChips); }), )); } @@ -248,30 +288,46 @@ class _JobsOverviewState extends State { title: const Text('Filter Options'), content: SizedBox( width: 400, - child: Wrap( - children: chips, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Job Type Filters:'), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Wrap( + children: jobTypeChips, + ), + ), + const Text('Offer Type Filters:'), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Wrap( + children: offerTypeChips, + ), + ), + ], ), ), actions: [ TextButton( child: const Text('Reset'), onPressed: () { - _setFilters({}); - // selectedChips = {}; + _setFilters({}, {}); Navigator.of(context).pop(); }, ), TextButton( child: const Text('Cancel'), onPressed: () { - // selectedChips = Set.from(filters); + // setDialogState(jobTypeFilters, offerTypeFilters); Navigator.of(context).pop(); }, ), TextButton( child: const Text('Apply'), onPressed: () { - _setFilters(selectedChips); + _setFilters( + selectedJobTypeChips, selectedOfferTypeChips); Navigator.of(context).pop(); }, ) @@ -337,9 +393,9 @@ class _JobDisplayPanelState extends State { ); } - List headers = []; + List<_JobHeader> headers = []; for (JobType jobType in widget.jobGroupedBusinesses.keys) { - headers.add(BusinessHeader( + headers.add(_JobHeader( jobType: jobType, widescreen: widget.widescreen, businesses: widget.jobGroupedBusinesses[jobType]!)); @@ -349,23 +405,22 @@ class _JobDisplayPanelState extends State { } } -class BusinessHeader extends StatefulWidget { +class _JobHeader extends StatefulWidget { final JobType jobType; final List businesses; final bool widescreen; - const BusinessHeader({ - super.key, + const _JobHeader({ required this.jobType, required this.businesses, required this.widescreen, }); @override - State createState() => _BusinessHeaderState(); + State<_JobHeader> createState() => _JobHeaderState(); } -class _BusinessHeaderState extends State { +class _JobHeaderState extends State<_JobHeader> { @override Widget build(BuildContext context) { return SliverStickyHeader( @@ -389,7 +444,8 @@ class _BusinessHeaderState extends State { getIconFromJobType(widget.jobType), color: Theme.of(context).colorScheme.onPrimary, )), - Text(getNameFromJobType(widget.jobType)), + Text(getNameFromJobType(widget.jobType), + style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)), ], ); } @@ -451,21 +507,34 @@ class _BusinessHeaderState extends State { 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); - }), + child: Badge( + label: Text( + getLetterFromOfferType( + business.listings![0].offerType!), + style: const TextStyle(fontSize: 16), + ), + largeSize: 26, + offset: const Offset(15, -5), + textColor: Theme.of(context).colorScheme.onPrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + 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( + getIconFromJobType(business.listings![0].type!), + size: 48); + }), + ), )), Flexible( child: Padding( padding: const EdgeInsets.all(8.0), child: Text( - business.listings![0].name, + '${business.listings![0].name} (${getNameFromOfferType(business.listings![0].offerType!)})', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold), maxLines: 2, @@ -559,14 +628,23 @@ class _BusinessHeaderState extends State { 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), + leading: Badge( + label: Text(getLetterFromOfferType(business.listings![0].offerType!)), + textColor: Theme.of(context).colorScheme.onPrimary, + isLabelVisible: true, + backgroundColor: Theme.of(context).colorScheme.primary, + child: 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( + getIconFromJobType(business.listings![0].type!), + ); + })), + ), + title: Text( + '${business.listings![0].name} (${getNameFromOfferType(business.listings![0].offerType!)})'), subtitle: Text(business.listings![0].description, maxLines: 2, overflow: TextOverflow.ellipsis), onTap: () { diff --git a/fbla_ui/lib/shared/api_logic.dart b/fbla_ui/lib/shared/api_logic.dart index 89203ff..ff7eecc 100644 --- a/fbla_ui/lib/shared/api_logic.dart +++ b/fbla_ui/lib/shared/api_logic.dart @@ -8,6 +8,7 @@ import 'package:http/http.dart' as http; var apiAddress = 'https://homelab.marinodev.com/fbla-api'; // var apiAddress = 'http://192.168.0.114:8000/fbla-api'; + var client = http.Client(); Future fetchBusinessData() async { @@ -50,15 +51,20 @@ Future fetchBusinessNames() async { } } -Future fetchBusinessDataOverviewJobs({List? typeFilters}) async { +Future fetchBusinessDataOverviewJobs( + {Iterable? typeFilters, Iterable? offerFilters}) async { try { - String? typeString = - typeFilters?.map((jobType) => jobType.name).toList().join(','); - Uri uri = - Uri.parse('$apiAddress/businessdata/overview/jobs?filters=$typeString'); - if (typeFilters == null || typeFilters.isEmpty) { - uri = Uri.parse('$apiAddress/businessdata/overview/jobs'); + String uriString = '$apiAddress/businessdata/overview/jobs'; + if (typeFilters != null && typeFilters.isNotEmpty) { + uriString += + '?typeFilters=${typeFilters.map((jobType) => jobType.name).join(',')}'; } + if (offerFilters != null && offerFilters.isNotEmpty) { + uriString += + '?offerFilters=${offerFilters.map((offerType) => offerType.name).join(',')}'; + } + Uri uri = Uri.parse(uriString); + var response = await http.get(uri).timeout(const Duration(seconds: 20)); if (response.statusCode == 200) { var decodedResponse = json.decode(response.body); @@ -225,6 +231,8 @@ Future createListing(JobListing listing) async { "businessId": ${listing.businessId}, "name": "${listing.name}", "description": "${listing.description.replaceAll('\n', '\\n')}", + "type": "${listing.type!.name}", + "offerType": "${listing.offerType!.name}", "wage": "${listing.wage}", "link": "${listing.link}" } @@ -324,6 +332,7 @@ Future editListing(JobListing listing) async { "name": "${listing.name}", "description": "${listing.description.replaceAll('\n', '\\n')}", "type": "${listing.type!.name}", + "offerType": "${listing.offerType!.name}", "wage": "${listing.wage}", "link": "${listing.link}" } diff --git a/fbla_ui/lib/shared/export.dart b/fbla_ui/lib/shared/export.dart index 0d112f3..716f017 100644 --- a/fbla_ui/lib/shared/export.dart +++ b/fbla_ui/lib/shared/export.dart @@ -11,6 +11,68 @@ import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; +Map dataTypePriorityBusiness = { + DataTypeBusiness.logo: 0, + DataTypeBusiness.name: 1, + DataTypeBusiness.description: 2, + DataTypeBusiness.type: 3, + DataTypeBusiness.website: 4, + DataTypeBusiness.contactName: 5, + DataTypeBusiness.contactEmail: 6, + DataTypeBusiness.contactPhone: 7, + DataTypeBusiness.notes: 8 +}; + +Map dataTypeFriendlyBusiness = { + DataTypeBusiness.logo: 'Logo', + DataTypeBusiness.name: 'Name', + DataTypeBusiness.description: 'Description', + DataTypeBusiness.type: 'Type', + DataTypeBusiness.website: 'Website', + DataTypeBusiness.contactName: 'Contact Name', + DataTypeBusiness.contactEmail: 'Contact Email', + DataTypeBusiness.contactPhone: 'Contact Phone', + DataTypeBusiness.notes: 'Notes' +}; + +Map dataTypePriorityJob = { + DataTypeJob.businessName: 1, + DataTypeJob.name: 2, + DataTypeJob.description: 3, + DataTypeJob.type: 4, + DataTypeJob.offerType: 5, + DataTypeJob.wage: 6, + DataTypeJob.link: 7, +}; + +Map dataTypeFriendlyJob = { + DataTypeJob.businessName: 'Business Name', + DataTypeJob.name: 'Job Listing Name', + DataTypeJob.description: 'Description', + DataTypeJob.type: 'Job Type', + DataTypeJob.offerType: 'Offer Type', + DataTypeJob.wage: 'Wage Information', + DataTypeJob.link: 'Additional Info Link', +}; + +Set sortDataTypesBusiness(Set set) { + List list = set.toList(); + list.sort((a, b) { + return dataTypePriorityBusiness[a]!.compareTo(dataTypePriorityBusiness[b]!); + }); + set = list.toSet(); + return set; +} + +Set sortDataTypesJob(Set set) { + List list = set.toList(); + list.sort((a, b) { + return dataTypePriorityJob[a]!.compareTo(dataTypePriorityJob[b]!); + }); + set = list.toSet(); + return set; +} + class _FilterBusinessDataTypeChips extends StatefulWidget { final Set selectedDataTypesBusiness; @@ -113,11 +175,13 @@ Future generatePDF( context: context, builder: (BuildContext context) { return AlertDialog( + contentPadding: const EdgeInsets.all(16), + scrollable: true, title: const Text('Export Settings'), content: SizedBox( width: 400, - height: 200, child: Column( + mainAxisSize: MainAxisSize.min, children: [ const Padding( padding: EdgeInsets.all(8.0), @@ -208,21 +272,42 @@ Future generatePDF( for (JobListing job in business.listings!) { List jobRow = []; for (DataTypeJob dataType in dataTypesJob) { - if (dataType != DataTypeJob.businessName) { - var currentValue = - jobValueFromDataType(job, dataType); - if (currentValue != null) { + switch (dataType) { + case DataTypeJob.businessName: jobRow.add(pw.Padding( - child: pw.Text(currentValue), + child: pw.Text(business.name!), + padding: const pw.EdgeInsets.all(4.0))); + case DataTypeJob.type: + jobRow.add(pw.Padding( + child: pw.Text(getNameFromJobType(job.type!)), + padding: const pw.EdgeInsets.all(4.0))); + case DataTypeJob.offerType: + jobRow.add(pw.Padding( + child: pw.Text( + getNameFromOfferType(job.offerType!)), + padding: const pw.EdgeInsets.all(4.0))); + default: + jobRow.add(pw.Padding( + child: pw.Text( + jobValueFromDataType(job, dataType) ?? + ''), padding: const pw.EdgeInsets.all(4.0))); - } else { - jobRow.add(pw.Container()); - } - } else { - jobRow.add(pw.Padding( - child: pw.Text(business.name!), - padding: const pw.EdgeInsets.all(4.0))); } + // if (dataType != DataTypeJob.businessName) { + // var currentValue = + // jobValueFromDataType(job, dataType); + // if (currentValue != null) { + // jobRow.add(pw.Padding( + // child: pw.Text(currentValue), + // padding: const pw.EdgeInsets.all(4.0))); + // } else { + // jobRow.add(pw.Container()); + // } + // } else { + // jobRow.add(pw.Padding( + // child: pw.Text(business.name!), + // padding: const pw.EdgeInsets.all(4.0))); + // } } tableRows.add(pw.TableRow(children: jobRow)); } @@ -344,6 +429,11 @@ Map _businessColumnSizes( map.addAll( {sorted.indexOf(DataTypeBusiness.logo): const pw.FixedColumnWidth(32)}); } + if (sorted.contains(DataTypeBusiness.type)) { + space -= 68; + map.addAll( + {sorted.indexOf(DataTypeBusiness.type): const pw.FixedColumnWidth(68)}); + } if (dataTypes.contains(DataTypeBusiness.contactName)) { space -= 72; map.addAll({ @@ -369,7 +459,7 @@ Map _businessColumnSizes( leftNum += 1; } if (dataTypes.contains(DataTypeBusiness.notes)) { - leftNum += 2; + leftNum += 1; } if (dataTypes.contains(DataTypeBusiness.description)) { leftNum += 3; @@ -391,9 +481,8 @@ Map _businessColumnSizes( }); } if (dataTypes.contains(DataTypeBusiness.notes)) { - map.addAll({ - sorted.indexOf(DataTypeBusiness.notes): pw.FixedColumnWidth(leftNum * 2) - }); + map.addAll( + {sorted.indexOf(DataTypeBusiness.notes): pw.FixedColumnWidth(leftNum)}); } if (dataTypes.contains(DataTypeBusiness.description)) { map.addAll({ @@ -416,6 +505,13 @@ Map _jobColumnSizes(Set dataTypes) { .first): const pw.FractionColumnWidth(0.2) }); } + if (dataTypes.contains(DataTypeJob.type)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.type) + .first): const pw.FractionColumnWidth(0.1) + }); + } if (dataTypes.contains(DataTypeJob.name)) { map.addAll({ sortedDataTypes.indexOf(sortedDataTypes @@ -427,7 +523,14 @@ Map _jobColumnSizes(Set dataTypes) { map.addAll({ sortedDataTypes.indexOf(sortedDataTypes .where((element) => element == DataTypeJob.description) - .first): const pw.FractionColumnWidth(0.4) + .first): const pw.FractionColumnWidth(0.3) + }); + } + if (dataTypes.contains(DataTypeJob.offerType)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.offerType) + .first): const pw.FractionColumnWidth(0.1) }); } if (dataTypes.contains(DataTypeJob.wage)) { @@ -456,7 +559,7 @@ dynamic businessValueFromDataType( case DataTypeBusiness.description: return business.description; case DataTypeBusiness.type: - return business.type; + return getNameFromBusinessType(business.type!); case DataTypeBusiness.website: return business.website; case DataTypeBusiness.contactName: @@ -478,6 +581,10 @@ dynamic jobValueFromDataType(JobListing job, DataTypeJob dataType) { return job.name; case DataTypeJob.description: return job.description; + case DataTypeJob.type: + return job.type; + case DataTypeJob.offerType: + return job.offerType; case DataTypeJob.wage: return job.wage; case DataTypeJob.link: diff --git a/fbla_ui/lib/shared/utils.dart b/fbla_ui/lib/shared/utils.dart index 312e656..320bbdc 100644 --- a/fbla_ui/lib/shared/utils.dart +++ b/fbla_ui/lib/shared/utils.dart @@ -17,68 +17,12 @@ enum DataTypeJob { businessName, name, description, + type, + offerType, wage, link, } -Map dataTypePriorityBusiness = { - DataTypeBusiness.logo: 0, - DataTypeBusiness.name: 1, - DataTypeBusiness.description: 2, - DataTypeBusiness.type: 3, - DataTypeBusiness.website: 4, - DataTypeBusiness.contactName: 5, - DataTypeBusiness.contactEmail: 6, - DataTypeBusiness.contactPhone: 7, - DataTypeBusiness.notes: 8 -}; - -Map dataTypeFriendlyBusiness = { - DataTypeBusiness.logo: 'Logo', - DataTypeBusiness.name: 'Name', - DataTypeBusiness.description: 'Description', - DataTypeBusiness.type: 'Type', - DataTypeBusiness.website: 'Website', - DataTypeBusiness.contactName: 'Contact Name', - DataTypeBusiness.contactEmail: 'Contact Email', - DataTypeBusiness.contactPhone: 'Contact Phone', - DataTypeBusiness.notes: 'Notes' -}; - -Map dataTypePriorityJob = { - DataTypeJob.businessName: 1, - DataTypeJob.name: 2, - DataTypeJob.description: 3, - DataTypeJob.wage: 4, - DataTypeJob.link: 5, -}; - -Map dataTypeFriendlyJob = { - DataTypeJob.businessName: 'Business Name', - DataTypeJob.name: 'Job Listing Name', - DataTypeJob.description: 'Description', - DataTypeJob.wage: 'Wage', - DataTypeJob.link: 'Additional Info Link', -}; - -Set sortDataTypesBusiness(Set set) { - List list = set.toList(); - list.sort((a, b) { - return dataTypePriorityBusiness[a]!.compareTo(dataTypePriorityBusiness[b]!); - }); - set = list.toSet(); - return set; -} - -Set sortDataTypesJob(Set set) { - List list = set.toList(); - list.sort((a, b) { - return dataTypePriorityJob[a]!.compareTo(dataTypePriorityJob[b]!); - }); - set = list.toSet(); - return set; -} - enum BusinessType { food, shop, @@ -90,6 +34,8 @@ enum BusinessType { enum JobType { cashier, server, mechanic, other } +enum OfferType { job, internship, apprenticeship } + class JobListing { int? id; int? businessId; @@ -98,6 +44,7 @@ class JobListing { JobType? type; String? wage; String? link; + OfferType? offerType; JobListing( {this.id, @@ -106,7 +53,8 @@ class JobListing { required this.description, this.type, this.wage, - this.link}); + this.link, + this.offerType}); factory JobListing.copy(JobListing input) { return JobListing( @@ -117,6 +65,7 @@ class JobListing { type: input.type, wage: input.wage, link: input.link, + offerType: input.offerType, ); } } @@ -139,7 +88,7 @@ class Business { {required this.id, required this.name, required this.description, - required this.website, + this.website, this.type, this.contactName, this.contactEmail, @@ -161,7 +110,9 @@ class Business { description: json['listings'][i]['description'], type: JobType.values.byName(json['listings'][i]['type']), wage: json['listings'][i]['wage'], - link: json['listings'][i]['link'])); + link: json['listings'][i]['link'], + offerType: + OfferType.values.byName(json['listings'][i]['offerType']))); } } @@ -288,6 +239,28 @@ String getNameFromJobType(JobType type) { } } +String getNameFromOfferType(OfferType type) { + switch (type) { + case OfferType.job: + return 'Job'; + case OfferType.internship: + return 'Internship'; + case OfferType.apprenticeship: + return 'Apprenticeship'; + } +} + +String getLetterFromOfferType(OfferType type) { + switch (type) { + case OfferType.job: + return 'J'; + case OfferType.internship: + return 'I'; + case OfferType.apprenticeship: + return 'A'; + } +} + IconData getIconFromThemeMode(ThemeMode theme) { switch (theme) { case ThemeMode.dark: diff --git a/fbla_ui/lib/shared/widgets.dart b/fbla_ui/lib/shared/widgets.dart index 4d9c677..3eb499b 100644 --- a/fbla_ui/lib/shared/widgets.dart +++ b/fbla_ui/lib/shared/widgets.dart @@ -504,6 +504,8 @@ class BusinessSearchBar extends StatefulWidget { } class _BusinessSearchBarState extends State { + TextEditingController controller = TextEditingController(); + @override Widget build(BuildContext context) { return SizedBox( @@ -511,6 +513,7 @@ class _BusinessSearchBarState extends State { height: 50, child: SearchBar( hintText: widget.searchTextHint, + controller: controller, backgroundColor: WidgetStateProperty.resolveWith((notNeeded) { return Theme.of(context).colorScheme.surfaceContainer; }), @@ -521,7 +524,17 @@ class _BusinessSearchBarState extends State { padding: EdgeInsets.only(left: 8.0), child: Icon(Icons.search), ), - trailing: [widget.filterIconButton]), + trailing: [ + if (controller.text != '') + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + controller.text = ''; + widget.setSearchCallback(''); + }, + ), + widget.filterIconButton + ]), ); } } @@ -624,10 +637,19 @@ class _MainSliverAppBarState extends State { Widget build(BuildContext context) { return SliverAppBar( title: widget.widescreen - ? BusinessSearchBar( - setSearchCallback: widget.setSearch, - searchTextHint: widget.searchHintText, - filterIconButton: widget.filterIconButton, + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: BusinessSearchBar( + setSearchCallback: widget.setSearch, + searchTextHint: widget.searchHintText, + filterIconButton: widget.filterIconButton, + ), + ) + // const PreferredSize( + // preferredSize: Size(144, 0), child: SizedBox()) + ], ) : const Text('Job Link'), toolbarHeight: 70,