init commit - move from separate git repos

This commit is contained in:
2024-04-07 12:32:23 -05:00
commit fee7ea5678
176 changed files with 10129 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
import 'dart:async';
import 'dart:convert';
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 client = http.Client();
// var apiAddress = '192.168.0.114:8000';
// var apiLogoAddress = 'https://homelab.marinodev.com/fbla-api';
// var apiLogoAddress = 'http://192.168.0.114:8000';
Future fetchBusinessData() async {
try {
var response = await http
.get(Uri.parse('$apiAddress/businessdata'))
.timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
var decodedResponse = json.decode(response.body);
List<Business> businessList = List<Business>.from(
decodedResponse.map((json) => Business.fromJson(json)).toList());
return businessList;
} 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 createBusiness(Business business, String jwt) async {
var json = '''
{
"id": ${business.id},
"name": "${business.name}",
"description": "${business.description}",
"type": "${business.type.name}",
"website": "${business.website}",
"contactName": "${business.contactName}",
"contactEmail": "${business.contactEmail}",
"contactPhone": "${business.contactPhone}",
"notes": "${business.notes}",
"locationName": "${business.locationName}",
"locationAddress": "${business.locationAddress}"
}
''';
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(Business business, String jwt) async {
var json = '''
{
"id": ${business.id}
}
''';
try {
var response = await http.post(Uri.parse('$apiAddress/deletebusiness'),
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, String jwt) async {
var json = '''
{
"id": ${business.id},
"name": "${business.name}",
"description": "${business.description}",
"type": "${business.type.name}",
"website": "${business.website}",
"contactName": "${business.contactName}",
"contactEmail": "${business.contactEmail}",
"contactPhone": "${business.contactPhone}",
"notes": "${business.notes}",
"locationName": "${business.locationName}",
"locationAddress": "${business.locationAddress}"
}
''';
print(json);
try {
var response = await http.post(Uri.parse('$apiAddress/editbusiness'),
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 = '''
{
"username": "$username",
"password": "$password"
}
''';
var response = await http.post(
Uri.parse('$apiAddress/signin'),
body: json,
);
if (response.statusCode == 200) {
return response.body;
} else {
return 'Error: ${response.body}';
}
}
Future marinoDevLogo() async {
var response = await http.get(
Uri.parse('$apiAddress/marinodev'),
);
// File logo = File ('${getTemporaryDirectory().toString()}/marinodev.svg');
// logo.writeAsBytes(response.bodyBytes);
return response.bodyBytes;
}
Future getLogo(int logoId) async {
var response = await http.get(
Uri.parse('$apiAddress/logos/$logoId'),
);
if (response.statusCode == 200) {
return response.bodyBytes;
} else {
return 'Error ${response.statusCode}';
}
}
+444
View File
@@ -0,0 +1,444 @@
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/pages/create_edit_business.dart';
import 'package:fbla_ui/pages/export_data.dart';
import 'package:fbla_ui/pages/signin_page.dart';
import 'package:fbla_ui/shared.dart';
import 'package:flutter/material.dart';
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;
const Home({super.key, required this.themeCallback});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
late Future refreshBusinessDataFuture;
bool _isPreviousData = false;
late List<Business> businesses;
@override
void initState() {
super.initState();
refreshBusinessDataFuture = fetchBusinessData();
initialLogin();
}
Future<void> initialLogin() async {
final prefs = await SharedPreferences.getInstance();
bool? rememberMe = prefs.getBool('rememberMe');
if (rememberMe != null && rememberMe) {
String? username = prefs.getString('username');
String? password = prefs.getString('password');
jwt = await signIn(username!, password!);
if (!jwt.contains('Error:')) {
setState(() {
loggedIn = true;
});
}
}
}
void setStateCallback() {
setState(() {
loggedIn = loggedIn;
});
}
@override
Widget build(BuildContext context) {
bool widescreen = MediaQuery.sizeOf(context).width >= 1000;
return Scaffold(
floatingActionButton: _getFAB(),
body: RefreshIndicator(
edgeOffset: 120,
onRefresh: () async {
var refreshedData = fetchBusinessData();
await refreshedData;
setState(() {
refreshBusinessDataFuture = refreshedData;
});
},
child: CustomScrollView(
slivers: [
SliverAppBar(
title: widescreen ? _searchBar() : const Text('Job Link'),
toolbarHeight: 70,
pinned: true,
scrolledUnderElevation: 0,
centerTitle: true,
expandedHeight: widescreen ? 70 : 120,
backgroundColor: Theme.of(context).colorScheme.background,
bottom: _getBottom(),
leading: IconButton(
icon: getIconFromThemeMode(themeMode),
onPressed: () {
setState(() {
widget.themeCallback();
});
},
),
actions: [
IconButton(
icon: const Icon(Icons.help),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('About'),
backgroundColor:
Theme.of(context).colorScheme.background,
content: SizedBox(
width: 500,
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Welcome to my FBLA 2024 Coding and Programming submission!\n\n'
'MarinoDev Job Link aims to provide comprehensive details of businesses and community partners'
' for Waukesha West High School\'s Career and Technical Education Department.\n\n'),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: const Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('Git Repo:'),
Text(
'https://git.marinodev.com/MarinoDev/FBLA24\n',
style: TextStyle(
color: Colors.blue)),
],
),
onTap: () {
launchUrl(Uri.https(
'git.marinodev.com',
'/MarinoDev/FBLA24'));
},
),
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: const Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Please direct any questions to'),
Text('drake@marinodev.com',
style: TextStyle(
color: Colors.blue)),
],
),
onTap: () {
launchUrl(Uri.parse(
'mailto:drake@marinodev.com'));
},
),
)
],
),
),
),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
}),
],
);
});
},
),
IconButton(
icon: const Icon(Icons.picture_as_pdf),
onPressed: () async {
if (!_isPreviousData) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text('There is no data!'),
duration: Duration(seconds: 2),
),
);
} else {
selectedDataTypes = <DataType>{};
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ExportData(businesses: businesses)));
}
},
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: loggedIn
? const Icon(Icons.account_circle)
: const Icon(Icons.login),
onPressed: () {
if (loggedIn) {
var payload = JWT.decode(jwt).payload;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor:
Theme.of(context).colorScheme.background,
title: Text('Hi, ${payload['username']}!'),
content: Text(
'You are logged in as an admin with username ${payload['username']}.'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Logout'),
onPressed: () async {
final prefs = await SharedPreferences
.getInstance();
prefs.setBool('rememberMe', false);
prefs.setString('username', '');
prefs.setString('password', '');
setState(() {
loggedIn = false;
});
Navigator.of(context).pop();
}),
],
);
});
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SignInPage(
refreshAccount: setStateCallback)));
}
},
),
),
],
),
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: () {
var refreshedData = fetchBusinessData();
setState(() {
refreshBusinessDataFuture = refreshedData;
});
},
),
),
]),
));
}
businesses = snapshot.data;
_isPreviousData = true;
return BusinessDisplayPanel(
businesses: businesses,
widescreen: widescreen,
selectable: false);
} else if (snapshot.hasError) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
));
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
if (_isPreviousData) {
return BusinessDisplayPanel(
businesses: businesses,
widescreen: widescreen,
selectable: false);
} else {
return SliverToBoxAdapter(
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',
// ),
),
));
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
}),
],
),
),
);
}
Widget? _getFAB() {
if (loggedIn) {
return FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CreateEditBusiness()));
},
);
}
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 = <BusinessType>{};
selectedChips = <BusinessType>{};
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(
preferredSize: const Size.fromHeight(0),
child: SizedBox(
// color: Theme.of(context).colorScheme.background,
height: 70,
child: Padding(
padding: const EdgeInsets.all(10),
child: _searchBar(),
),
),
);
}
return null;
}
}
+103
View File
@@ -0,0 +1,103 @@
import 'package:fbla_ui/home.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
ThemeMode themeMode = ThemeMode.system;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
bool? isDark = prefs.getBool('isDark');
themeMode = isDark != null && isDark
? ThemeMode.dark
: isDark != null && !isDark
? ThemeMode.light
: ThemeMode.system;
runApp(const MaterialApp(
title: 'Job Link',
home: MainApp(),
));
}
class MainApp extends StatefulWidget {
final bool? isDark;
const MainApp({super.key, this.isDark});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
void _switchTheme() async {
final prefs = await SharedPreferences.getInstance();
if (MediaQuery.of(context).platformBrightness == Brightness.dark &&
themeMode == ThemeMode.system) {
setState(() {
themeMode = ThemeMode.light;
});
prefs.setBool('isDark', false);
} else if (MediaQuery.of(context).platformBrightness == Brightness.light &&
themeMode == ThemeMode.system) {
setState(() {
themeMode = ThemeMode.dark;
});
prefs.setBool('isDark', true);
} else if (themeMode == ThemeMode.light) {
setState(() {
themeMode = ThemeMode.dark;
});
prefs.setBool('isDark', true);
} else if (themeMode == ThemeMode.dark) {
setState(() {
themeMode = ThemeMode.light;
});
prefs.setBool('isDark', false);
}
}
@override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
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),
tertiary: Colors.green.shade900,
),
iconTheme: const IconThemeData(color: Colors.white),
inputDecorationTheme: const InputDecorationTheme(),
useMaterial3: true,
),
theme: ThemeData(
colorScheme: ColorScheme.light(
brightness: Brightness.light,
primary: Colors.blue,
onPrimary: Colors.white,
secondary: Colors.blue.shade200,
background: Colors.white,
tertiary: Colors.green,
),
iconTheme: const IconThemeData(color: Colors.black),
inputDecorationTheme:
const InputDecorationTheme(border: UnderlineInputBorder()),
useMaterial3: true,
),
home: Home(themeCallback: _switchTheme),
);
}
}
+228
View File
@@ -0,0 +1,228 @@
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/signin_page.dart';
import 'package:fbla_ui/shared.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class BusinessDetail extends StatefulWidget {
final Business inputBusiness;
const BusinessDetail({super.key, required this.inputBusiness});
@override
State<BusinessDetail> createState() => _CreateBusinessDetailState();
}
class _CreateBusinessDetailState extends State<BusinessDetail> {
@override
Widget build(BuildContext context) {
Business business = Business.copy(widget.inputBusiness);
return Scaffold(
appBar: AppBar(
title: Text(business.name),
actions: _getActions(business),
),
body: ListView(
children: [
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(
'https://$apiAddress/fbla-api/logos/${business.id}',
width: 48,
height: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 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}'));
},
),
],
),
),
Visibility(
visible: (business.contactEmail.isNotEmpty ||
business.contactPhone.isNotEmpty),
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.isEmpty
? 'Contact ${business.name}'
: business.contactName,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
],
),
Visibility(
visible: business.contactPhone.isNotEmpty,
child: ListTile(
leading: Icon(Icons.phone),
title: Text(business.contactPhone),
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();
}),
],
);
});
},
),
),
Visibility(
visible: business.contactEmail.isNotEmpty,
child: ListTile(
leading: const Icon(Icons.email),
title: Text(business.contactEmail),
onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}'));
},
),
),
],
),
),
),
Visibility(
child: Card(
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: const Icon(Icons.location_on),
title: Text(business.locationName),
subtitle: Text(business.locationAddress),
onTap: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
),
),
Visibility(
visible: business.notes.isNotEmpty,
child: Card(
child: ListTile(
leading: const Icon(Icons.notes),
title: const Text(
'Additional Notes:',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
subtitle: Text(business.notes),
),
),
),
],
),
);
}
List<Widget>? _getActions(Business business) {
if (loggedIn) {
return [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
CreateEditBusiness(inputBusiness: business)));
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.background,
title: const Text('Are You Sure?'),
content:
Text('This will permanently delete ${business.name}.'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Yes'),
onPressed: () async {
String? deleteResult =
await deleteBusiness(business, jwt);
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;
}
}
+528
View File
@@ -0,0 +1,528 @@
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:flutter/services.dart';
class CreateEditBusiness extends StatefulWidget {
final Business? inputBusiness;
const CreateEditBusiness({super.key, this.inputBusiness});
@override
State<CreateEditBusiness> createState() => _CreateEditBusinessState();
}
class _CreateEditBusinessState extends State<CreateEditBusiness> {
late TextEditingController _nameController;
late TextEditingController _websiteController;
late TextEditingController _descriptionController;
late TextEditingController _contactNameController;
late TextEditingController _contactPhoneController;
late TextEditingController _contactEmailController;
late TextEditingController _notesController;
late TextEditingController _locationNameController;
late TextEditingController _locationAddressController;
Business business = Business(
id: 0,
name: 'Business',
description: 'Add details about the business below.',
type: BusinessType.other,
website: '',
contactName: '',
contactEmail: '',
contactPhone: '',
notes: '',
locationName: '',
locationAddress: '',
);
bool _isLoading = false;
@override
void initState() {
super.initState();
if (widget.inputBusiness != null) {
business = Business.copy(widget.inputBusiness!);
_nameController = TextEditingController(text: business.name);
_descriptionController =
TextEditingController(text: business.description);
} else {
_nameController = TextEditingController();
_descriptionController = TextEditingController();
}
_websiteController = TextEditingController(text: business.website);
_contactNameController = TextEditingController(text: business.contactName);
_contactPhoneController =
TextEditingController(text: business.contactPhone);
_contactEmailController =
TextEditingController(text: business.contactEmail);
_notesController = TextEditingController(text: business.notes);
_locationNameController =
TextEditingController(text: business.locationName);
_locationAddressController =
TextEditingController(text: business.locationAddress);
}
final formKey = GlobalKey<FormState>();
final TextEditingController businessTypeController = TextEditingController();
@override
Widget build(BuildContext context) {
return PopScope(
canPop: !_isLoading,
onPopInvoked: _handlePop,
child: Form(
key: formKey,
child: Scaffold(
appBar: AppBar(
title: (widget.inputBusiness != null)
? Text('Edit ${widget.inputBusiness?.name}', maxLines: 1)
: const Text('Add New Business'),
),
floatingActionButton: FloatingActionButton(
child: _isLoading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
onPressed: () async {
if (formKey.currentState!.validate()) {
formKey.currentState?.save();
setState(() {
_isLoading = true;
});
String? result;
// if (business.contactName == '') {
// business.contactName = 'Contact ${business.name}';
// }
if (widget.inputBusiness != null) {
result = await editBusiness(business, jwt);
} else {
result = await createBusiness(business, jwt);
}
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: ListView(
children: [
Center(
child: SizedBox(
width: 1000,
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(
width: 48,
height: 48,
'https://logo.clearbit.com/${business.website}',
errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48,
Theme.of(context).colorScheme.onBackground);
}),
),
),
Card(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0),
child: TextFormField(
controller: _nameController,
autovalidateMode:
AutovalidateMode.onUserInteraction,
maxLength: 30,
onChanged: (inputName) {
setState(() {
business.name = inputName;
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Business Name',
),
validator: (value) {
if (value != null && value.isEmpty) {
return 'Name is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _websiteController,
autovalidateMode:
AutovalidateMode.onUserInteraction,
keyboardType: TextInputType.url,
onChanged: (inputUrl) {
business.website = Uri.encodeFull(inputUrl
.toLowerCase()
.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''));
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Website',
),
validator: (value) {
if (value != null && value.isEmpty) {
return 'Website is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0),
child: TextFormField(
controller: _descriptionController,
autovalidateMode:
AutovalidateMode.onUserInteraction,
maxLength: 500,
onChanged: (inputDesc) {
setState(() {
business.description = inputDesc;
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Business Description',
),
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: 16.0),
// child: Row(
// children: [
// ElevatedButton(
// style: ButtonStyle(
// backgroundColor:
// MaterialStateProperty.all(
// Theme.of(context)
// .colorScheme
// .background)),
// child: const Row(
// children: [
// Icon(Icons.search),
// Text('Search For Location'),
// ],
// ),
// onPressed: () {},
// ),
// const Padding(
// padding: EdgeInsets.only(
// left: 32.0, right: 32.0),
// child: Text(
// 'OR',
// style: TextStyle(fontSize: 24),
// ),
// ),
// Expanded(
// child: Column(
// children: [
// TextFormField(
// controller: _locationNameController,
// onChanged: (inputName) {
// setState(() {
// business.locationName =
// inputName;
// });
// },
// onTapOutside:
// (PointerDownEvent event) {
// FocusScope.of(context).unfocus();
// },
// decoration: const InputDecoration(
// labelText:
// 'Location Name (optional)',
// ),
// ),
// TextFormField(
// controller:
// _locationAddressController,
// onChanged: (inputAddr) {
// setState(() {
// business.locationAddress =
// inputAddr;
// });
// },
// onTapOutside:
// (PointerDownEvent event) {
// FocusScope.of(context).unfocus();
// },
// decoration: const InputDecoration(
// labelText:
// 'Location Address (optional)',
// ),
// ),
// ],
// ),
// ),
// ],
// ),
// ),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _locationNameController,
onChanged: (inputName) {
setState(() {
business.locationName = inputName;
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Location Name (optional)',
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _locationAddressController,
onChanged: (inputAddr) {
setState(() {
business.locationAddress = inputAddr;
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Location Address (optional)',
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text('Type of Business',
style: TextStyle(fontSize: 16)),
DropdownMenu<BusinessType>(
initialSelection: business.type,
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.other,
label: 'Other'),
],
onSelected: (inputType) {
business.type = inputType!;
},
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _contactNameController,
onSaved: (inputText) {
business.contactName = inputText!;
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Contact Information Name (optional)',
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _contactPhoneController,
inputFormatters: [PhoneFormatter()],
keyboardType: TextInputType.phone,
onSaved: (inputText) {
business.contactPhone = inputText!;
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Contact Phone # (optional)',
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _contactEmailController,
keyboardType: TextInputType.emailAddress,
onSaved: (inputText) {
business.contactEmail = inputText!;
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Contact Email (optional)',
),
validator: (value) {
if (value != null) {
if (value.isEmpty) {
return null;
} else if (!RegExp(
r'^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
.hasMatch(value)) {
return 'Enter a valid Email';
} else if (value.characters.length > 50) {
return 'Contact Email cannot be longer than 50 characters';
}
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _notesController,
maxLength: 300,
onSaved: (inputText) {
business.notes = inputText!;
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Other Notes (optional)',
),
),
),
],
),
),
SizedBox(
height: 75,
)
],
),
),
),
],
),
)),
);
}
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.'),
),
);
}
}
}
class PhoneFormatter extends TextInputFormatter {
String phoneFormat(value) {
String input = value.replaceAll(RegExp(r'[\D]'), '');
String phoneFormatted = input.isNotEmpty
? '(${input.substring(0, input.length >= 3 ? 3 : null)}${input.length >= 4 ? ') ' : ''}${input.length > 3 ? input.substring(3, input.length >= 5 ? 6 : null) + (input.length >= 7 ? '-${input.substring(6, input.length >= 10 ? 10 : null)}' : '') : ''}'
: input;
return phoneFormatted;
}
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
String text = newValue.text;
if (newValue.selection.baseOffset == 0) {
return newValue;
}
return newValue.copyWith(
text: phoneFormat(text),
selection: TextSelection.collapsed(offset: phoneFormat(text).length));
}
}
+574
View File
@@ -0,0 +1,574 @@
import 'dart:convert';
import 'dart:io';
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/shared.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:open_filex/open_filex.dart';
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;
class ExportData extends StatefulWidget {
final List<Business> businesses;
const ExportData({super.key, required this.businesses});
@override
State<ExportData> createState() => _ExportDataState();
}
class _ExportDataState extends State<ExportData> {
late Future refreshBusinessDataFuture;
@override
void initState() {
super.initState();
refreshBusinessDataFuture = fetchBusinessData();
_isLoading = false;
selectedBusinesses = <Business>{};
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _FAB(businesses: widget.businesses),
body: CustomScrollView(
slivers: [
SliverAppBar(
forceMaterialTransparency: false,
title: const Text('Export Data'),
toolbarHeight: 70,
pinned: true,
centerTitle: true,
expandedHeight: 120,
backgroundColor: Theme.of(context).colorScheme.background,
actions: [
IconButton(
icon: const Icon(Icons.settings),
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('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(),
],
),
),
actions: [
TextButton(
child: const Text('Reset'),
onPressed: () {
setState(() {
dataTypeFilters = <DataType>{};
selectedDataTypes = <DataType>{};
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();
}),
],
);
});
},
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(0),
child: SizedBox(
height: 70,
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 = <BusinessType>{};
selectedChips = <BusinessType>{};
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();
}),
],
);
});
},
),
),
),
),
),
),
),
),
BusinessDisplayPanel(
businesses: widget.businesses,
widescreen: MediaQuery.sizeOf(context).width >= 1000,
selectable: true),
const SliverToBoxAdapter(
child: SizedBox(
height: 100,
),
),
],
),
);
}
}
class _FAB extends StatefulWidget {
final List<Business> businesses;
const _FAB({required this.businesses});
@override
State<_FAB> createState() => _FABState();
}
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;
});
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<pw.Padding> 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<pw.TableRow> rows = [];
if (selectedBusinesses.isEmpty) {
selectedBusinesses.addAll(widget.businesses);
isBusinessesFiltered = false;
} else {
isBusinessesFiltered = true;
}
double remainingSpace = 744;
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<int, pw.TableColumnWidth> 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<pw.Padding> 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 = <Business>{};
}
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),
));
}
},
);
}
}
+207
View File
@@ -0,0 +1,207 @@
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';
bool loggedIn = false;
class SignInPage extends StatefulWidget {
final Callback refreshAccount;
const SignInPage({super.key, required this.refreshAccount});
@override
State<SignInPage> createState() => _SignInPageState();
}
class _SignInPageState extends State<SignInPage> {
final _signInKey = GlobalKey<FormState>();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
String username = '';
String password = '';
bool obscurePassword = true;
bool rememberMe = false;
bool _isloading = false;
String? errorMessage;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Admin Sign In'),
centerTitle: true,
),
body: SingleChildScrollView(
child: Form(
key: _signInKey,
child: Center(
heightFactor: 1.0,
child: Container(
padding: const EdgeInsets.fromLTRB(12, 50, 12, 50),
height: 475,
width: 500,
child: Card(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
child: Column(
children: [
const Center(
child: Text(
'Admin Sign In',
style: TextStyle(
fontSize: 30, fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
onChanged: (value) {
username = value.trim();
},
controller: _usernameController,
autocorrect: false,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.person_outline),
labelText: 'Username',
border: OutlineInputBorder()),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
onChanged: (value) {
password = value.trim();
},
onFieldSubmitted: (value) async {
password = value.trim();
setState(() {
errorMessage = null;
_isloading = true;
});
jwt = await signIn(username, password).timeout(
const Duration(seconds: 20), onTimeout: () {
_isloading = false;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content:
Text('Could not Sign in (timeout)!')),
);
});
if (!jwt.contains('Error:')) {
final SharedPreferences prefs =
await SharedPreferences.getInstance();
await prefs.setString('username', username);
await prefs.setString('password', password);
await prefs.setBool('rememberMe', rememberMe);
loggedIn = true;
widget.refreshAccount();
Navigator.of(context).pop();
} else {
setState(() {
errorMessage = 'Invalid Username/Password';
_isloading = false;
});
}
},
controller: _passwordController,
autocorrect: false,
obscureText: obscurePassword,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.fingerprint),
labelText: 'Password',
border: const OutlineInputBorder(),
suffixIcon: Padding(
padding: const EdgeInsets.all(8.0),
child: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
});
},
icon: obscurePassword
? const Icon(Icons.visibility_off)
: const Icon(Icons.visibility),
),
),
),
),
),
if (errorMessage != null)
Text(
errorMessage!,
style: const TextStyle(color: Colors.red),
),
CheckboxListTile(
value: rememberMe,
onChanged: (value) async {
setState(() {
rememberMe = value!;
});
},
title: const Text('Remember me'),
),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primary,
// padding: const EdgeInsets.only(left: 20.0, right: 20.0, top: 12.0, bottom: 12.0),
),
icon: _isloading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
))
: const Icon(Icons.done, color: Colors.white),
label: const Text('Sign In',
style: TextStyle(color: Colors.white)),
onPressed: () async {
setState(() {
errorMessage = null;
_isloading = true;
});
jwt = await signIn(username, password).timeout(
const Duration(seconds: 20), onTimeout: () {
_isloading = false;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content:
Text('Could not Sign in (timeout)!')),
);
});
if (!jwt.contains('Error:')) {
final SharedPreferences prefs =
await SharedPreferences.getInstance();
await prefs.setString('username', username);
await prefs.setString('password', password);
await prefs.setBool('rememberMe', rememberMe);
loggedIn = true;
widget.refreshAccount();
Navigator.of(context).pop();
} else {
setState(() {
errorMessage = 'Invalid Username/Password';
_isloading = false;
});
}
},
),
],
),
),
),
),
),
),
),
);
}
}
+761
View File
@@ -0,0 +1,761 @@
import 'package:collection/collection.dart';
import 'package:fbla_ui/api_logic.dart';
import 'package:fbla_ui/pages/business_detail.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:sliver_tools/sliver_tools.dart';
import 'package:url_launcher/url_launcher.dart';
late String jwt;
Set<BusinessType> filters = <BusinessType>{};
Set<BusinessType> selectedChips = <BusinessType>{};
String searchFilter = '';
bool isFiltered = false;
Set<Business> selectedBusinesses = <Business>{};
Set<DataType> selectedDataTypes = <DataType>{};
Set<DataType> dataTypeFilters = <DataType>{};
enum DataType {
logo,
name,
description,
type,
website,
contactName,
contactEmail,
contactPhone,
notes,
}
Map<DataType, int> dataTypeValues = {
DataType.logo: 0,
DataType.name: 1,
DataType.description: 2,
DataType.type: 3,
DataType.website: 4,
DataType.contactName: 5,
DataType.contactEmail: 6,
DataType.contactPhone: 7,
DataType.notes: 8
};
Map<DataType, String> dataTypeFriendly = {
DataType.logo: 'Logo',
DataType.name: 'Name',
DataType.description: 'Description',
DataType.type: 'Type',
DataType.website: 'Website',
DataType.contactName: 'Contact Name',
DataType.contactEmail: 'Contact Email',
DataType.contactPhone: 'Contact Phone',
DataType.notes: 'Notes'
};
Set<DataType> sortDataTypes(Set<DataType> set) {
List<DataType> list = set.toList();
list.sort((a, b) {
return dataTypeValues[a]!.compareTo(dataTypeValues[b]!);
});
set = list.toSet();
return set;
}
enum BusinessType {
food,
shop,
outdoors,
manufacturing,
other,
}
class Business {
int id;
String name;
String description;
BusinessType type;
String website;
String contactName;
String contactEmail;
String contactPhone;
String notes;
String locationName;
String locationAddress;
Business({
required this.id,
required this.name,
required this.description,
required this.type,
required this.website,
required this.contactName,
required this.contactEmail,
required this.contactPhone,
required this.notes,
required this.locationName,
required this.locationAddress,
});
factory Business.fromJson(Map<String, dynamic> json) {
bool typeValid = true;
try {
BusinessType.values.byName(json['type']);
} catch (e) {
typeValid = false;
}
return Business(
id: json['id'],
name: json['name'],
description: json['description'],
type: typeValid
? BusinessType.values.byName(json['type'])
: BusinessType.other,
website: json['website'],
contactName: json['contactName'],
contactEmail: json['contactEmail'],
contactPhone: json['contactPhone'],
notes: json['notes'],
locationName: json['locationName'],
locationAddress: json['locationAddress'],
);
}
factory Business.copy(Business input) {
return Business(
id: input.id,
name: input.name,
description: input.description,
type: input.type,
website: input.website,
contactName: input.contactName,
contactEmail: input.contactEmail,
contactPhone: input.contactPhone,
notes: input.notes,
locationName: input.locationName,
locationAddress: input.locationAddress,
);
}
}
Map<BusinessType, List<Business>> groupBusinesses(List<Business> businesses) {
Map<BusinessType, List<Business>> groupedBusinesses =
groupBy<Business, BusinessType>(businesses, (business) => business.type);
return groupedBusinesses;
}
Icon getIconFromType(BusinessType type, double size, Color color) {
switch (type) {
case BusinessType.food:
return Icon(
Icons.restaurant,
size: size,
color: color,
);
case BusinessType.shop:
return Icon(
Icons.store,
size: size,
color: color,
);
case BusinessType.outdoors:
return Icon(
Icons.forest,
size: size,
color: color,
);
case BusinessType.manufacturing:
return Icon(
Icons.factory,
size: size,
color: color,
);
case BusinessType.other:
return Icon(
Icons.business,
size: size,
color: color,
);
}
}
pw.Icon getPwIconFromType(BusinessType type, double size, PdfColor color) {
switch (type) {
case BusinessType.food:
return pw.Icon(const pw.IconData(0xe56c), size: size, color: color);
case BusinessType.shop:
return pw.Icon(const pw.IconData(0xea12), size: size, color: color);
case BusinessType.outdoors:
return pw.Icon(const pw.IconData(0xea99), size: size, color: color);
case BusinessType.manufacturing:
return pw.Icon(const pw.IconData(0xebbc), size: size, color: color);
case BusinessType.other:
return pw.Icon(const pw.IconData(0xe0af), size: size, color: color);
}
}
Text getNameFromType(BusinessType type, Color color) {
switch (type) {
case BusinessType.food:
return Text('Food Related', style: TextStyle(color: color));
case BusinessType.shop:
return Text('Shops', style: TextStyle(color: color));
case BusinessType.outdoors:
return Text('Outdoors', style: TextStyle(color: color));
case BusinessType.manufacturing:
return Text('Manufacturing', style: TextStyle(color: color));
case BusinessType.other:
return Text('Other', style: TextStyle(color: color));
}
}
Icon getIconFromThemeMode(ThemeMode theme) {
switch (theme) {
case ThemeMode.dark:
return const Icon(Icons.dark_mode);
case ThemeMode.light:
return const Icon(Icons.light_mode);
case ThemeMode.system:
return const Icon(Icons.brightness_4);
}
}
class BusinessDisplayPanel extends StatefulWidget {
final List<Business> businesses;
final bool widescreen;
final bool selectable;
const BusinessDisplayPanel(
{super.key,
required this.businesses,
required this.widescreen,
required this.selectable});
@override
State<BusinessDisplayPanel> createState() => _BusinessDisplayPanelState();
}
class _BusinessDisplayPanelState extends State<BusinessDisplayPanel> {
Set<Business> selectedBusinesses = <Business>{};
@override
Widget build(BuildContext context) {
List<BusinessHeader> headers = [];
List<Business> filteredBusinesses = [];
for (var business in widget.businesses) {
if (business.name.toLowerCase().contains(searchFilter.toLowerCase())) {
filteredBusinesses.add(business);
}
}
var groupedBusinesses = groupBusinesses(filteredBusinesses);
var businessTypes = groupedBusinesses.keys.toList();
for (var i = 0; i < businessTypes.length; i++) {
if (filters.contains(businessTypes[i])) {
isFiltered = true;
}
}
if (isFiltered) {
for (var i = 0; i < businessTypes.length; i++) {
if (filters.contains(businessTypes[i])) {
headers.add(BusinessHeader(
type: businessTypes[i],
widescreen: widget.widescreen,
selectable: widget.selectable,
selectedBusinesses: selectedBusinesses,
businesses: groupedBusinesses[businessTypes[i]]!));
}
}
} else {
for (var i = 0; i < businessTypes.length; i++) {
headers.add(BusinessHeader(
type: businessTypes[i],
widescreen: widget.widescreen,
selectable: widget.selectable,
selectedBusinesses: selectedBusinesses,
businesses: groupedBusinesses[businessTypes[i]]!));
}
}
headers.sort((a, b) => a.type.index.compareTo(b.type.index));
return MultiSliver(children: headers);
}
}
class BusinessHeader extends StatefulWidget {
final BusinessType type;
final List<Business> businesses;
final Set<Business> selectedBusinesses;
final bool widescreen;
final bool selectable;
const BusinessHeader({
super.key,
required this.type,
required this.businesses,
required this.selectedBusinesses,
required this.widescreen,
required this.selectable,
});
@override
State<BusinessHeader> createState() => _BusinessHeaderState();
}
class _BusinessHeaderState extends State<BusinessHeader> {
refresh() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return SliverStickyHeader(
header: Container(
height: 55.0,
color: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: _getHeaderRow(widget.selectable),
),
sliver: _getChildSliver(
widget.businesses, widget.widescreen, widget.selectable),
);
}
Widget _getHeaderRow(bool selectable) {
if (selectable) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary),
),
getNameFromType(
widget.type, Theme.of(context).colorScheme.onPrimary),
],
),
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Checkbox(
checkColor: Theme.of(context).colorScheme.primary,
activeColor: Theme.of(context).colorScheme.onPrimary,
value: selectedBusinesses.containsAll(widget.businesses),
onChanged: (value) {
if (value!) {
setState(() {
selectedBusinesses.addAll(widget.businesses);
});
} else {
setState(() {
selectedBusinesses.removeAll(widget.businesses);
});
}
},
),
),
],
);
} else {
return Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
child: getIconFromType(
widget.type, 24, Theme.of(context).colorScheme.onPrimary),
),
getNameFromType(widget.type, Theme.of(context).colorScheme.onPrimary),
],
);
}
}
Widget _getChildSliver(
List<Business> businesses, bool widescreen, bool selectable) {
if (widescreen) {
return SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisExtent: 250.0,
maxCrossAxisExtent: 400.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
// childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return BusinessCard(
business: businesses[index],
selectable: selectable,
widescreen: widescreen,
callback: refresh,
);
},
),
);
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: businesses.length,
(BuildContext context, int index) {
return BusinessCard(
business: businesses[index],
selectable: selectable,
widescreen: widescreen,
callback: refresh,
);
},
),
);
}
}
}
class BusinessCard extends StatefulWidget {
final Business business;
final bool widescreen;
final bool selectable;
final Function callback;
const BusinessCard(
{super.key,
required this.business,
required this.widescreen,
required this.selectable,
required this.callback});
@override
State<BusinessCard> createState() => _BusinessCardState();
}
class _BusinessCardState extends State<BusinessCard> {
@override
Widget build(BuildContext context) {
if (widget.widescreen) {
return _businessTile(widget.business, widget.selectable);
} else {
return _businessListItem(
widget.business, widget.selectable, widget.callback);
}
}
Widget _businessTile(Business business, bool selectable) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(inputBusiness: business)));
},
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getTileRow(business, selectable, widget.callback),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.description,
maxLines: selectable ? 7 : 5,
overflow: TextOverflow.ellipsis,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: !selectable
? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.link),
onPressed: () {
launchUrl(
Uri.parse('https://${business.website}'));
},
),
if (business.locationName.isNotEmpty)
IconButton(
icon: const Icon(Icons.location_on),
onPressed: () {
launchUrl(Uri.parse(Uri.encodeFull(
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
},
),
if (business.contactPhone.isNotEmpty)
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Theme.of(context)
.colorScheme
.background,
title: Text(
'Contact ${business.contactName}?'),
content: Text(
'Would you like to call or text ${business.contactName}?'),
actions: [
TextButton(
child: const Text('Text'),
onPressed: () {
launchUrl(Uri.parse(
'sms:${business.contactPhone}'));
Navigator.of(context).pop();
}),
TextButton(
child: const Text('Call'),
onPressed: () async {
launchUrl(Uri.parse(
'tel:${business.contactPhone}'));
Navigator.of(context).pop();
}),
],
);
});
},
),
if (business.contactEmail.isNotEmpty)
IconButton(
icon: const Icon(Icons.email),
onPressed: () {
launchUrl(Uri.parse(
'mailto:${business.contactEmail}'));
},
),
],
)
: null),
],
),
),
),
);
}
Widget _getTileRow(Business business, bool selectable, Function callback) {
if (selectable) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 48, width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(
business.type, 48, Theme.of(context).colorScheme.onSurface);
}),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.name,
style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: _checkbox(callback),
)
],
);
} else {
return Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 48, width: 48, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48,
Theme.of(context).colorScheme.onSurface);
}),
)),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
business.name,
style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
);
}
}
Widget _businessListItem(
Business business, bool selectable, Function callback) {
return Card(
child: ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(3.0),
child: Image.network('$apiAddress/logos/${business.id}',
height: 24, width: 24, errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(
business.type, 24, Theme.of(context).colorScheme.onSurface);
})),
title: Text(business.name),
subtitle: Text(business.description,
maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: _getCheckbox(selectable, callback),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => BusinessDetail(inputBusiness: business)));
},
),
);
}
Widget _checkbox(Function callback) {
return Checkbox(
value: selectedBusinesses.contains(widget.business),
onChanged: (value) {
if (value!) {
setState(() {
selectedBusinesses.add(widget.business);
});
} else {
setState(() {
selectedBusinesses.remove(widget.business);
});
}
callback();
},
);
}
Widget? _getCheckbox(bool selectable, Function callback) {
if (selectable) {
return _checkbox(callback);
} else {
return null;
}
}
}
class FilterChips extends StatefulWidget {
const FilterChips({super.key});
@override
State<FilterChips> createState() => _FilterChipsState();
}
class _FilterChipsState extends State<FilterChips> {
List<Padding> filterChips() {
List<Padding> chips = [];
for (var type in BusinessType.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: Text(type.name),
selected: selectedChips.contains(type),
onSelected: (bool selected) {
setState(() {
if (selected) {
selectedChips.add(type);
} else {
selectedChips.remove(type);
}
});
}),
));
}
return chips;
}
@override
Widget build(BuildContext context) {
return Wrap(
children: filterChips(),
);
}
}
class FilterDataTypeChips extends StatefulWidget {
const FilterDataTypeChips({super.key});
@override
State<FilterDataTypeChips> createState() => _FilterDataTypeChipsState();
}
class _FilterDataTypeChipsState extends State<FilterDataTypeChips> {
List<Padding> filterDataTypeChips() {
List<Padding> 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(),
);
}
}