609 lines
28 KiB
Dart
609 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(
|
|
'$apiAddress/clearbit/${Uri.encodeComponent(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) {
|
|
setState(() {
|
|
business.website =
|
|
Uri.encodeFull(inputUrl);
|
|
});
|
|
if (inputUrl.trim().isEmpty) {
|
|
business.website = null;
|
|
} else {
|
|
if (!business.website!
|
|
.contains('http://') &&
|
|
!business.website!
|
|
.contains('https://')) {
|
|
setState(() {
|
|
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));
|
|
}
|
|
}
|