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 createState() => _CreateEditJobListingState(); } class _CreateEditJobListingState extends State { late Future getBusinessNameMapping; late TextEditingController _nameController; late TextEditingController _descriptionController; late TextEditingController _wageController; late TextEditingController _linkController; List> 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; late String businessName; bool _isRetrying = 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(); businessName = widget.inputBusiness?.name ?? 'Offering business'; } final formKey = GlobalKey(); @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( heroTag: 'saveListing', 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 _saveListing(context); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( width: 400, behavior: SnackBarBehavior.floating, content: Text('Please wait for it to save.'), ), ); } }) : 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 { if (!_isRetrying) { setState(() { _isRetrying = true; }); var refreshedData = fetchBusinessNames(); await refreshedData; setState(() { getBusinessNameMapping = refreshedData; _isRetrying = false; }); } }, ), ), ]), ); } 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: [ 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(listing.name, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold)), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( businessName, 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: Padding( padding: const EdgeInsets.only( left: 8.0, right: 8.0, bottom: 16.0, top: 8.0), child: DropdownMenu( 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 map in nameMapping) DropdownMenuEntry( value: map['id']!, label: map['name']) ], onSelected: (inputType) { setState(() { listing.businessId = inputType!; businessName = nameMapping .where((element) => element['id'] == listing.businessId) .first['name']; businessDropdownErrorText = null; }); }, ), ), ), 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( initialSelection: listing.type, label: const Text('Job Type'), errorText: typeDropdownErrorText, width: calculateDropdownWidth( context), menuHeight: 300, 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( 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; }); }, ), ), ], ), ), Padding( padding: const EdgeInsets.only( left: 8.0, right: 8.0, bottom: 8.0), child: TextFormField( controller: _nameController, 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, 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, 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: 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 _saveListing(context); } else { ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( width: 400, behavior: SnackBarBehavior.floating, content: Text( 'Please wait for it to save.'), ), ); } }, ), ), ) ], ), ), ), ], ); } 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 _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)), ), ); } } } }