FBLA24/fbla_ui/lib/pages/create_edit_listing.dart
2024-06-23 17:02:19 -05:00

574 lines
28 KiB
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:rive/rive.dart';
import '../main.dart';
class CreateEditJobListing extends StatefulWidget {
final JobListing? inputJobListing;
final Business? inputBusiness;
const CreateEditJobListing(
{super.key, this.inputJobListing, this.inputBusiness});
@override
State<CreateEditJobListing> createState() => _CreateEditJobListingState();
}
class _CreateEditJobListingState extends State<CreateEditJobListing> {
late Future getBusinessNameMapping;
late TextEditingController _nameController;
late TextEditingController _descriptionController;
late TextEditingController _wageController;
late TextEditingController _linkController;
List<Map<String, dynamic>> nameMapping = [];
String? typeDropdownErrorText;
String? businessDropdownErrorText;
late bool widescreen;
JobListing listing = JobListing(
id: null,
businessId: null,
name: 'Job Listing',
description: 'Add details about the business below.',
type: null,
wage: null,
link: null,
offerType: null);
bool _isLoading = false;
@override
void initState() {
super.initState();
if (widget.inputJobListing != null) {
listing = JobListing.copy(widget.inputJobListing!);
_nameController = TextEditingController(text: listing.name);
_descriptionController = TextEditingController(text: listing.description);
} else {
_nameController = TextEditingController();
_descriptionController = TextEditingController();
}
_wageController = TextEditingController(text: listing.wage);
_linkController = TextEditingController(
text: listing.link
?.replaceAll('https://', '')
.replaceAll('http://', '')
.replaceAll('www.', ''));
getBusinessNameMapping = fetchBusinessNames();
}
final formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
if (widget.inputBusiness != null) {
listing.businessId = widget.inputBusiness!.id;
}
return PopScope(
canPop: !_isLoading,
onPopInvoked: _handlePop,
child: Form(
key: formKey,
child: Scaffold(
appBar: AppBar(
title: (widget.inputJobListing != null)
? Text('Edit ${widget.inputJobListing?.name}', maxLines: 1)
: const Text('Add New Job Listing'),
),
floatingActionButton: !widescreen
? FloatingActionButton.extended(
label: const Text('Save'),
icon: _isLoading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3.0,
),
)
: const Icon(Icons.save),
onPressed: () async {
await _saveListing(context);
})
: null,
body: FutureBuilder(
future: getBusinessNameMapping,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
if (snapshot.data.runtimeType == String) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Center(
child: Text(snapshot.data,
textAlign: TextAlign.center)),
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Text('Retry'),
onPressed: () async {
var refreshedData = fetchBusinessNames();
await refreshedData;
setState(() {
getBusinessNameMapping = refreshedData;
});
},
),
),
]),
);
}
nameMapping = snapshot.data;
nameMapping.sort((a, b) =>
a['name'].toString().compareTo(b['name'].toString()));
return ListView(
children: [
Center(
child: SizedBox(
width: 800,
child: Column(
children: [
ListTile(
titleAlignment:
ListTileTitleAlignment.titleHeight,
title: Text(listing.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
getNameFromJobType(listing.type!),
style: const TextStyle(fontSize: 18),
),
Text(
listing.description,
),
],
),
contentPadding: const EdgeInsets.only(
bottom: 8, left: 16),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(
'$apiAddress/logos/${listing.businessId}',
width: 48,
height: 48, errorBuilder:
(BuildContext context,
Object exception,
StackTrace? stackTrace) {
return Icon(
getIconFromJobType(
listing.type ?? JobType.other),
size: 48);
}),
),
),
// Business Type Dropdown
Card(
child: Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: Wrap(
children: [
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0,
top: 8.0),
child: DropdownMenu<JobType>(
initialSelection:
listing.type,
label: const Text('Job Type'),
errorText:
typeDropdownErrorText,
width: calculateDropdownWidth(
context),
dropdownMenuEntries: [
for (JobType type
in JobType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromJobType(
type))
],
onSelected: (inputType) {
setState(() {
listing.type = inputType!;
typeDropdownErrorText =
null;
});
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0,
top: 8.0),
child: DropdownMenu<OfferType>(
initialSelection:
listing.offerType,
label:
const Text('Offer Type'),
errorText:
typeDropdownErrorText,
width: calculateDropdownWidth(
context),
dropdownMenuEntries: [
for (OfferType type
in OfferType.values)
DropdownMenuEntry(
value: type,
label:
getNameFromOfferType(
type))
],
onSelected: (inputType) {
setState(() {
listing.offerType =
inputType!;
typeDropdownErrorText =
null;
});
},
),
),
],
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 16.0,
top: 8.0),
child: DropdownMenu<int>(
menuHeight: 300,
width: (MediaQuery.sizeOf(context)
.width -
24) <
776
? MediaQuery.sizeOf(context)
.width -
24
: 776,
errorText:
businessDropdownErrorText,
initialSelection:
widget.inputBusiness?.id,
label: const Text(
'Offering Business'),
dropdownMenuEntries: [
for (Map<String, dynamic> map
in nameMapping)
DropdownMenuEntry(
value: map['id']!,
label: map['name'])
],
onSelected: (inputType) {
setState(() {
listing.businessId =
inputType!;
businessDropdownErrorText =
null;
});
},
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0),
child: TextFormField(
controller: _nameController,
autovalidateMode: AutovalidateMode
.onUserInteraction,
maxLength: 30,
onChanged: (inputName) {
setState(() {
listing.name = inputName;
});
},
onTapOutside:
(PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Job Listing Name (required)',
),
validator: (value) {
if (value != null &&
value.isEmpty) {
return 'Name is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 8.0),
child: TextFormField(
controller: _descriptionController,
autovalidateMode: AutovalidateMode
.onUserInteraction,
maxLength: 500,
maxLines: null,
onChanged: (inputDesc) {
setState(() {
listing.description = inputDesc;
});
},
onTapOutside:
(PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Job Listing Description (required)',
),
validator: (value) {
if (value != null &&
value.isEmpty) {
return 'Description is required';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 16.0),
child: TextFormField(
controller: _wageController,
onChanged: (input) {
setState(() {
listing.wage = input;
});
},
onTapOutside:
(PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText: 'Wage Information',
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 16.0),
child: TextFormField(
controller: _linkController,
autovalidateMode: AutovalidateMode
.onUserInteraction,
keyboardType: TextInputType.url,
onChanged: (inputUrl) {
if (inputUrl != '') {
listing.link =
Uri.encodeFull(inputUrl);
if (!listing.link!
.contains('http://') &&
!listing.link!
.contains('https://')) {
listing.link =
'https://${listing.link}';
}
}
listing.link = null;
},
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;
},
onTapOutside:
(PointerDownEvent event) {
FocusScope.of(context).unfocus();
},
decoration: const InputDecoration(
labelText:
'Additional Information Link',
),
),
),
],
),
),
if (!widescreen)
const SizedBox(
height: 75,
)
else
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.only(
top: 8.0,
right: 8.0,
bottom: 8.0),
child: Icon(Icons.save),
),
Text('Save'),
],
),
onPressed: () async {
await _saveListing(context);
},
),
),
)
],
),
),
),
],
);
} else if (snapshot.hasError) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Text(
'Error when loading data! Error: ${snapshot.error}'),
);
}
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
return Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const SizedBox(
width: 75,
height: 75,
child: RiveAnimation.asset(
'assets/mdev_triangle_loading.riv'),
),
);
}
return const Padding(
padding: EdgeInsets.only(left: 16.0, right: 16.0),
child: Text('Error when loading data!'),
);
}),
)),
);
}
double calculateDropdownWidth(BuildContext context) {
double screenWidth = MediaQuery.sizeOf(context).width;
if ((screenWidth - 40) / 2 < 200) {
return screenWidth - 24;
} else if ((screenWidth - 40) / 2 < 380) {
return (screenWidth - 40) / 2;
} else {
return 380;
}
}
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> _saveListing(BuildContext context) async {
if (listing.type == null || listing.businessId == null) {
if (listing.type == null) {
setState(() {
typeDropdownErrorText = 'Job type is required';
});
formKey.currentState!.validate();
}
if (listing.businessId == null) {
setState(() {
businessDropdownErrorText = 'Business is required';
});
formKey.currentState!.validate();
}
} else {
setState(() {
typeDropdownErrorText = null;
businessDropdownErrorText = null;
});
if (formKey.currentState!.validate()) {
formKey.currentState?.save();
setState(() {
_isLoading = true;
});
String? result;
if (widget.inputJobListing != null) {
result = await editListing(listing);
} else {
result = await createListing(listing);
}
setState(() {
_isLoading = false;
});
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
width: 400,
behavior: SnackBarBehavior.floating,
content: Text(result)));
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const MainApp(
initialPage: 1,
)));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Check field inputs!'),
width: 200,
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
}
}
}