FBLA24/fbla_ui/lib/pages/create_edit_business.dart

604 lines
28 KiB
Dart

import 'package:fbla_ui/main.dart';
import 'package:fbla_ui/shared/api_logic.dart';
import 'package:fbla_ui/shared/global_vars.dart';
import 'package:fbla_ui/shared/utils.dart';
import 'package:flutter/material.dart';
import 'package: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;
late bool widescreen;
Business business = Business(
id: 0,
name: 'Business',
description: 'Add details about the business below.',
type: null,
website: null,
contactName: null,
contactEmail: null,
contactPhone: null,
notes: null,
locationName: '',
locationAddress: null,
);
bool _isLoading = false;
String? dropDownErrorText;
@override
void initState() {
super.initState();
if (widget.inputBusiness != null) {
business = Business.copy(widget.inputBusiness!);
_nameController = TextEditingController(text: business.name);
_descriptionController =
TextEditingController(text: business.description);
business.type = widget.inputBusiness?.type;
} else {
_nameController = TextEditingController();
_descriptionController = TextEditingController();
}
_websiteController = TextEditingController(
text: business.website
?.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''));
_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>();
@override
Widget build(BuildContext context) {
widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
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: !widescreen
? FloatingActionButton.extended(
heroTag: 'saveBusiness',
label: const Text('Save'),
icon: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
onPressed: () async {
if (!_isLoading) {
await _saveBusiness(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text('Please wait for it to save.'),
),
);
}
},
)
: null,
body: ListView(
children: [
Center(
child: SizedBox(
width: 800,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Card(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ListTile(
titleAlignment:
ListTileTitleAlignment.titleHeight,
title: Text(business.name!,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold)),
subtitle: Text(
business.description!,
),
contentPadding:
const EdgeInsets.only(bottom: 8, left: 16),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'https://logo.clearbit.com/${business.website}',
width: 48,
height: 48, errorBuilder:
(BuildContext context,
Object exception,
StackTrace? stackTrace) {
return Icon(
getIconFromBusinessType(business.type ??
BusinessType.other),
size: 48);
}),
),
),
),
),
),
Card(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: 8.0,
bottom: 8.0,
left: 8.0,
right: 8.0),
child: TextFormField(
controller: _nameController,
maxLength: 30,
onChanged: (inputName) {
setState(() {
business.name = inputName;
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Business Name (required)',
),
validator: (value) {
if (value != null && value.trim().isEmpty) {
return 'Name is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
bottom: 8.0, left: 8.0, right: 8.0),
child: TextFormField(
controller: _descriptionController,
maxLength: 500,
maxLines: null,
onChanged: (inputDesc) {
setState(() {
business.description = inputDesc;
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Business Description (required)',
),
validator: (value) {
if (value != null && value.trim().isEmpty) {
return 'Description is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _websiteController,
keyboardType: TextInputType.url,
onChanged: (inputUrl) {
business.website = Uri.encodeFull(inputUrl);
if (inputUrl.trim().isEmpty) {
business.website = null;
} else {
if (!business.website!
.contains('http://') &&
!business.website!
.contains('https://')) {
business.website =
'https://${business.website!.trim()}';
}
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Website',
),
validator: (value) {
if (value != null &&
value.isNotEmpty &&
!RegExp(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?:/[^/\s]*)*')
.hasMatch(value)) {
return 'Enter a valid Website';
}
return null;
},
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: DropdownMenu<BusinessType>(
initialSelection: business.type,
width: (MediaQuery.sizeOf(context).width -
24) <
776
? MediaQuery.sizeOf(context).width - 24
: 776,
menuHeight: 300,
label: const Text('Business Type'),
errorText: dropDownErrorText,
dropdownMenuEntries: [
for (BusinessType type
in BusinessType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromBusinessType(type)),
],
onSelected: (inputType) {
setState(() {
business.type = inputType!;
dropDownErrorText = null;
});
},
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _contactNameController,
onSaved: (inputText) {
business.contactName = inputText!;
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Contact Information Name (required)',
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Contact name is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _contactPhoneController,
inputFormatters: [PhoneFormatter()],
keyboardType: TextInputType.phone,
onChanged: (inputText) {
if (inputText.trim().isEmpty) {
business.contactPhone = null;
} else {
business.contactPhone = inputText.trim();
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Contact Phone #',
),
validator: (value) {
if (business.contactEmail == null &&
(value == null || value.isEmpty)) {
return 'At least one contact method is required';
}
if (value != null &&
value.isNotEmpty &&
value.length != 14) {
return 'Enter a valid phone number';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _contactEmailController,
keyboardType: TextInputType.emailAddress,
onChanged: (inputText) {
if (inputText.trim().isEmpty) {
business.contactEmail = null;
} else {
business.contactEmail = inputText.trim();
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Contact Email',
),
validator: (value) {
value = value?.trim();
if (value != null && value.isEmpty) {
value = null;
}
if (value == null &&
business.contactPhone == null) {
return 'At least one contact method is required';
}
if (value != null) {
if (!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: 16.0),
child: TextFormField(
controller: _locationNameController,
onChanged: (inputName) {
setState(() {
business.locationName = inputName.trim();
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Location Name (required)',
),
validator: (value) {
if (value != null && value.trim().isEmpty) {
return 'Location name is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 16.0),
child: TextFormField(
controller: _locationAddressController,
onChanged: (inputAddr) {
setState(() {
business.locationAddress = inputAddr;
});
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Location Address (required)',
),
validator: (value) {
if (value != null && value.trim().isEmpty) {
return 'Location Address is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, right: 8.0, bottom: 8.0),
child: TextFormField(
controller: _notesController,
maxLength: 300,
maxLines: null,
onSaved: (inputText) {
if (inputText == null ||
inputText.trim().isEmpty) {
business.notes = null;
} else {
business.notes = inputText.trim();
}
},
onTapOutside: (PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Other Notes',
),
),
),
],
),
),
if (!widescreen)
const SizedBox(
height: 75,
)
else
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(
top: 8.0, right: 8.0, bottom: 8.0),
child: _isLoading
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Theme.of(context)
.colorScheme
.onPrimary,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
),
const Text('Save'),
],
),
onPressed: () async {
if (!_isLoading) {
await _saveBusiness(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content:
Text('Please wait for it to save.'),
),
);
}
},
),
),
)
],
),
),
),
],
),
)),
);
}
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.'),
),
);
}
}
Future<void> _saveBusiness(BuildContext context) async {
if (business.type == null) {
setState(() {
dropDownErrorText = 'Business type is required';
});
formKey.currentState!.validate();
} else {
setState(() {
dropDownErrorText = null;
});
if (formKey.currentState!.validate()) {
formKey.currentState?.save();
setState(() {
_isLoading = true;
});
String? result;
if (widget.inputBusiness != null) {
result = await editBusiness(business);
} else {
result = await createBusiness(business);
}
setState(() {
_isLoading = false;
});
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text(result)));
} else {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const MainApp()));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Check field inputs!'),
width: 200,
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
}
}
}
class PhoneFormatter extends TextInputFormatter {
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));
}
}