API Rewrite with UI

This commit is contained in:
2024-06-13 15:30:48 -05:00
parent 64e493012a
commit cfade0e075
5 changed files with 587 additions and 389 deletions
+271 -189
View File
@@ -4,229 +4,311 @@ 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:rive/rive.dart';
import 'package:url_launcher/url_launcher.dart';
class BusinessDetail extends StatefulWidget {
final Business inputBusiness;
final int id;
final String name;
final JobType clickFromType;
const BusinessDetail({super.key, required this.inputBusiness});
const BusinessDetail(
{super.key,
required this.id,
required this.name,
required this.clickFromType});
@override
State<BusinessDetail> createState() => _CreateBusinessDetailState();
}
class _CreateBusinessDetailState extends State<BusinessDetail> {
late Future loadBusiness;
@override
void initState() {
super.initState();
loadBusiness = fetchBusiness(widget.id);
}
@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: [
// Title, logo, desc, website
Card(
return FutureBuilder(
future: loadBusiness,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType != String) {
return Scaffold(
appBar: AppBar(
title: Text(snapshot.data.name),
actions: _getActions(snapshot.data, widget.clickFromType),
),
body: _detailBody(snapshot.data),
);
} else {
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: 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 = fetchBusiness(widget.id);
setState(() {
loadBusiness = refreshedData;
});
},
),
),
]),
),
);
}
}
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: 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 Padding(
padding: const EdgeInsets.all(8.0),
child: Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Text(
'\nError: ${snapshot.error}',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
),
);
});
}
ListView _detailBody(Business business) {
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),
child: 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 getIconFromType(business.type, 48,
Theme.of(context).colorScheme.onSurface);
}),
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(
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 != null,
child: ListTile(
leading: const Icon(Icons.email),
title: Text(business.contactEmail!),
onTap: () {
launchUrl(Uri.parse('mailto:${business.contactEmail}'));
},
),
),
],
),
),
// Available positions
Card(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Available Postitions',
style: const 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(''));
},
),
],
),
]),
),
// Location
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}')));
},
),
),
// Contact info
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}'));
},
),
),
],
),
// Notes
Visibility(
visible: business.notes != null,
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!),
),
),
// Location
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}')));
},
),
),
),
// Notes
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) {
List<Widget>? _getActions(Business business, JobType clickFromType) {
if (loggedIn) {
return [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
CreateEditBusiness(inputBusiness: business)));
builder: (context) => CreateEditBusiness(
inputBusiness: business,
clickFromType: clickFromType,
)));
},
),
IconButton(
@@ -250,7 +332,7 @@ class _CreateBusinessDetailState extends State<BusinessDetail> {
child: const Text('Yes'),
onPressed: () async {
String? deleteResult =
await deleteBusiness(business, jwt);
await deleteBusiness(business.id, jwt);
if (deleteResult != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
+51 -46
View File
@@ -6,8 +6,9 @@ import 'package:flutter/services.dart';
class CreateEditBusiness extends StatefulWidget {
final Business? inputBusiness;
final JobType? clickFromType;
const CreateEditBusiness({super.key, this.inputBusiness});
const CreateEditBusiness({super.key, this.inputBusiness, this.clickFromType});
@override
State<CreateEditBusiness> createState() => _CreateEditBusinessState();
@@ -27,7 +28,6 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
id: 0,
name: 'Business',
description: 'Add details about the business below.',
type: BusinessType.other,
website: '',
contactName: '',
contactEmail: '',
@@ -155,7 +155,9 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
'https://logo.clearbit.com/${business.website}',
errorBuilder: (BuildContext context,
Object exception, StackTrace? stackTrace) {
return getIconFromType(business.type, 48,
return getIconFromJobType(
widget.clickFromType ?? JobType.other,
48,
Theme.of(context).colorScheme.onBackground);
}),
),
@@ -239,7 +241,8 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Business Description (required)',
labelText:
'Business Description (required)',
),
validator: (value) {
if (value != null && value.isEmpty) {
@@ -357,46 +360,49 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
),
),
),
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.entertainment,
label: 'Entertainment'),
DropdownMenuEntry(
value: BusinessType.other,
label: 'Other'),
],
onSelected: (inputType) {
business.type = inputType!;
},
),
],
),
),
// 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<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.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),
@@ -409,8 +415,7 @@ class _CreateEditBusinessState extends State<CreateEditBusiness> {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Contact Information Name',
labelText: 'Contact Information Name',
),
),
),
+42 -31
View File
@@ -16,9 +16,9 @@ bool isBusinessesFiltered = true;
bool _isLoading = false;
class ExportData extends StatefulWidget {
final List<Business> businesses;
final Map<JobType, List<Business>> groupedBusinesses;
const ExportData({super.key, required this.businesses});
const ExportData({super.key, required this.groupedBusinesses});
@override
State<ExportData> createState() => _ExportDataState();
@@ -31,7 +31,7 @@ class _ExportDataState extends State<ExportData> {
void initState() {
super.initState();
refreshBusinessDataFuture = fetchBusinessData();
refreshBusinessDataFuture = fetchBusinessDataOverview();
_isLoading = false;
selectedBusinesses = <Business>{};
}
@@ -39,7 +39,7 @@ class _ExportDataState extends State<ExportData> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _FAB(businesses: widget.businesses),
floatingActionButton: _FAB(groupedBusinesses: widget.groupedBusinesses),
body: CustomScrollView(
slivers: [
SliverAppBar(
@@ -196,7 +196,7 @@ class _ExportDataState extends State<ExportData> {
),
),
BusinessDisplayPanel(
businesses: widget.businesses,
groupedBusinesses: widget.groupedBusinesses,
widescreen: MediaQuery.sizeOf(context).width >= 1000,
selectable: true),
const SliverToBoxAdapter(
@@ -211,15 +211,26 @@ class _ExportDataState extends State<ExportData> {
}
class _FAB extends StatefulWidget {
final List<Business> businesses;
final Map<JobType, List<Business>> groupedBusinesses;
const _FAB({required this.businesses});
const _FAB({required this.groupedBusinesses});
@override
State<_FAB> createState() => _FABState();
}
class _FABState extends State<_FAB> {
List<Business> allBusinesses = [];
@override
void initState() {
super.initState();
for (JobType type in widget.groupedBusinesses.keys) {
allBusinesses.addAll(widget.groupedBusinesses[type]!);
}
}
@override
Widget build(BuildContext context) {
return FloatingActionButton(
@@ -273,7 +284,7 @@ class _FABState extends State<_FAB> {
List<pw.TableRow> rows = [];
if (selectedBusinesses.isEmpty) {
selectedBusinesses.addAll(widget.businesses);
selectedBusinesses.addAll(allBusinesses);
isBusinessesFiltered = false;
} else {
isBusinessesFiltered = true;
@@ -284,9 +295,9 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.logo)) {
remainingSpace -= 32;
}
if (dataTypeFilters.contains(DataType.type)) {
remainingSpace -= 56;
}
// if (dataTypeFilters.contains(DataType.type)) {
// remainingSpace -= 56;
// }
if (dataTypeFilters.contains(DataType.contactName)) {
remainingSpace -= 72;
}
@@ -331,9 +342,9 @@ class _FABState extends State<_FAB> {
} 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.type) {
// width = const pw.FixedColumnWidth(56);
// columnNum++;
} else if (dataType == DataType.website) {
width = pw.FixedColumnWidth(websiteWidth);
columnNum++;
@@ -394,18 +405,18 @@ class _FABState extends State<_FAB> {
),
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.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,
business.website ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@@ -413,7 +424,7 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.contactName)) {
data.add(pw.Padding(
child: pw.Text(
business.contactName,
business.contactName ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@@ -421,7 +432,7 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.contactEmail)) {
data.add(pw.Padding(
child: pw.Text(
business.contactEmail,
business.contactEmail ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@@ -429,7 +440,7 @@ class _FABState extends State<_FAB> {
if (dataTypeFilters.contains(DataType.contactPhone)) {
data.add(pw.Padding(
child: pw.Text(
business.contactPhone,
business.contactPhone ?? '',
// style: const pw.TextStyle(fontSize: 10)
),
padding: const pw.EdgeInsets.all(4.0)));
@@ -440,7 +451,7 @@ class _FABState extends State<_FAB> {
style = const pw.TextStyle(fontSize: 8);
}
data.add(pw.Padding(
child: pw.Text(business.notes, style: style),
child: pw.Text(business.notes ?? '', style: style),
padding: const pw.EdgeInsets.all(4.0)));
}
@@ -461,10 +472,10 @@ class _FABState extends State<_FAB> {
} else {
rows.add(pw.TableRow(
children: [
pw.Padding(
child: getPwIconFromType(
business.type, 24, PdfColors.black),
padding: const pw.EdgeInsets.all(4.0)),
// pw.Padding(
// child: getPwIconFromType(
// business.type, 24, PdfColors.black),
// padding: const pw.EdgeInsets.all(4.0)),
...data
],
));