From 32e3cc574ca8a9ef0b266a124a8b270c81771caf Mon Sep 17 00:00:00 2001 From: drake Date: Sun, 16 Jun 2024 14:04:12 -0500 Subject: [PATCH] Major Job Listings refactor --- fbla-api/lib/fbla_api.dart | 103 +- fbla_ui/lib/api_logic.dart | 143 ++- fbla_ui/lib/home.dart | 171 +-- fbla_ui/lib/main.dart | 7 +- fbla_ui/lib/pages/business_detail.dart | 365 +++--- fbla_ui/lib/pages/create_edit_business.dart | 62 +- fbla_ui/lib/pages/create_edit_listing.dart | 431 +++++++ fbla_ui/lib/pages/export_data.dart | 1211 +++++++++++-------- fbla_ui/lib/pages/listing_detail.dart | 221 ++++ fbla_ui/lib/pages/signin_page.dart | 3 +- fbla_ui/lib/shared.dart | 386 +++--- 11 files changed, 2147 insertions(+), 956 deletions(-) create mode 100644 fbla_ui/lib/pages/create_edit_listing.dart create mode 100644 fbla_ui/lib/pages/listing_detail.dart diff --git a/fbla-api/lib/fbla_api.dart b/fbla-api/lib/fbla_api.dart index d5b50da..814e13d 100644 --- a/fbla-api/lib/fbla_api.dart +++ b/fbla-api/lib/fbla_api.dart @@ -65,8 +65,8 @@ class Business { } class JobListing { - String? id; - String? businessId; + int? id; + int? businessId; String name; String description; JobType type; @@ -188,6 +188,26 @@ void main() async { }, ); }); + app.get('/fbla-api/businessdata/businessnames', (Request request) async { + print('business names request received'); + + var postgresResult = (await postgres.query(''' + SELECT json_agg( + json_build_object( + 'id', id, + 'name', name + ) + ) FROM public.businesses + '''))[0][0]; + + return Response.ok( + json.encode(postgresResult), + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'text/plain' + }, + ); + }); app.get('/fbla-api/businessdata/business/', (Request request, String business) async { print('idividual business data request received'); @@ -209,6 +229,7 @@ void main() async { json_agg( json_build_object( 'id', l.id, + 'businessId', l."businessId", 'name', l.name, 'description', l.description, 'type', l.type, @@ -231,11 +252,85 @@ void main() async { }, ); }); + app.get('/fbla-api/businessdata/businesses', (Request request) async { + print('list of business data request received'); + + if (request.url.queryParameters['businesses'] == null) { + return Response.badRequest( + body: 'query \'businesses\' required', + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'text/plain' + }, + ); + } + var filters = request.url.queryParameters['businesses']!.split(','); + + var result = (await postgres.query(''' + SELECT + json_build_object( + 'id', b.id, + 'name', b.name, + 'description', b.description, + 'website', b.website, + 'contactName', b."contactName", + 'contactEmail', b."contactEmail", + 'contactPhone', b."contactPhone", + 'notes', b.notes, + 'locationName', b."locationName", + 'locationAddress', b."locationAddress", + 'listings', + 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 + ) + ) + ) + FROM businesses b + LEFT JOIN listings l ON b.id = l."businessId" + WHERE b.id IN ${'$filters'.replaceAll('[', '(').replaceAll(']', ')')} + GROUP BY b.id; + ''')); + + var output = result.map((element) => element[0]).toList(); + + return Response.ok( + json.encode(output), + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'text/plain' + }, + ); + }); app.get('/fbla-api/businessdata', (Request request) async { print('business data request received'); - final output = await fetchBusinessData(); + final result = await postgres.query(''' + SELECT json_agg( + json_build_object( + 'id', id, + 'name', name, + 'description', description, + 'type', type, + 'website', website, + 'contactName', "contactName", + 'contactEmail', "contactEmail", + 'contactPhone', "contactPhone", + 'notes', notes, + 'locationName', "locationName", + 'locationAddress', "locationAddress" + ) + ) FROM businesses + '''); + + var encoded = json.encode(result[0][0]); return Response.ok( - output.toString(), + encoded, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain' diff --git a/fbla_ui/lib/api_logic.dart b/fbla_ui/lib/api_logic.dart index 41c2017..9ace035 100644 --- a/fbla_ui/lib/api_logic.dart +++ b/fbla_ui/lib/api_logic.dart @@ -5,9 +5,9 @@ import 'dart:io'; import 'package:fbla_ui/shared.dart'; import 'package:http/http.dart' as http; -var apiAddress = 'https://homelab.marinodev.com/fbla-api'; +// var apiAddress = 'https://homelab.marinodev.com/fbla-api'; +var apiAddress = 'http://192.168.0.114:8000/fbla-api'; var client = http.Client(); -// var apiAddress = '192.168.0.114:8000'; Future fetchBusinessData() async { try { @@ -29,11 +29,36 @@ Future fetchBusinessData() async { } } -Future fetchBusinessDataOverview() async { +Future fetchBusinessNames() async { try { var response = await http - .get(Uri.parse('$apiAddress/businessdata/overview')) + .get(Uri.parse('$apiAddress/businessdata/businessnames')) .timeout(const Duration(seconds: 20)); + if (response.statusCode == 200) { + List> decodedResponse = + json.decode(response.body).cast>(); + + return decodedResponse; + } else { + return 'Error ${response.statusCode}! Please try again later!'; + } + } on TimeoutException { + return 'Unable to connect to server (timeout).\nPlease try again later.'; + } on SocketException { + return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n'; + } +} + +Future fetchBusinessDataOverview({List? typeFilters}) async { + try { + String? typeString = + typeFilters?.map((jobType) => jobType.name).toList().join(','); + Uri uri = + Uri.parse('$apiAddress/businessdata/overview?filters=$typeString'); + if (typeFilters == null || typeFilters.isEmpty) { + uri = Uri.parse('$apiAddress/businessdata/overview'); + } + var response = await http.get(uri).timeout(const Duration(seconds: 20)); if (response.statusCode == 200) { var decodedResponse = json.decode(response.body); Map> groupedBusinesses = {}; @@ -49,13 +74,6 @@ Future fetchBusinessDataOverview() async { groupedBusinesses .addAll({JobType.values.byName(stringType): businesses}); } - - // for (JobType type in decodedResponse.keys) { - // groupedBusinesses.addAll({ - // JobType.values.byName(decodedResponse[i]): - // decodedResponse.map((json) => Business.fromJson(json)).toList() - // }); - // } return groupedBusinesses; } else { return 'Error ${response.statusCode}! Please try again later!'; @@ -67,6 +85,30 @@ Future fetchBusinessDataOverview() async { } } +Future fetchBusinesses(List ids) async { + try { + var response = await http + .get(Uri.parse( + '$apiAddress/businessdata/businesses?businesses=${ids.join(',')}')) + .timeout(const Duration(seconds: 20)); + if (response.statusCode == 200) { + List decodedResponse = json.decode(response.body); + + List businesses = decodedResponse + .map((json) => Business.fromJson(json)) + .toList(); + + return businesses; + } else { + return 'Error ${response.statusCode}! Please try again later!'; + } + } on TimeoutException { + return 'Unable to connect to server (timeout).\nPlease try again later.'; + } on SocketException { + return 'Unable to connect to server (socket exception).\nPlease check your internet connection.\n'; + } +} + Future fetchBusiness(int id) async { try { var response = await http @@ -87,7 +129,7 @@ Future fetchBusiness(int id) async { } } -Future createBusiness(Business business, String jwt) async { +Future createBusiness(Business business) async { var json = ''' { "id": ${business.id}, @@ -117,7 +159,33 @@ Future createBusiness(Business business, String jwt) async { } } -Future deleteBusiness(int id, String jwt) async { +Future createListing(JobListing listing) async { + var json = ''' + { + "id": ${listing.id}, + "businessId": ${listing.businessId}, + "name": "${listing.name}", + "description": "${listing.description}", + "wage": "${listing.wage}", + "link": "${listing.link}" + } + '''; + + try { + var response = await http.post(Uri.parse('$apiAddress/createbusiness'), + body: json, + headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20)); + if (response.statusCode != 200) { + return response.body; + } + } on TimeoutException { + return 'Unable to connect to server (timeout). Please try again later'; + } on SocketException { + return 'Unable to connect to server (socket exception). Please check your internet connection.'; + } +} + +Future deleteBusiness(int id) async { var json = ''' { "id": $id @@ -138,7 +206,28 @@ Future deleteBusiness(int id, String jwt) async { } } -Future editBusiness(Business business, String jwt) async { +Future deleteListing(int id) async { + var json = ''' + { + "id": $id + } + '''; + + try { + var response = await http.post(Uri.parse('$apiAddress/deletelisting'), + body: json, + headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20)); + if (response.statusCode != 200) { + return response.body; + } + } on TimeoutException { + return 'Unable to connect to server (timeout). Please try again later'; + } on SocketException { + return 'Unable to connect to server (socket exception). Please check your internet connection.'; + } +} + +Future editBusiness(Business business) async { var json = ''' { "id": ${business.id}, @@ -167,6 +256,32 @@ Future editBusiness(Business business, String jwt) async { } } +Future editListing(JobListing listing) async { + var json = ''' + { + "id": ${listing.id}, + "businessId": ${listing.businessId}, + "name": "${listing.name}", + "description": "${listing.description}", + "type": "${listing.type.name}", + "wage": "${listing.wage}", + "link": "${listing.link}" + } + '''; + try { + var response = await http.post(Uri.parse('$apiAddress/editlisting'), + body: json, + headers: {'Authorization': jwt}).timeout(const Duration(seconds: 20)); + if (response.statusCode != 200) { + return response.body; + } + } on TimeoutException { + return 'Unable to connect to server (timeout). Please try again later'; + } on SocketException { + return 'Unable to connect to server (socket exception). Please check your internet connection.'; + } +} + Future signIn(String username, String password) async { var json = ''' { diff --git a/fbla_ui/lib/home.dart b/fbla_ui/lib/home.dart index cda5496..b55710e 100644 --- a/fbla_ui/lib/home.dart +++ b/fbla_ui/lib/home.dart @@ -10,10 +10,8 @@ import 'package:rive/rive.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; -typedef Callback = void Function(); - class Home extends StatefulWidget { - final Callback themeCallback; + final void Function() themeCallback; const Home({super.key, required this.themeCallback}); @@ -25,6 +23,51 @@ class _HomeState extends State { late Future refreshBusinessDataOverviewFuture; bool _isPreviousData = false; late Map> overviewBusinesses; + Set jobTypeFilters = {}; + String searchQuery = ''; + Set selectedDataTypesJob = {}; + Set selectedDataTypesBusiness = {}; + + Future _setFilters(Set filters) async { + setState(() { + jobTypeFilters = filters; + }); + _updateOverviewBusinesses(); + } + + Future _updateOverviewBusinesses() async { + var refreshedData = + fetchBusinessDataOverview(typeFilters: jobTypeFilters.toList()); + await refreshedData; + setState(() { + refreshBusinessDataOverviewFuture = refreshedData; + }); + } + + Map> _filterBySearch( + Map> businesses) { + Map> 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 _setSearch(String search) async { + setState(() { + searchQuery = search; + }); + _updateOverviewBusinesses(); + } @override void initState() { @@ -61,26 +104,27 @@ class _HomeState extends State { Widget build(BuildContext context) { bool widescreen = MediaQuery.sizeOf(context).width >= 1000; return Scaffold( + // backgroundColor: Theme.of(context).scaffoldBackgroundColor, floatingActionButton: _getFAB(), body: RefreshIndicator( edgeOffset: 120, onRefresh: () async { - var refreshedData = fetchBusinessDataOverview(); - await refreshedData; - setState(() { - refreshBusinessDataOverviewFuture = refreshedData; - }); + _updateOverviewBusinesses(); }, child: CustomScrollView( slivers: [ SliverAppBar( - title: widescreen ? _searchBar() : const Text('Job Link'), + title: widescreen + ? BusinessSearchBar( + filters: jobTypeFilters, + setFiltersCallback: _setFilters, + setSearchCallback: _setSearch) + : const Text('Job Link'), toolbarHeight: 70, pinned: true, scrolledUnderElevation: 0, centerTitle: true, expandedHeight: widescreen ? 70 : 120, - backgroundColor: Theme.of(context).colorScheme.surface, bottom: _getBottom(), leading: IconButton( icon: getIconFromThemeMode(themeMode), @@ -100,7 +144,7 @@ class _HomeState extends State { return AlertDialog( title: const Text('About'), backgroundColor: - Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.surface, content: SizedBox( width: 500, child: IntrinsicHeight( @@ -181,7 +225,7 @@ class _HomeState extends State { ), ); } else { - selectedDataTypes = {}; + selectedDataTypesBusiness = {}; Navigator.push( context, MaterialPageRoute( @@ -205,7 +249,7 @@ class _HomeState extends State { builder: (BuildContext context) { return AlertDialog( backgroundColor: - Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.surface, title: Text('Hi, ${payload['username']}!'), content: Text( 'You are logged in as an admin with username ${payload['username']}.'), @@ -263,12 +307,7 @@ class _HomeState extends State { child: FilledButton( child: const Text('Retry'), onPressed: () { - var refreshedData = - fetchBusinessDataOverview(); - setState(() { - refreshBusinessDataOverviewFuture = - refreshedData; - }); + _updateOverviewBusinesses(); }, ), ), @@ -280,7 +319,8 @@ class _HomeState extends State { _isPreviousData = true; return BusinessDisplayPanel( - groupedBusinesses: overviewBusinesses, + groupedBusinesses: + _filterBySearch(overviewBusinesses), widescreen: widescreen, selectable: false); } else if (snapshot.hasError) { @@ -295,7 +335,8 @@ class _HomeState extends State { ConnectionState.waiting) { if (_isPreviousData) { return BusinessDisplayPanel( - groupedBusinesses: overviewBusinesses, + groupedBusinesses: + _filterBySearch(overviewBusinesses), widescreen: widescreen, selectable: false); } else { @@ -303,15 +344,11 @@ class _HomeState extends State { child: Container( padding: const EdgeInsets.all(8.0), alignment: Alignment.center, - // child: const CircularProgressIndicator(), child: const SizedBox( width: 75, height: 75, child: RiveAnimation.asset( 'assets/mdev_triangle_loading.riv'), - // child: RiveAnimation.file( - // 'assets/mdev_triangle_loading.riv', - // ), ), )); } @@ -353,85 +390,6 @@ class _HomeState extends State { return null; } - Widget _searchBar() { - return SizedBox( - width: 800, - height: 50, - child: TextField( - onChanged: (query) { - setState(() { - searchFilter = query; - }); - }, - decoration: InputDecoration( - labelText: 'Search', - hintText: 'Search', - prefixIcon: const Padding( - padding: EdgeInsets.only(left: 8.0), - child: Icon(Icons.search), - ), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), - ), - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - tooltip: 'Filters', - icon: Icon(Icons.filter_list, - color: isFiltered - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onBackground), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - // DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree - backgroundColor: - Theme.of(context).colorScheme.background, - title: const Text('Filter Options'), - content: const FilterChips(), - actions: [ - TextButton( - child: const Text('Reset'), - onPressed: () { - setState(() { - filters = {}; - selectedChips = {}; - isFiltered = false; - }); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Cancel'), - onPressed: () { - selectedChips = Set.from(filters); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Apply'), - onPressed: () { - setState(() { - filters = Set.from(selectedChips); - if (filters.isNotEmpty) { - isFiltered = true; - } else { - isFiltered = false; - } - }); - Navigator.of(context).pop(); - }), - ], - ); - }); - }, - ), - ), - ), - ), - ); - } - PreferredSizeWidget? _getBottom() { if (MediaQuery.sizeOf(context).width <= 1000) { return PreferredSize( @@ -441,7 +399,10 @@ class _HomeState extends State { height: 70, child: Padding( padding: const EdgeInsets.all(10), - child: _searchBar(), + child: BusinessSearchBar( + filters: jobTypeFilters, + setFiltersCallback: _setFilters, + setSearchCallback: _setSearch), ), ), ); diff --git a/fbla_ui/lib/main.dart b/fbla_ui/lib/main.dart index 55475ca..9428d88 100644 --- a/fbla_ui/lib/main.dart +++ b/fbla_ui/lib/main.dart @@ -69,14 +69,14 @@ class _MainAppState extends State { return MaterialApp( title: 'Job Link', themeMode: themeMode, - // themeMode: ThemeMode.light, darkTheme: ThemeData( colorScheme: ColorScheme.dark( brightness: Brightness.dark, primary: Colors.blue, onPrimary: Colors.white, secondary: Colors.blue.shade900, - background: const Color.fromARGB(255, 31, 31, 31), + surface: const Color.fromARGB(255, 31, 31, 31), + surfaceContainer: const Color.fromARGB(255, 40, 40, 40), tertiary: Colors.green.shade900, ), iconTheme: const IconThemeData(color: Colors.white), @@ -89,7 +89,8 @@ class _MainAppState extends State { primary: Colors.blue, onPrimary: Colors.white, secondary: Colors.blue.shade200, - background: Colors.grey.shade300, + surface: Colors.grey.shade200, + surfaceContainer: Colors.grey.shade300, tertiary: Colors.green, ), iconTheme: const IconThemeData(color: Colors.black), diff --git a/fbla_ui/lib/pages/business_detail.dart b/fbla_ui/lib/pages/business_detail.dart index 9ccdcfc..669b961 100644 --- a/fbla_ui/lib/pages/business_detail.dart +++ b/fbla_ui/lib/pages/business_detail.dart @@ -1,6 +1,8 @@ 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_listing.dart'; +import 'package:fbla_ui/pages/listing_detail.dart'; import 'package:fbla_ui/pages/signin_page.dart'; import 'package:fbla_ui/shared.dart'; import 'package:flutter/material.dart'; @@ -111,167 +113,132 @@ class _CreateBusinessDetailState extends State { return ListView( children: [ // Title, logo, desc, website - Card( - clipBehavior: Clip.antiAlias, - 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 getIconFromJobType(widget.clickFromType, 48, - Theme.of(context).colorScheme.onSurface); - }), - ), - ), - ListTile( - leading: const Icon(Icons.link), - title: const Text('Website'), - subtitle: Text(business.website!, - style: const TextStyle(color: Colors.blue)), - onTap: () { - launchUrl(Uri.parse('https://${business.website}')); - }, - ), - ], - ), - ), - // Available positions - Card( - child: Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Available Postitions', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - // Container( - // height: 400, - // width: 300, - ListView( - scrollDirection: Axis.vertical, - shrinkWrap: true, - children: [ - ListTile( - title: Text('Postition 1'), - leading: Icon(Icons.work), - onTap: () { - // launchUrl(Uri.parse('')); - }, - ), - ListTile( - title: Text('Postition 2'), - leading: Icon(Icons.work), - onTap: () { - // launchUrl(Uri.parse('')); - }, - ), - ListTile( - title: Text('Postition 3'), - leading: Icon(Icons.work), - onTap: () { - // launchUrl(Uri.parse('')); - }, - ), - ], - ), - ]), - ), - ), - // Contact info - Visibility( - visible: - (business.contactEmail != null || business.contactPhone != null), + Padding( + padding: const EdgeInsets.only(top: 4.0), child: Card( clipBehavior: Clip.antiAlias, child: Column( children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), - child: Text( - business.contactName ?? 'Contact ${business.name}', - textAlign: TextAlign.left, - style: const TextStyle( - fontSize: 20, fontWeight: FontWeight.bold), - ), - ), - ], - ), - Visibility( - visible: business.contactPhone != null, - child: ListTile( - leading: 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.background, - title: Text(business.contactName!.isEmpty - ? 'Contact ${business.name}?' - : 'Contact ${business.contactName}'), - content: Text(business.contactName!.isEmpty - ? 'Would you like to call or text ${business.name}?' - : '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(); - }), - ], - ); - }); - }, + 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 getIconFromJobType(widget.clickFromType, 48, + Theme.of(context).colorScheme.onSurface); + }), ), ), - Visibility( - visible: business.contactEmail != null, - child: ListTile( - leading: const Icon(Icons.email), - title: Text(business.contactEmail!), - onTap: () { - launchUrl(Uri.parse('mailto:${business.contactEmail}')); - }, - ), + 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 + 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), + ), + ), + ], + ), + Visibility( + visible: business.contactPhone != null, + child: 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(); + }), + ], + ); + }); + }, + ), + ), + ListTile( + leading: const Icon(Icons.email), + title: Text(business.contactEmail), + onTap: () { + launchUrl(Uri.parse('mailto:${business.contactEmail}')); + }, + ), + ], + ), + ), // Location Visibility( child: Card( clipBehavior: Clip.antiAlias, child: ListTile( leading: const Icon(Icons.location_on), - title: Text(business.locationName!), + title: Text(business.locationName), subtitle: Text(business.locationAddress!), onTap: () { launchUrl(Uri.parse(Uri.encodeFull( @@ -282,12 +249,12 @@ class _CreateBusinessDetailState extends State { ), // Notes Visibility( - visible: business.notes != null, + visible: business.notes != null && business.notes != '', child: Card( child: ListTile( leading: const Icon(Icons.notes), title: const Text( - 'Additional Notes:', + 'Additional Notes', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), subtitle: Text(business.notes!), @@ -318,7 +285,7 @@ class _CreateBusinessDetailState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).colorScheme.surface, title: const Text('Are You Sure?'), content: Text('This will permanently delete ${business.name}.'), @@ -332,7 +299,7 @@ class _CreateBusinessDetailState extends State { child: const Text('Yes'), onPressed: () async { String? deleteResult = - await deleteBusiness(business.id, jwt); + await deleteBusiness(business.id); if (deleteResult != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -356,3 +323,105 @@ class _CreateBusinessDetailState extends State { return null; } } + +class _JobList extends StatelessWidget { + final Business business; + + const _JobList({required this.business}); + + @override + Widget build(BuildContext context) { + List<_JobListItem> listItems = []; + for (JobListing listing in business.listings!) { + listItems.add(_JobListItem( + jobListing: listing, + fromBusiness: business, + )); + } + return ListView( + shrinkWrap: true, + children: listItems, + ); + } +} + +class _JobListItem extends StatelessWidget { + final JobListing jobListing; + final Business fromBusiness; + + const _JobListItem({required this.jobListing, required this.fromBusiness}); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: getIconFromJobType( + jobListing.type, 24, Theme.of(context).colorScheme.onSurface), + title: Text(jobListing.name), + subtitle: Text( + jobListing.description, + style: const TextStyle(overflow: TextOverflow.ellipsis), + ), + trailing: _getEditIcon(context, fromBusiness, jobListing), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => JobListingDetail( + listing: jobListing, + fromBusiness: fromBusiness, + ))); + }, + ); + } + + Widget? _getEditIcon( + BuildContext context, Business fromBusiness, JobListing inputListing) { + if (loggedIn) { + return IconButton( + icon: const Icon( + Icons.edit, + ), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => CreateEditJobListing( + inputBusiness: fromBusiness, + inputJobListing: inputListing, + ))); + }, + ); + } + return null; + } +} + +class _GetListingsTitle extends StatelessWidget { + final Business fromBusiness; + + const _GetListingsTitle(this.fromBusiness); + + @override + Widget build(BuildContext context) { + if (!loggedIn) { + return const Text('Available Postitions', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)); + } else { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Available Postitions', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + Padding( + padding: const EdgeInsets.only(right: 24.0), + child: IconButton( + icon: const Icon(Icons.add), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => CreateEditJobListing( + inputBusiness: fromBusiness, + ))); + }, + ), + ) + ], + ); + } + } +} diff --git a/fbla_ui/lib/pages/create_edit_business.dart b/fbla_ui/lib/pages/create_edit_business.dart index 613a51b..4f5b6a9 100644 --- a/fbla_ui/lib/pages/create_edit_business.dart +++ b/fbla_ui/lib/pages/create_edit_business.dart @@ -101,9 +101,9 @@ class _CreateEditBusinessState extends State { // business.contactName = 'Contact ${business.name}'; // } if (widget.inputBusiness != null) { - result = await editBusiness(business, jwt); + result = await editBusiness(business); } else { - result = await createBusiness(business, jwt); + result = await createBusiness(business); } setState(() { _isLoading = false; @@ -159,7 +159,7 @@ class _CreateEditBusinessState extends State { return getIconFromJobType( widget.clickFromType ?? JobType.other, 48, - Theme.of(context).colorScheme.onBackground); + Theme.of(context).colorScheme.onSurface); }), ), ), @@ -202,13 +202,13 @@ class _CreateEditBusinessState extends State { AutovalidateMode.onUserInteraction, keyboardType: TextInputType.url, onChanged: (inputUrl) { - setState(() { - business.website = Uri.encodeFull(inputUrl - .toLowerCase() - .replaceAll('https://', '') - .replaceAll('http://', '') - .replaceAll('www.', '')); - }); + 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(); @@ -362,48 +362,6 @@ class _CreateEditBusinessState extends State { ), ), - // Business Type Dropdown - - // 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, - // controller: businessTypeController, - // label: const Text('Business Type'), - // dropdownMenuEntries: const [ - // DropdownMenuEntry( - // value: BusinessType.food, - // label: 'Food Related'), - // DropdownMenuEntry( - // value: BusinessType.shop, - // label: 'Shop'), - // DropdownMenuEntry( - // value: BusinessType.outdoors, - // label: 'Outdoors'), - // DropdownMenuEntry( - // value: BusinessType.manufacturing, - // label: 'Manufacturing'), - // DropdownMenuEntry( - // value: BusinessType.entertainment, - // label: 'Entertainment'), - // DropdownMenuEntry( - // value: BusinessType.other, - // label: 'Other'), - // ], - // onSelected: (inputType) { - // business.type = inputType!; - // }, - // ), - // ], - // ), - // ), Padding( padding: const EdgeInsets.only( left: 8.0, right: 8.0, bottom: 8.0), diff --git a/fbla_ui/lib/pages/create_edit_listing.dart b/fbla_ui/lib/pages/create_edit_listing.dart new file mode 100644 index 0000000..b98432d --- /dev/null +++ b/fbla_ui/lib/pages/create_edit_listing.dart @@ -0,0 +1,431 @@ +import 'package:fbla_ui/api_logic.dart'; +import 'package:fbla_ui/main.dart'; +import 'package:fbla_ui/shared.dart'; +import 'package:flutter/material.dart'; +import 'package:rive/rive.dart'; + +class CreateEditJobListing extends StatefulWidget { + final JobListing? inputJobListing; + final Business inputBusiness; + + const CreateEditJobListing( + {super.key, this.inputJobListing, required this.inputBusiness}); + + @override + State createState() => _CreateEditJobListingState(); +} + +class _CreateEditJobListingState extends State { + late Future getBusinessNameMapping; + late TextEditingController _nameController; + late TextEditingController _descriptionController; + late TextEditingController _wageController; + late TextEditingController _linkController; + List nameMapping = []; + String? businessErrorText; + + JobListing listing = JobListing( + id: null, + businessId: null, + name: 'Job Listing', + description: 'Add details about the business below.', + type: JobType.other, + wage: null, + link: null); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + if (widget.inputJobListing != null) { + listing = JobListing.copy(widget.inputJobListing!); + _nameController = TextEditingController(text: listing.name); + _descriptionController = TextEditingController(text: listing.description); + } else { + _nameController = TextEditingController(); + _descriptionController = TextEditingController(); + } + _wageController = TextEditingController(text: listing.wage); + _linkController = TextEditingController(text: listing.link); + getBusinessNameMapping = fetchBusinessNames(); + } + + final formKey = GlobalKey(); + final TextEditingController jobTypeController = TextEditingController(); + final TextEditingController businessController = TextEditingController(); + + @override + Widget build(BuildContext context) { + listing.businessId = widget.inputBusiness.id; + return PopScope( + canPop: !_isLoading, + onPopInvoked: _handlePop, + child: Form( + key: formKey, + child: Scaffold( + appBar: AppBar( + title: (widget.inputJobListing != null) + ? 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 (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())); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Check field inputs!'), + width: 200, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + ); + } + }, + ), + body: FutureBuilder( + future: getBusinessNameMapping, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasData) { + if (snapshot.data.runtimeType == String) { + return 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: () async { + var refreshedData = fetchBusinessNames(); + await refreshedData; + setState(() { + getBusinessNameMapping = refreshedData; + }); + }, + ), + ), + ]), + ); + } + + nameMapping = snapshot.data; + + return ListView( + children: [ + Center( + child: SizedBox( + width: 1000, + child: Column( + children: [ + ListTile( + title: Text(listing.name, + textAlign: TextAlign.left, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold)), + subtitle: Text( + listing.description, + textAlign: TextAlign.left, + ), + leading: ClipRRect( + borderRadius: BorderRadius.circular(6.0), + child: Image.network( + width: 48, + height: 48, + listing.businessId != null + ? '$apiAddress/logos/${listing.businessId}' + : '', + errorBuilder: (BuildContext context, + Object exception, + StackTrace? stackTrace) { + return getIconFromJobType( + listing.type, + 48, + Theme.of(context) + .colorScheme + .onSurface); + }), + ), + ), + // Business Type Dropdown + 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, + children: [ + const Text('Type of Job', + style: + TextStyle(fontSize: 16)), + DropdownMenu( + initialSelection: listing.type, + controller: jobTypeController, + label: const Text('Job Type'), + dropdownMenuEntries: [ + for (JobType type + in JobType.values) + DropdownMenuEntry( + value: type, + label: + getNameFromJobType( + type)) + ], + onSelected: (inputType) { + setState(() { + listing.type = inputType!; + }); + }, + ), + ], + ), + ), + 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( + initialSelection: + widget.inputBusiness.id, + controller: businessController, + label: const Text('Business'), + dropdownMenuEntries: [ + for (Map map + in nameMapping) + DropdownMenuEntry( + value: map['id']!, + label: map['name']) + ], + onSelected: (inputType) { + setState(() { + listing.businessId = + inputType!; + }); + }, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 8.0, right: 8.0), + child: TextFormField( + controller: _nameController, + autovalidateMode: AutovalidateMode + .onUserInteraction, + maxLength: 30, + onChanged: (inputName) { + setState(() { + listing.name = inputName; + }); + }, + onTapOutside: + (PointerDownEvent event) { + FocusScope.of(context).unfocus(); + }, + decoration: const InputDecoration( + labelText: + 'Job Listing Name (required)', + ), + validator: (value) { + if (value != null && + value.isEmpty) { + return 'Name is required'; + } + return null; + }, + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 8.0, right: 8.0), + child: TextFormField( + controller: _descriptionController, + autovalidateMode: AutovalidateMode + .onUserInteraction, + maxLength: 500, + maxLines: null, + onChanged: (inputDesc) { + setState(() { + listing.description = inputDesc; + }); + }, + onTapOutside: + (PointerDownEvent event) { + FocusScope.of(context).unfocus(); + }, + decoration: const InputDecoration( + labelText: + 'Job Listing Description (required)', + ), + validator: (value) { + if (value != null && + value.isEmpty) { + return 'Description is required'; + } + return null; + }, + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + bottom: 8.0), + child: TextFormField( + controller: _wageController, + onChanged: (input) { + setState(() { + listing.wage = input; + }); + }, + onTapOutside: + (PointerDownEvent event) { + FocusScope.of(context).unfocus(); + }, + decoration: const InputDecoration( + labelText: 'Wage Information', + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + bottom: 8.0), + child: TextFormField( + controller: _linkController, + autovalidateMode: AutovalidateMode + .onUserInteraction, + keyboardType: TextInputType.url, + onChanged: (inputUrl) { + if (listing.link != null && + listing.link != '') { + listing.link = + Uri.encodeFull(inputUrl); + if (!listing.link! + .contains('http://') && + !listing.link! + .contains('https://')) { + listing.link = + 'https://${listing.link}'; + } + } + }, + onTapOutside: + (PointerDownEvent event) { + FocusScope.of(context).unfocus(); + }, + decoration: const InputDecoration( + labelText: + 'Additional Information Link', + ), + ), + ), + ], + ), + ), + const SizedBox( + height: 75, + ) + ], + ), + ), + ), + ], + ); + } else if (snapshot.hasError) { + return 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) { + return 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 const Padding( + padding: EdgeInsets.only(left: 16.0, right: 16.0), + child: Text('Error when loading data!'), + ); + }), + )), + ); + } + + void _handlePop(bool didPop) { + if (!didPop) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + width: 400, + behavior: SnackBarBehavior.floating, + content: Text('Please wait for it to save.'), + ), + ); + } + } +} diff --git a/fbla_ui/lib/pages/export_data.dart b/fbla_ui/lib/pages/export_data.dart index 57a15d9..a80e5e0 100644 --- a/fbla_ui/lib/pages/export_data.dart +++ b/fbla_ui/lib/pages/export_data.dart @@ -1,3 +1,4 @@ +// import 'dart:html' as html; import 'dart:convert'; import 'dart:io'; @@ -10,10 +11,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; - -bool isDataTypesFiltered = false; -bool isBusinessesFiltered = true; -bool _isLoading = false; +import 'package:rive/rive.dart'; class ExportData extends StatefulWidget { final Map> groupedBusinesses; @@ -25,21 +23,91 @@ class ExportData extends StatefulWidget { } class _ExportDataState extends State { + String documentType = 'Business'; late Future refreshBusinessDataFuture; + bool _isPreviousData = false; + late Map> overviewBusinesses; + Set jobTypeFilters = {}; + String searchQuery = ''; + Set selectedDataTypesJob = {}; + Set selectedDataTypesBusiness = {}; + + Future _setFilters(Set filters) async { + setState(() { + jobTypeFilters = filters; + }); + _updateOverviewBusinesses(); + } + + Future _updateOverviewBusinesses() async { + var refreshedData = + fetchBusinessDataOverview(typeFilters: jobTypeFilters.toList()); + await refreshedData; + setState(() { + refreshBusinessDataFuture = refreshedData; + }); + } + + Future _setSearch(String search) async { + setState(() { + searchQuery = search; + }); + _updateOverviewBusinesses(); + } + + Map> _filterBySearch( + Map> businesses) { + Map> 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; + } @override void initState() { super.initState(); refreshBusinessDataFuture = fetchBusinessDataOverview(); - _isLoading = false; selectedBusinesses = {}; } + void _setStateCallbackReset() { + setState(() { + selectedDataTypesBusiness = {}; + selectedDataTypesJob = {}; + documentType = 'Business'; + }); + } + + void _setStateCallbackApply(String docType, Set dataFiltersJob, + Set dataFiltersBusiness) { + setState(() { + selectedDataTypesBusiness = dataFiltersBusiness; + selectedDataTypesJob = dataFiltersJob; + documentType = docType; + }); + } + @override Widget build(BuildContext context) { + bool widescreen = MediaQuery.sizeOf(context).width >= 1000; return Scaffold( - floatingActionButton: _FAB(groupedBusinesses: widget.groupedBusinesses), + floatingActionButton: _FAB( + groupedBusinesses: widget.groupedBusinesses, + documentType: documentType, + selectedDataTypesBusiness: selectedDataTypesBusiness, + selectedDataTypesJob: selectedDataTypesJob, + ), body: CustomScrollView( slivers: [ SliverAppBar( @@ -49,7 +117,7 @@ class _ExportDataState extends State { pinned: true, centerTitle: true, expandedHeight: 120, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).colorScheme.surface, actions: [ IconButton( icon: const Icon(Icons.settings), @@ -57,57 +125,84 @@ class _ExportDataState extends State { showDialog( context: context, builder: (BuildContext context) { - return AlertDialog( - // DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree - backgroundColor: - Theme.of(context).colorScheme.background, - title: const Text('Data Types'), - content: const SizedBox( - width: 400, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Data Columns you would like to show on the datasheet'), - FilterDataTypeChips(), - ], + Set dataFiltersBusinessTmp = + Set.from( + selectedDataTypesBusiness); + Set dataFiltersJobTmp = + Set.from(selectedDataTypesJob); + String docTypeTmp = documentType; + return StatefulBuilder(builder: (context, setState) { + void segmentedCallback(String docType) { + setState(() { + docTypeTmp = docType; + }); + } + + void chipsCallback( + {Set? selectedDataTypesJob, + Set? + selectedDataTypesBusiness}) { + if (selectedDataTypesJob != null) { + dataFiltersJobTmp = selectedDataTypesJob; + } + if (selectedDataTypesBusiness != null) { + dataFiltersBusinessTmp = + selectedDataTypesBusiness; + } + } + + return AlertDialog( + // DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree + backgroundColor: + Theme.of(context).colorScheme.surface, + title: const Text('Export Settings'), + content: SizedBox( + width: 450, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Document Type:'), + _SegmentedButton( + callback: segmentedCallback, + docType: docTypeTmp, + ), + const Text( + 'Data Columns you would like to show on the datasheet:'), + Padding( + padding: const EdgeInsets.all(8.0), + child: _FilterDataTypeChips( + docTypeTmp, + dataFiltersJobTmp, + dataFiltersBusinessTmp, + chipsCallback), + ), + ], + ), ), - ), - actions: [ - TextButton( - child: const Text('Reset'), - onPressed: () { - setState(() { - dataTypeFilters = {}; - selectedDataTypes = {}; - isDataTypesFiltered = false; - }); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Cancel'), - onPressed: () { - selectedDataTypes = Set.from(dataTypeFilters); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Apply'), - onPressed: () { - setState(() { - selectedDataTypes = - sortDataTypes(selectedDataTypes); - dataTypeFilters = - Set.from(selectedDataTypes); - if (dataTypeFilters.isNotEmpty) { - isDataTypesFiltered = true; - } else { - isDataTypesFiltered = false; - } - }); - Navigator.of(context).pop(); - }), - ], - ); + actions: [ + TextButton( + child: const Text('Reset'), + onPressed: () { + _setStateCallbackReset(); + Navigator.of(context).pop(); + }), + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }), + TextButton( + child: const Text('Apply'), + onPressed: () { + _setStateCallbackApply( + docTypeTmp, + dataFiltersJobTmp, + dataFiltersBusinessTmp); + Navigator.of(context).pop(); + }), + ], + ); + }); }); }, ), @@ -119,86 +214,88 @@ class _ExportDataState extends State { width: 1000, child: Padding( padding: const EdgeInsets.all(10), - child: TextField( - onChanged: (query) { - setState(() { - searchFilter = query; - }); - }, - decoration: InputDecoration( - labelText: 'Search', - hintText: 'Search', - prefixIcon: const Padding( - padding: EdgeInsets.only(left: 8.0), - child: Icon(Icons.search), - ), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), - ), - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - icon: Icon(Icons.filter_list, - color: isFiltered - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onBackground), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - // DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree - backgroundColor: Theme.of(context) - .colorScheme - .background, - title: const Text('Filter Options'), - content: const FilterChips(), - actions: [ - TextButton( - child: const Text('Reset'), - onPressed: () { - setState(() { - filters = {}; - selectedChips = {}; - isFiltered = false; - }); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Cancel'), - onPressed: () { - selectedChips = Set.from(filters); - Navigator.of(context).pop(); - }), - TextButton( - child: const Text('Apply'), - onPressed: () { - setState(() { - filters = selectedChips; - if (filters.isNotEmpty) { - isFiltered = true; - } else { - isFiltered = false; - } - }); - Navigator.of(context).pop(); - }), - ], - ); - }); - }, - ), - ), - ), - ), + child: BusinessSearchBar( + filters: jobTypeFilters, + setFiltersCallback: _setFilters, + setSearchCallback: _setSearch), ), ), ), ), - BusinessDisplayPanel( - groupedBusinesses: widget.groupedBusinesses, - widescreen: MediaQuery.sizeOf(context).width >= 1000, - selectable: true), + FutureBuilder( + future: refreshBusinessDataFuture, + 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: true); + } 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: true); + } 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: 100, @@ -210,10 +307,67 @@ class _ExportDataState extends State { } } -class _FAB extends StatefulWidget { - final Map> groupedBusinesses; +class _SegmentedButton extends StatefulWidget { + final void Function(String) callback; + final String docType; - const _FAB({required this.groupedBusinesses}); + const _SegmentedButton({required this.callback, required this.docType}); + + @override + State<_SegmentedButton> createState() => _SegmentedButtonState(); +} + +class _SegmentedButtonState extends State<_SegmentedButton> { + Set _selected = {}; + + void updateSelected(Set newSelection) { + setState(() { + _selected = newSelection; + }); + widget.callback(newSelection.first); + } + + @override + void initState() { + super.initState(); + _selected = {widget.docType}; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: SegmentedButton( + segments: const >[ + ButtonSegment( + value: 'Business', + label: Text('Businesses'), + icon: Icon(Icons.business)), + ButtonSegment( + value: 'Job Listing', + label: Text('Job Listings'), + icon: Icon(Icons.work)) + ], + selected: _selected, + onSelectionChanged: updateSelected, + style: SegmentedButton.styleFrom( + side: BorderSide(color: Theme.of(context).colorScheme.secondary), + )), + ); + } +} + +class _FAB extends StatefulWidget { + final String documentType; + final Map> groupedBusinesses; + final Set selectedDataTypesJob; + final Set selectedDataTypesBusiness; + + const _FAB( + {required this.groupedBusinesses, + required this.documentType, + required this.selectedDataTypesJob, + required this.selectedDataTypesBusiness}); @override State<_FAB> createState() => _FABState(); @@ -221,6 +375,7 @@ class _FAB extends StatefulWidget { class _FABState extends State<_FAB> { List allBusinesses = []; + bool _isLoading = false; @override void initState() { @@ -234,352 +389,462 @@ class _FABState extends State<_FAB> { @override Widget build(BuildContext context) { return FloatingActionButton( - child: _isLoading - ? const Padding( - padding: EdgeInsets.all(16.0), - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 3.0, - ), - ) - : const Icon(Icons.save_alt), - onPressed: () async { - setState(() { - _isLoading = true; - }); + child: _isLoading + ? const Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 3.0, + ), + ) + : const Icon(Icons.save_alt), + onPressed: () async { + setState(() { + _isLoading = true; + }); - try { - DateTime dateTime = DateTime.now(); - String minute = '00'; - if (dateTime.minute.toString().length < 2) { - minute = '0${dateTime.minute}'; - } else { - minute = dateTime.minute.toString(); - } - - String time = dateTime.hour <= 13 - ? '${dateTime.hour}:${minute}AM' - : '${dateTime.hour - 12}:${minute}PM'; - String fileName = - 'Business Data - ${dateTime.month}-${dateTime.day}-${dateTime.year} $time.pdf'; - - final pdf = pw.Document(); - var svgBytes = await marinoDevLogo(); - selectedDataTypes = sortDataTypes(selectedDataTypes); - - List headers = []; - if (selectedDataTypes.isEmpty) { - dataTypeFilters.addAll(DataType.values); - } else { - for (var filter in selectedDataTypes) { - dataTypeFilters.add(filter); - } - } - for (var filter in dataTypeFilters) { - headers.add(pw.Padding( - child: pw.Text(dataTypeFriendly[filter]!, - style: const pw.TextStyle(fontSize: 10)), - padding: const pw.EdgeInsets.all(4.0))); - } - - List rows = []; + Set generateBusinesses = {}; if (selectedBusinesses.isEmpty) { - selectedBusinesses.addAll(allBusinesses); - isBusinessesFiltered = false; + generateBusinesses = Set.from(allBusinesses); } else { - isBusinessesFiltered = true; + generateBusinesses = selectedBusinesses; } - double remainingSpace = 744; + await _generatePDF(context, widget.documentType, generateBusinesses, + widget.selectedDataTypesBusiness, widget.selectedDataTypesJob); - if (dataTypeFilters.contains(DataType.logo)) { - remainingSpace -= 32; - } - // if (dataTypeFilters.contains(DataType.type)) { - // remainingSpace -= 56; - // } - if (dataTypeFilters.contains(DataType.contactName)) { - remainingSpace -= 72; - } - if (dataTypeFilters.contains(DataType.contactPhone)) { - remainingSpace -= 76; - } - - double nameWidth = 0; - double websiteWidth = 0; - double contactEmailWidth = 0; - double notesWidth = 0; - double descriptionWidth = 0; - if (dataTypeFilters.contains(DataType.name)) { - nameWidth = (remainingSpace / 6); - } - if (dataTypeFilters.contains(DataType.website)) { - websiteWidth = (remainingSpace / 5); - } - if (dataTypeFilters.contains(DataType.contactEmail)) { - contactEmailWidth = (remainingSpace / 5); - } - if (dataTypeFilters.contains(DataType.notes)) { - notesWidth = (remainingSpace / 7); - } - remainingSpace -= - (nameWidth + websiteWidth + contactEmailWidth + notesWidth); - if (dataTypeFilters.contains(DataType.description)) { - descriptionWidth = remainingSpace; - } - - Map columnWidths = {}; - - int columnNum = -1; - for (var dataType in dataTypeFilters) { - pw.TableColumnWidth width = const pw.FixedColumnWidth(0); - if (dataType == DataType.logo) { - width = const pw.FixedColumnWidth(32); - columnNum++; - } else if (dataType == DataType.name) { - width = pw.FixedColumnWidth(nameWidth); - columnNum++; - } else if (dataType == DataType.description) { - width = pw.FixedColumnWidth(descriptionWidth); - columnNum++; - // } else if (dataType == DataType.type) { - // width = const pw.FixedColumnWidth(56); - // columnNum++; - } else if (dataType == DataType.website) { - width = pw.FixedColumnWidth(websiteWidth); - columnNum++; - } else if (dataType == DataType.contactName) { - width = const pw.FixedColumnWidth(72); - columnNum++; - } else if (dataType == DataType.contactEmail) { - width = pw.FixedColumnWidth(contactEmailWidth); - columnNum++; - } else if (dataType == DataType.contactPhone) { - width = const pw.FixedColumnWidth(76); - columnNum++; - } else if (dataType == DataType.notes) { - width = pw.FixedColumnWidth(notesWidth); - columnNum++; - } - - columnWidths.addAll({columnNum: width}); - } - - for (var business in selectedBusinesses) { - List data = []; - bool hasLogo = false; - Uint8List businessLogo = Uint8List(0); - if (dataTypeFilters.contains(DataType.logo)) { - try { - var apiLogo = await getLogo(business.id); - if (apiLogo.runtimeType != String) { - businessLogo = apiLogo; - hasLogo = true; - } - } catch (e) { - if (kDebugMode) { - print('Logo not available! $e'); - } - } - } - if (dataTypeFilters.contains(DataType.name)) { - data.add(pw.Padding( - child: pw.Text( - business.name, - // style: const pw.TextStyle(fontSize: 10) - ), - padding: const pw.EdgeInsets.all(4.0))); - } - if (dataTypeFilters.contains(DataType.description)) { - pw.TextStyle style = const pw.TextStyle(fontSize: 9); - if (business.description.length >= 200) { - style = const pw.TextStyle(fontSize: 8); - } - if (business.description.length >= 400) { - style = const pw.TextStyle(fontSize: 7); - } - data.add(pw.Padding( - child: pw.Text( - business.description, - style: style, - ), - padding: const pw.EdgeInsets.all(4.0))); - } - // if (dataTypeFilters.contains(DataType.type)) { - // data.add(pw.Padding( - // child: pw.Text( - // business.type.name, - // // style: const pw.TextStyle(fontSize: 10) - // ), - // padding: const pw.EdgeInsets.all(4.0))); - // } - if (dataTypeFilters.contains(DataType.website)) { - data.add(pw.Padding( - child: pw.Text( - business.website ?? '', - // style: const pw.TextStyle(fontSize: 10) - ), - padding: const pw.EdgeInsets.all(4.0))); - } - if (dataTypeFilters.contains(DataType.contactName)) { - data.add(pw.Padding( - child: pw.Text( - business.contactName ?? '', - // style: const pw.TextStyle(fontSize: 10) - ), - padding: const pw.EdgeInsets.all(4.0))); - } - if (dataTypeFilters.contains(DataType.contactEmail)) { - data.add(pw.Padding( - child: pw.Text( - business.contactEmail ?? '', - // style: const pw.TextStyle(fontSize: 10) - ), - padding: const pw.EdgeInsets.all(4.0))); - } - if (dataTypeFilters.contains(DataType.contactPhone)) { - data.add(pw.Padding( - child: pw.Text( - business.contactPhone ?? '', - // style: const pw.TextStyle(fontSize: 10) - ), - padding: const pw.EdgeInsets.all(4.0))); - } - if (dataTypeFilters.contains(DataType.notes)) { - pw.TextStyle style = const pw.TextStyle(fontSize: 9); - if (business.description.length >= 200) { - style = const pw.TextStyle(fontSize: 8); - } - data.add(pw.Padding( - child: pw.Text(business.notes ?? '', style: style), - padding: const pw.EdgeInsets.all(4.0))); - } - - if (dataTypeFilters.contains(DataType.logo)) { - if (hasLogo) { - rows.add(pw.TableRow( - children: [ - pw.Padding( - child: pw.ClipRRect( - child: pw.Image(pw.MemoryImage(businessLogo), - height: 24, width: 24), - horizontalRadius: 4, - verticalRadius: 4), - padding: const pw.EdgeInsets.all(4.0)), - ...data - ], - )); - } else { - rows.add(pw.TableRow( - children: [ - // pw.Padding( - // child: getPwIconFromType( - // business.type, 24, PdfColors.black), - // padding: const pw.EdgeInsets.all(4.0)), - ...data - ], - )); - } - } else { - rows.add(pw.TableRow( - children: data, - )); - } - } - - var themeIcon = pw.ThemeData.withFont( - base: await PdfGoogleFonts.notoSansDisplayMedium(), - icons: await PdfGoogleFonts.materialIcons()); - - var finaltheme = themeIcon.copyWith( - defaultTextStyle: const pw.TextStyle(fontSize: 9), - ); - pdf.addPage(pw.MultiPage( - theme: finaltheme, - // theme: pw.ThemeData( - // tableCell: const pw.TextStyle(fontSize: 4), - // defaultTextStyle: const pw.TextStyle(fontSize: 4), - // header0: const pw.TextStyle(fontSize: 4), - // paragraphStyle: const pw.TextStyle(fontSize: 4), - // ), - // theme: pw.ThemeData.withFont( - // icons: await PdfGoogleFonts.materialIcons()), - pageFormat: PdfPageFormat.letter, - orientation: pw.PageOrientation.landscape, - margin: const pw.EdgeInsets.all(24), - build: (pw.Context context) { - return [ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.SvgImage(svg: utf8.decode(svgBytes), height: 40), - pw.Padding( - padding: const pw.EdgeInsets.all(8.0), - child: pw.Text('Business Datasheet', - style: pw.TextStyle( - fontSize: 32, - fontWeight: pw.FontWeight.bold)), - ), - pw.Text( - 'Generated on ${dateTime.month}/${dateTime.day}/${dateTime.year} at $time', - style: const pw.TextStyle(fontSize: 12), - textAlign: pw.TextAlign.right), - // - ]), - pw.Table( - columnWidths: columnWidths, - // defaultColumnWidth: pw.IntrinsicColumnWidth(), - border: const pw.TableBorder( - bottom: pw.BorderSide(), - left: pw.BorderSide(), - right: pw.BorderSide(), - top: pw.BorderSide(), - horizontalInside: pw.BorderSide(), - verticalInside: pw.BorderSide()), - children: [ - pw.TableRow( - decoration: - const pw.BoxDecoration(color: PdfColors.blue400), - children: headers, - repeat: true, - ), - ...rows, - ]), - ]; - })); - - Uint8List pdfBytes = await pdf.save(); - - if (kIsWeb) { - await Printing.sharePdf( - bytes: await pdf.save(), - filename: fileName, - ); - } else { - var dir = await getTemporaryDirectory(); - var tempDir = dir.path; - - File pdfFile = File('$tempDir/$fileName'); - pdfFile.writeAsBytesSync(pdfBytes); - - OpenFilex.open(pdfFile.path); - } - - if (!isBusinessesFiltered) { - selectedBusinesses = {}; - } setState(() { _isLoading = false; }); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('Error generating PDF! $e'), - width: 300, - behavior: SnackBarBehavior.floating, - duration: const Duration(seconds: 2), - )); - } - }, + }); + } +} + +class _FilterDataTypeChips extends StatefulWidget { + final String documentType; + final Set selectedDataTypesJob; + final Set selectedDataTypesBusiness; + final void Function( + {Set? selectedDataTypesJob, + Set? selectedDataTypesBusiness}) updateCallback; + + const _FilterDataTypeChips(this.documentType, this.selectedDataTypesJob, + this.selectedDataTypesBusiness, this.updateCallback); + + @override + State<_FilterDataTypeChips> createState() => _FilterDataTypeChipsState(); +} + +class _FilterDataTypeChipsState extends State<_FilterDataTypeChips> { + List filterDataTypeChips() { + List chips = []; + + if (widget.documentType == 'Business') { + for (var type in DataTypeBusiness.values) { + chips.add(Padding( + padding: const EdgeInsets.only( + left: 3.0, right: 3.0, bottom: 3.0, top: 3.0), + child: FilterChip( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: Theme.of(context).colorScheme.secondary)), + label: Text(dataTypeFriendlyBusiness[type]!), + showCheckmark: false, + selected: widget.selectedDataTypesBusiness.contains(type), + onSelected: (bool selected) { + setState(() { + if (selected) { + widget.selectedDataTypesBusiness.add(type); + } else { + widget.selectedDataTypesBusiness.remove(type); + } + }); + widget.updateCallback( + selectedDataTypesBusiness: + widget.selectedDataTypesBusiness); + }), + )); + } + } else if (widget.documentType == 'Job Listing') { + for (var type in DataTypeJob.values) { + chips.add(Padding( + padding: const EdgeInsets.only( + left: 3.0, right: 3.0, bottom: 3.0, top: 3.0), + child: FilterChip( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: Theme.of(context).colorScheme.secondary)), + label: Text(dataTypeFriendlyJob[type]!), + showCheckmark: false, + selected: widget.selectedDataTypesJob.contains(type), + onSelected: (bool selected) { + setState(() { + if (selected) { + widget.selectedDataTypesJob.add(type); + } else { + widget.selectedDataTypesJob.remove(type); + } + }); + widget.updateCallback( + selectedDataTypesJob: widget.selectedDataTypesJob); + }), + )); + } + } + return chips; + } + + @override + Widget build(BuildContext context) { + return Wrap( + children: filterDataTypeChips(), ); } } + +Future _generatePDF( + BuildContext context, + String documentType, + Set? selectedBusinesses, + Set? dataTypesBusinessInput, + Set? dataTypesJobInput) async { + Set dataTypesBusiness = {}; + Set dataTypesJob = {}; + List headerColumns = []; + List tableRows = []; + List businesses = await fetchBusinesses( + selectedBusinesses!.map((business) => business.id).toList()); + + if (documentType == 'Business') { + dataTypesBusiness = Set.from(dataTypesBusinessInput!); + if (dataTypesBusiness.isEmpty) { + dataTypesBusiness.addAll(DataTypeBusiness.values); + } + dataTypesBusiness = sortDataTypesBusiness(dataTypesBusiness); + + for (Business business in businesses) { + List businessRow = []; + if (dataTypesBusiness.contains(DataTypeBusiness.logo)) { + var apiLogo = await getLogo(business.id); + if (apiLogo.runtimeType != String) { + businessRow.add(pw.Padding( + child: pw.ClipRRect( + child: + pw.Image(pw.MemoryImage(apiLogo), height: 24, width: 24), + horizontalRadius: 4, + verticalRadius: 4), + padding: const pw.EdgeInsets.all(4.0))); + } else { + businessRow.add(pw.Padding( + child: pw.Icon(const pw.IconData(0xe0af), size: 24), + padding: const pw.EdgeInsets.all(4.0))); + } + } + for (DataTypeBusiness dataType in dataTypesBusiness) { + if (dataType != DataTypeBusiness.logo) { + businessRow.add(pw.Padding( + child: pw.Text(businessValueFromDataType(business, dataType)), + padding: const pw.EdgeInsets.all(4.0))); + } + } + tableRows.add(pw.TableRow(children: businessRow)); + } + + for (var filter in dataTypesBusiness) { + headerColumns.add(pw.Padding( + child: pw.Text(dataTypeFriendlyBusiness[filter]!, + style: const pw.TextStyle(fontSize: 10)), + padding: const pw.EdgeInsets.all(4.0))); + } + } else if (documentType == 'Job Listing') { + dataTypesJob = Set.from(dataTypesJobInput!); + if (dataTypesJob.isEmpty) { + dataTypesJob.addAll(DataTypeJob.values); + } + List dataTypesJobList = + sortDataTypesJob(dataTypesJob).toList(); + + List> nameMapping = await fetchBusinessNames(); + + for (Business business in businesses) { + for (JobListing job in business.listings!) { + List jobRow = []; + for (DataTypeJob dataType in dataTypesJobList) { + jobRow.add(pw.Padding( + child: pw.Text(jobValueFromDataType(job, dataType, nameMapping)), + padding: const pw.EdgeInsets.all(4.0))); + } + tableRows.add(pw.TableRow(children: jobRow)); + } + } + + for (var filter in dataTypesJobList) { + headerColumns.add(pw.Padding( + child: pw.Text(dataTypeFriendlyJob[filter]!, + style: const pw.TextStyle(fontSize: 10)), + padding: const pw.EdgeInsets.all(4.0))); + } + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text( + 'Could not identify document type! Please select a type in the generation settings.'))); + return; + } + + // Final Generation + DateTime dateTime = DateTime.now(); + String minute = '00'; + if (dateTime.minute.toString().length < 2) { + minute = '0${dateTime.minute}'; + } else { + minute = dateTime.minute.toString(); + } + + String time = dateTime.hour <= 12 + ? '${dateTime.hour}:${minute}AM' + : '${dateTime.hour - 12}:${minute}PM'; + String fileName = + '$documentType Data - ${dateTime.month}-${dateTime.day}-${dateTime.year} $time.pdf'; + + final pdf = pw.Document(); + var svgBytes = await marinoDevLogo(); + + var themeIcon = pw.ThemeData.withFont( + base: await PdfGoogleFonts.notoSansDisplayMedium(), + icons: await PdfGoogleFonts.materialIcons()); + + var finalTheme = themeIcon.copyWith( + defaultTextStyle: const pw.TextStyle(fontSize: 9), + ); + + pdf.addPage(pw.MultiPage( + theme: finalTheme, + pageFormat: PdfPageFormat.letter, + orientation: pw.PageOrientation.landscape, + margin: const pw.EdgeInsets.all(24), + build: (pw.Context context) { + return [ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.SvgImage(svg: utf8.decode(svgBytes), height: 40), + pw.Padding( + padding: const pw.EdgeInsets.all(8.0), + child: pw.Text('$documentType Datasheet', + style: pw.TextStyle( + fontSize: 32, fontWeight: pw.FontWeight.bold)), + ), + pw.Text( + 'Generated on ${dateTime.month}/${dateTime.day}/${dateTime.year} at $time', + style: const pw.TextStyle(fontSize: 12), + textAlign: pw.TextAlign.right), + // + ]), + pw.Table( + columnWidths: documentType == 'Business' + ? _businessColumnSizes(dataTypesBusiness) + : _jobColumnSizes(dataTypesJob), + border: const pw.TableBorder( + bottom: pw.BorderSide(), + left: pw.BorderSide(), + right: pw.BorderSide(), + top: pw.BorderSide(), + horizontalInside: pw.BorderSide(), + verticalInside: pw.BorderSide()), + children: [ + pw.TableRow( + decoration: const pw.BoxDecoration(color: PdfColors.blue400), + children: headerColumns, + repeat: true, + ), + ...tableRows, + ]) + ]; + })); + + Uint8List pdfBytes = await pdf.save(); + + if (kIsWeb) { + // List fileInts = List.from(pdfBytes); + // html.AnchorElement( + // href: + // 'data:application/octet-stream;charset=utf-16le;base64,${base64.encode(fileInts)}') + // ..setAttribute('download', fileName) + // ..click(); + + await Printing.sharePdf( + bytes: await pdf.save(), + filename: fileName, + ); + } else { + var dir = await getTemporaryDirectory(); + var tempDir = dir.path; + + File pdfFile = File('$tempDir/$fileName'); + pdfFile.writeAsBytesSync(pdfBytes); + + OpenFilex.open(pdfFile.path); + } +} + +Map _businessColumnSizes( + Set dataTypes) { + double space = 744.0; + Map map = {}; + + if (dataTypes.contains(DataTypeBusiness.logo)) { + space -= 28; + map.addAll({ + dataTypePriorityBusiness[DataTypeBusiness.logo]!: + const pw.FixedColumnWidth(28) + }); + } + if (dataTypes.contains(DataTypeBusiness.contactName)) { + space -= 72; + map.addAll({ + dataTypePriorityBusiness[DataTypeBusiness.contactName]!: + const pw.FixedColumnWidth(72) + }); + } + if (dataTypes.contains(DataTypeBusiness.contactPhone)) { + space -= 76; + map.addAll({ + dataTypePriorityBusiness[DataTypeBusiness.contactPhone]!: + const pw.FixedColumnWidth(76) + }); + } + double leftNum = 0; + if (dataTypes.contains(DataTypeBusiness.name)) { + leftNum += 1; + } + if (dataTypes.contains(DataTypeBusiness.website)) { + leftNum += 1; + } + if (dataTypes.contains(DataTypeBusiness.contactEmail)) { + leftNum += 1; + } + if (dataTypes.contains(DataTypeBusiness.notes)) { + leftNum += 2; + } + if (dataTypes.contains(DataTypeBusiness.description)) { + leftNum += 3; + } + leftNum = space / leftNum; + if (dataTypes.contains(DataTypeBusiness.name)) { + map.addAll({ + dataTypePriorityBusiness[DataTypeBusiness.name]!: + pw.FixedColumnWidth(leftNum) + }); + } + if (dataTypes.contains(DataTypeBusiness.website)) { + map.addAll({ + dataTypePriorityBusiness[DataTypeBusiness.website]!: + pw.FixedColumnWidth(leftNum) + }); + } + if (dataTypes.contains(DataTypeBusiness.contactEmail)) { + map.addAll({ + dataTypePriorityBusiness[DataTypeBusiness.contactEmail]!: + pw.FixedColumnWidth(leftNum) + }); + } + if (dataTypes.contains(DataTypeBusiness.notes)) { + map.addAll({ + dataTypePriorityBusiness[DataTypeBusiness.notes]!: + pw.FixedColumnWidth(leftNum * 2) + }); + } + if (dataTypes.contains(DataTypeBusiness.description)) { + map.addAll({ + dataTypePriorityBusiness[DataTypeBusiness.description]!: + pw.FixedColumnWidth(leftNum * 3) + }); + } + + return map; +} + +Map _jobColumnSizes(Set dataTypes) { + Map map = {}; + List sortedDataTypes = sortDataTypesJob(dataTypes).toList(); + + if (dataTypes.contains(DataTypeJob.businessName)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.businessName) + .first): const pw.FractionColumnWidth(0.2) + }); + } + if (dataTypes.contains(DataTypeJob.name)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.name) + .first): const pw.FractionColumnWidth(0.2) + }); + } + if (dataTypes.contains(DataTypeJob.description)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.description) + .first): const pw.FractionColumnWidth(0.4) + }); + } + if (dataTypes.contains(DataTypeJob.wage)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.wage) + .first): const pw.FractionColumnWidth(0.15) + }); + } + if (dataTypes.contains(DataTypeJob.link)) { + map.addAll({ + sortedDataTypes.indexOf(sortedDataTypes + .where((element) => element == DataTypeJob.link) + .first): const pw.FractionColumnWidth(0.2) + }); + } + + return map; +} + +dynamic businessValueFromDataType( + Business business, DataTypeBusiness dataType) { + switch (dataType) { + case DataTypeBusiness.name: + return business.name; + case DataTypeBusiness.description: + return business.description; + case DataTypeBusiness.website: + return business.website; + case DataTypeBusiness.contactName: + return business.contactName; + case DataTypeBusiness.contactEmail: + return business.contactEmail; + case DataTypeBusiness.contactPhone: + return business.contactPhone; + case DataTypeBusiness.notes: + return business.notes; + case DataTypeBusiness.logo: + return null; + } +} + +dynamic jobValueFromDataType(JobListing job, DataTypeJob dataType, + List> nameMapping) { + switch (dataType) { + case DataTypeJob.name: + return job.name; + case DataTypeJob.description: + return job.description; + case DataTypeJob.wage: + return job.wage; + case DataTypeJob.link: + return job.link; + case DataTypeJob.businessName: + return nameMapping + .where((element) => element['id'] == job.businessId) + .first['name']; + } +} diff --git a/fbla_ui/lib/pages/listing_detail.dart b/fbla_ui/lib/pages/listing_detail.dart new file mode 100644 index 0000000..ddd68fb --- /dev/null +++ b/fbla_ui/lib/pages/listing_detail.dart @@ -0,0 +1,221 @@ +import 'package:fbla_ui/api_logic.dart'; +import 'package:fbla_ui/main.dart'; +import 'package:fbla_ui/pages/create_edit_listing.dart'; +import 'package:fbla_ui/pages/signin_page.dart'; +import 'package:fbla_ui/shared.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class JobListingDetail extends StatefulWidget { + final JobListing listing; + final Business fromBusiness; + + const JobListingDetail( + {super.key, required this.listing, required this.fromBusiness}); + + @override + State createState() => _CreateBusinessDetailState(); +} + +class _CreateBusinessDetailState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.listing.name), + actions: _getActions(widget.listing, widget.fromBusiness), + ), + body: _detailBody(widget.listing), + ); + } + + ListView _detailBody(JobListing listing) { + return ListView( + children: [ + // Title, logo, desc, website + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Card( + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + ListTile( + title: Text(listing.name, + textAlign: TextAlign.left, + style: const TextStyle( + fontSize: 24, fontWeight: FontWeight.bold)), + subtitle: Text( + listing.description, + textAlign: TextAlign.left, + ), + 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 getIconFromJobType(listing.type, 48, + Theme.of(context).colorScheme.onSurface); + }), + ), + ), + Visibility( + visible: listing.link != null && listing.link != '', + child: 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), + ), + ), + ], + ), + Visibility( + visible: widget.fromBusiness.contactPhone != null, + child: 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(); + }), + ], + ); + }); + }, + ), + ), + ListTile( + leading: const Icon(Icons.email), + title: Text(widget.fromBusiness.contactEmail), + onTap: () { + launchUrl( + Uri.parse('mailto:${widget.fromBusiness.contactEmail}')); + }, + ), + ], + ), + ), + ], + ); + } + + List? _getActions(JobListing listing, Business fromBusiness) { + if (loggedIn) { + return [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => CreateEditJobListing( + inputJobListing: listing, + inputBusiness: fromBusiness, + ))); + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + title: const Text('Are You Sure?'), + content: + Text('This will permanently delete ${listing.name}.'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }), + TextButton( + child: const Text('Yes'), + onPressed: () async { + String? deleteResult = + await deleteListing(listing.id!); + if (deleteResult != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text(deleteResult))); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const MainApp())); + } + }), + ], + ); + }); + }, + ), + ]; + } + return null; + } +} diff --git a/fbla_ui/lib/pages/signin_page.dart b/fbla_ui/lib/pages/signin_page.dart index 650b603..c3ffa81 100644 --- a/fbla_ui/lib/pages/signin_page.dart +++ b/fbla_ui/lib/pages/signin_page.dart @@ -1,5 +1,4 @@ import 'package:fbla_ui/api_logic.dart'; -import 'package:fbla_ui/home.dart'; import 'package:fbla_ui/shared.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -7,7 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart'; bool loggedIn = false; class SignInPage extends StatefulWidget { - final Callback refreshAccount; + final void Function() refreshAccount; const SignInPage({super.key, required this.refreshAccount}); diff --git a/fbla_ui/lib/shared.dart b/fbla_ui/lib/shared.dart index cf4c5da..2ec4339 100644 --- a/fbla_ui/lib/shared.dart +++ b/fbla_ui/lib/shared.dart @@ -8,19 +8,13 @@ import 'package:sliver_tools/sliver_tools.dart'; import 'package:url_launcher/url_launcher.dart'; late String jwt; -Set filters = {}; -Set selectedChips = {}; String searchFilter = ''; -bool isFiltered = false; Set selectedBusinesses = {}; -Set selectedDataTypes = {}; -Set dataTypeFilters = {}; -enum DataType { +enum DataTypeBusiness { logo, name, description, - // type, website, contactName, contactEmail, @@ -28,34 +22,67 @@ enum DataType { notes, } -Map dataTypeValues = { - DataType.logo: 0, - DataType.name: 1, - DataType.description: 2, +enum DataTypeJob { + businessName, + name, + description, + wage, + link, +} + +Map dataTypePriorityBusiness = { + DataTypeBusiness.logo: 0, + DataTypeBusiness.name: 1, + DataTypeBusiness.description: 2, // DataType.type: 3, - DataType.website: 4, - DataType.contactName: 5, - DataType.contactEmail: 6, - DataType.contactPhone: 7, - DataType.notes: 8 + DataTypeBusiness.website: 4, + DataTypeBusiness.contactName: 5, + DataTypeBusiness.contactEmail: 6, + DataTypeBusiness.contactPhone: 7, + DataTypeBusiness.notes: 8 }; -Map dataTypeFriendly = { - DataType.logo: 'Logo', - DataType.name: 'Name', - DataType.description: 'Description', +Map dataTypeFriendlyBusiness = { + DataTypeBusiness.logo: 'Logo', + DataTypeBusiness.name: 'Name', + DataTypeBusiness.description: 'Description', // DataType.type: 'Type', - DataType.website: 'Website', - DataType.contactName: 'Contact Name', - DataType.contactEmail: 'Contact Email', - DataType.contactPhone: 'Contact Phone', - DataType.notes: 'Notes' + DataTypeBusiness.website: 'Website', + DataTypeBusiness.contactName: 'Contact Name', + DataTypeBusiness.contactEmail: 'Contact Email', + DataTypeBusiness.contactPhone: 'Contact Phone', + DataTypeBusiness.notes: 'Notes' }; -Set sortDataTypes(Set set) { - List list = set.toList(); +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: 'Listing Name', + DataTypeJob.description: 'Description', + DataTypeJob.wage: 'Wage', + DataTypeJob.link: 'Link', +}; + +Set sortDataTypesBusiness(Set set) { + List list = set.toList(); list.sort((a, b) { - return dataTypeValues[a]!.compareTo(dataTypeValues[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; @@ -73,8 +100,8 @@ enum BusinessType { enum JobType { cashier, server, mechanic, other } class JobListing { - String? id; - String? businessId; + int? id; + int? businessId; String name; String description; JobType type; @@ -89,18 +116,30 @@ class JobListing { required this.type, this.wage, this.link}); + + factory JobListing.copy(JobListing input) { + return JobListing( + id: input.id, + businessId: input.businessId, + name: input.name, + description: input.description, + type: input.type, + wage: input.wage, + link: input.link, + ); + } } class Business { int id; String name; String description; - String? website; + String website; String? contactName; - String? contactEmail; + String contactEmail; String? contactPhone; String? notes; - String? locationName; + String locationName; String? locationAddress; List? listings; @@ -108,12 +147,12 @@ class Business { {required this.id, required this.name, required this.description, - this.website, + required this.website, this.contactName, - this.contactEmail, + required this.contactEmail, this.contactPhone, this.notes, - this.locationName, + required this.locationName, this.locationAddress, this.listings}); @@ -123,11 +162,13 @@ class Business { listings = []; for (int i = 0; i < json['listings'].length; i++) { listings.add(JobListing( - name: json['listings']['name'], - description: json['listings']['description'], - type: json['listings']['type'], - wage: json['listings']['wage'], - link: json['listings']['link'])); + id: json['listings'][i]['id'], + businessId: json['listings'][i]['businessId'], + name: json['listings'][i]['name'], + description: json['listings'][i]['description'], + type: JobType.values.byName(json['listings'][i]['type']), + wage: json['listings'][i]['wage'], + link: json['listings'][i]['link'])); } } @@ -168,7 +209,7 @@ class Business { // return groupedBusinesses; // } -Icon getIconFromType(BusinessType type, double size, Color color) { +Icon getIconFromBusinessType(BusinessType type, double size, Color color) { switch (type) { case BusinessType.food: return Icon( @@ -238,7 +279,8 @@ Icon getIconFromJobType(JobType type, double size, Color color) { } } -pw.Icon getPwIconFromType(BusinessType type, double size, PdfColor color) { +pw.Icon getPwIconFromBusinessType( + BusinessType type, double size, PdfColor color) { switch (type) { case BusinessType.food: return pw.Icon(const pw.IconData(0xe56c), size: size, color: color); @@ -268,33 +310,33 @@ pw.Icon getPwIconFromJobType(JobType type, double size, PdfColor color) { } } -Text getNameFromType(BusinessType type, Color color) { +String getNameFromBusinessType(BusinessType type) { switch (type) { case BusinessType.food: - return Text('Food Related', style: TextStyle(color: color)); + return 'Food Related'; case BusinessType.shop: - return Text('Shops', style: TextStyle(color: color)); + return 'Shops'; case BusinessType.outdoors: - return Text('Outdoors', style: TextStyle(color: color)); + return 'Outdoors'; case BusinessType.manufacturing: - return Text('Manufacturing', style: TextStyle(color: color)); + return 'Manufacturing'; case BusinessType.entertainment: - return Text('Entertainment', style: TextStyle(color: color)); + return 'Entertainment'; case BusinessType.other: - return Text('Other', style: TextStyle(color: color)); + return 'Other'; } } -Text getNameFromJobType(JobType type, Color color) { +String getNameFromJobType(JobType type) { switch (type) { case JobType.cashier: - return Text('Cashier', style: TextStyle(color: color)); + return 'Cashier'; case JobType.server: - return Text('Server', style: TextStyle(color: color)); + return 'Server'; case JobType.mechanic: - return Text('Mechanic', style: TextStyle(color: color)); + return 'Mechanic'; case JobType.other: - return Text('Other', style: TextStyle(color: color)); + return 'Other'; } } @@ -337,9 +379,9 @@ class _BusinessDisplayPanelState extends State { // } // } - if (filters.isNotEmpty) { - isFiltered = true; - } + // if (filters.isNotEmpty) { + // isFiltered = true; + // } // for (var i = 0; i < businessTypes.length; i++) { // if (filters.contains(businessTypes[i])) { @@ -347,27 +389,27 @@ class _BusinessDisplayPanelState extends State { // } // } - if (isFiltered) { - for (JobType jobType in widget.groupedBusinesses.keys) { - if (filters.contains(jobType)) { - headers.add(BusinessHeader( - type: jobType, - widescreen: widget.widescreen, - selectable: widget.selectable, - selectedBusinesses: selectedBusinesses, - businesses: widget.groupedBusinesses[jobType]!)); - } - } - } else { - for (JobType jobType in widget.groupedBusinesses.keys) { - headers.add(BusinessHeader( - type: jobType, - widescreen: widget.widescreen, - selectable: widget.selectable, - selectedBusinesses: selectedBusinesses, - businesses: widget.groupedBusinesses[jobType]!)); - } + // if (isFiltered) { + // for (JobType jobType in widget.groupedBusinesses.keys) { + // if (filters.contains(jobType)) { + // headers.add(BusinessHeader( + // type: jobType, + // widescreen: widget.widescreen, + // selectable: widget.selectable, + // selectedBusinesses: selectedBusinesses, + // businesses: widget.groupedBusinesses[jobType]!)); + // } + // } + // } else { + for (JobType jobType in widget.groupedBusinesses.keys) { + headers.add(BusinessHeader( + type: jobType, + widescreen: widget.widescreen, + selectable: widget.selectable, + selectedBusinesses: selectedBusinesses, + businesses: widget.groupedBusinesses[jobType]!)); } + // } headers.sort((a, b) => a.type.index.compareTo(b.type.index)); return MultiSliver(children: headers); } @@ -425,8 +467,7 @@ class _BusinessHeaderState extends State { child: getIconFromJobType( widget.type, 24, Theme.of(context).colorScheme.onPrimary), ), - getNameFromJobType( - widget.type, Theme.of(context).colorScheme.onPrimary), + Text(getNameFromJobType(widget.type)), ], ), Padding( @@ -458,8 +499,10 @@ class _BusinessHeaderState extends State { child: getIconFromJobType( widget.type, 24, Theme.of(context).colorScheme.onPrimary), ), - getNameFromJobType( - widget.type, Theme.of(context).colorScheme.onPrimary), + Text( + getNameFromJobType(widget.type), + style: TextStyle(color: Theme.of(context).colorScheme.onPrimary), + ), ], ); } @@ -578,8 +621,7 @@ class _BusinessCardState extends State { Uri.parse('https://${business.website}')); }, ), - if ((business.locationName != null) && - (business.locationName != '')) + if (business.locationName != '') IconButton( icon: const Icon(Icons.location_on), onPressed: () { @@ -598,7 +640,7 @@ class _BusinessCardState extends State { return AlertDialog( backgroundColor: Theme.of(context) .colorScheme - .background, + .surface, title: Text((business.contactName == null || business.contactName == '') @@ -629,8 +671,7 @@ class _BusinessCardState extends State { }); }, ), - if ((business.contactEmail != null) && - (business.contactEmail != '')) + if (business.contactEmail != '') IconButton( icon: const Icon(Icons.email), onPressed: () { @@ -770,8 +811,105 @@ class _BusinessCardState extends State { } } +class BusinessSearchBar extends StatefulWidget { + final Set filters; + final Future Function(Set) setFiltersCallback; + final Future Function(String) setSearchCallback; + + const BusinessSearchBar( + {super.key, + required this.filters, + required this.setFiltersCallback, + required this.setSearchCallback}); + + @override + State createState() => _BusinessSearchBarState(); +} + +class _BusinessSearchBarState extends State { + bool isFiltered = false; + + @override + Widget build(BuildContext context) { + Set selectedChips = Set.from(widget.filters); + return SizedBox( + width: 800, + height: 50, + child: SearchBar( + backgroundColor: WidgetStateProperty.resolveWith((notNeeded) { + return Theme.of(context).colorScheme.surfaceContainer; + }), + onChanged: (query) { + widget.setSearchCallback(query); + }, + leading: const Padding( + padding: EdgeInsets.only(left: 8.0), + child: Icon(Icons.search), + ), + trailing: [ + IconButton( + tooltip: 'Filters', + icon: Icon(Icons.filter_list, + color: isFiltered + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + // DO NOT MOVE TO SEPARATE WIDGET, setState is needed in main tree + title: const Text('Filter Options'), + content: FilterChips( + selectedChips: selectedChips, + ), + actions: [ + TextButton( + child: const Text('Reset'), + onPressed: () async { + setState(() { + selectedChips = {}; + isFiltered = false; + }); + widget.setFiltersCallback({}); + Navigator.of(context).pop(); + }), + TextButton( + child: const Text('Cancel'), + onPressed: () { + selectedChips = Set.from(widget.filters); + Navigator.of(context).pop(); + }), + TextButton( + child: const Text('Apply'), + onPressed: () async { + widget.setFiltersCallback( + Set.from(selectedChips)); + if (selectedChips.isNotEmpty) { + setState(() { + isFiltered = true; + }); + } else { + setState(() { + isFiltered = false; + }); + } + Navigator.of(context).pop(); + }), + ], + ); + }); + }, + ) + ]), + ); + } +} + class FilterChips extends StatefulWidget { - const FilterChips({super.key}); + final Set selectedChips; + + const FilterChips({super.key, required this.selectedChips}); @override State createState() => _FilterChipsState(); @@ -781,22 +919,21 @@ class _FilterChipsState extends State { List filterChips() { List chips = []; - for (var type in BusinessType.values) { + for (var type in JobType.values) { chips.add(Padding( padding: const EdgeInsets.only(left: 4.0, right: 4.0), child: FilterChip( showCheckmark: false, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - label: - getNameFromType(type, Theme.of(context).colorScheme.onSurface), - selected: selectedChips.contains(type), + label: Text(getNameFromJobType(type)), + selected: widget.selectedChips.contains(type), onSelected: (bool selected) { setState(() { if (selected) { - selectedChips.add(type); + widget.selectedChips.add(type); } else { - selectedChips.remove(type); + widget.selectedChips.remove(type); } }); }), @@ -812,64 +949,3 @@ class _FilterChipsState extends State { ); } } - -class FilterDataTypeChips extends StatefulWidget { - const FilterDataTypeChips({super.key}); - - @override - State createState() => _FilterDataTypeChipsState(); -} - -class _FilterDataTypeChipsState extends State { - List filterDataTypeChips() { - List chips = []; - - for (var type in DataType.values) { - chips.add(Padding( - padding: - const EdgeInsets.only(left: 3.0, right: 3.0, bottom: 3.0, top: 3.0), - // child: ActionChip( - // avatar: selectedDataTypes.contains(type) ? Icon(Icons.check_box) : Icon(Icons.check_box_outline_blank), - // label: Text(type.name), - // onPressed: () { - // if (!selectedDataTypes.contains(type)) { - // setState(() { - // selectedDataTypes.add(type); - // }); - // } else { - // setState(() { - // selectedDataTypes.remove(type); - // }); - // } - // }, - - // ), - child: FilterChip( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: - BorderSide(color: Theme.of(context).colorScheme.secondary)), - label: Text(dataTypeFriendly[type]!), - showCheckmark: false, - selected: selectedDataTypes.contains(type), - onSelected: (bool selected) { - setState(() { - if (selected) { - selectedDataTypes.add(type); - } else { - selectedDataTypes.remove(type); - } - }); - }), - )); - } - return chips; - } - - @override - Widget build(BuildContext context) { - return Wrap( - children: filterDataTypeChips(), - ); - } -}