576 lines
28 KiB
Dart
576 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 SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
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 ?? JobType.other),
|
|
style: const TextStyle(fontSize: 18),
|
|
),
|
|
Text(
|
|
listing.description,
|
|
),
|
|
],
|
|
),
|
|
contentPadding:
|
|
const EdgeInsets.only(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)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|