697 lines
26 KiB
Dart
697 lines
26 KiB
Dart
import 'package:fbla_ui/pages/create_edit_listing.dart';
|
|
import 'package:fbla_ui/pages/listing_detail.dart';
|
|
import 'package:fbla_ui/shared/api_logic.dart';
|
|
import 'package:fbla_ui/shared/export.dart';
|
|
import 'package:fbla_ui/shared/global_vars.dart';
|
|
import 'package:fbla_ui/shared/utils.dart';
|
|
import 'package:fbla_ui/shared/widgets.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
|
import 'package:rive/rive.dart';
|
|
import 'package:sliver_tools/sliver_tools.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class JobsOverview extends StatefulWidget {
|
|
final String searchQuery;
|
|
final Future refreshJobDataOverviewFuture;
|
|
final Future<void> Function(Set<JobType>?, Set<OfferType>?)
|
|
updateBusinessesCallback;
|
|
final void Function() themeCallback;
|
|
final void Function(bool) updateLoggedIn;
|
|
|
|
const JobsOverview({
|
|
super.key,
|
|
required this.searchQuery,
|
|
required this.refreshJobDataOverviewFuture,
|
|
required this.updateBusinessesCallback,
|
|
required this.themeCallback,
|
|
required this.updateLoggedIn,
|
|
});
|
|
|
|
@override
|
|
State<JobsOverview> createState() => _JobsOverviewState();
|
|
}
|
|
|
|
class _JobsOverviewState extends State<JobsOverview> {
|
|
bool _isPreviousData = false;
|
|
late Map<JobType, List<Business>> overviewBusinesses;
|
|
Set<JobType> jobTypeFilters = <JobType>{};
|
|
Set<OfferType> offerTypeFilters = <OfferType>{};
|
|
String searchQuery = '';
|
|
ScrollController controller = ScrollController();
|
|
bool _extended = true;
|
|
double prevPixelPosition = 0;
|
|
bool _isRetrying = false;
|
|
|
|
Map<JobType, List<Business>> _filterBySearch(
|
|
Map<JobType, List<Business>> businesses, String query) {
|
|
Map<JobType, List<Business>> filteredBusinesses = {};
|
|
|
|
for (JobType jobType in businesses.keys) {
|
|
filteredBusinesses[jobType] = List.from(businesses[jobType]!.where(
|
|
(element) => element.listings![0].name
|
|
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
|
|
.toLowerCase()
|
|
.contains(query
|
|
.replaceAll(RegExp(r'[^a-zA-Z]'), '')
|
|
.toLowerCase()
|
|
.trim())));
|
|
}
|
|
|
|
filteredBusinesses.removeWhere((key, value) => value.isEmpty);
|
|
return filteredBusinesses;
|
|
}
|
|
|
|
void _setSearch(String search) async {
|
|
setState(() {
|
|
searchQuery = search;
|
|
});
|
|
}
|
|
|
|
void _setFilters(Set<JobType>? newJobTypeFilters,
|
|
Set<OfferType>? newOfferTypeFilters) async {
|
|
if (newJobTypeFilters != null) {
|
|
jobTypeFilters = Set.from(newJobTypeFilters);
|
|
}
|
|
if (newOfferTypeFilters != null) {
|
|
offerTypeFilters = Set.from(newOfferTypeFilters);
|
|
}
|
|
widget.updateBusinessesCallback(jobTypeFilters, offerTypeFilters);
|
|
}
|
|
|
|
void _scrollListener() {
|
|
if ((prevPixelPosition - controller.position.pixels).abs() > 10) {
|
|
setState(() {
|
|
_extended =
|
|
controller.position.userScrollDirection == ScrollDirection.forward;
|
|
});
|
|
}
|
|
prevPixelPosition = controller.position.pixels;
|
|
}
|
|
|
|
void _generatePDF() {
|
|
List<Business> allJobs = [];
|
|
for (List<Business> businesses
|
|
in _filterBySearch(overviewBusinesses, searchQuery).values) {
|
|
allJobs.addAll(businesses);
|
|
}
|
|
|
|
generatePDF(
|
|
context: context,
|
|
documentTypeIndex: 1,
|
|
selectedJobs: Set.from(allJobs));
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
controller.addListener(_scrollListener);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
bool widescreen = MediaQuery.sizeOf(context).width >= widescreenWidth;
|
|
return Scaffold(
|
|
floatingActionButton: _getFAB(widescreen),
|
|
body: CustomScrollView(
|
|
controller: controller,
|
|
slivers: [
|
|
MainSliverAppBar(
|
|
widescreen: widescreen,
|
|
setSearch: _setSearch,
|
|
searchHintText: 'Search Job Listings',
|
|
themeCallback: widget.themeCallback,
|
|
filterIconButton:
|
|
_filterIconButton(jobTypeFilters, offerTypeFilters),
|
|
updateLoggedIn: widget.updateLoggedIn,
|
|
generatePDF: _generatePDF,
|
|
),
|
|
FutureBuilder(
|
|
future: widget.refreshJobDataOverviewFuture,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.done) {
|
|
if (snapshot.hasData) {
|
|
if (snapshot.data.runtimeType == String) {
|
|
_isPreviousData = false;
|
|
return SliverToBoxAdapter(
|
|
child: 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;
|
|
});
|
|
await widget.updateBusinessesCallback(
|
|
null, null);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
]),
|
|
));
|
|
}
|
|
|
|
overviewBusinesses = snapshot.data;
|
|
_isPreviousData = true;
|
|
|
|
return JobDisplayPanel(
|
|
jobGroupedBusinesses:
|
|
_filterBySearch(overviewBusinesses, searchQuery),
|
|
widescreen: widescreen,
|
|
);
|
|
} else if (snapshot.hasError) {
|
|
return SliverToBoxAdapter(
|
|
child: 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) {
|
|
if (_isPreviousData) {
|
|
return JobDisplayPanel(
|
|
jobGroupedBusinesses:
|
|
_filterBySearch(overviewBusinesses, searchQuery),
|
|
widescreen: widescreen,
|
|
);
|
|
} else {
|
|
return SliverToBoxAdapter(
|
|
child: 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 SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'\nError: ${snapshot.error}',
|
|
style: const TextStyle(fontSize: 18),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _filterIconButton(
|
|
Set<JobType> jobTypeFilters, Set<OfferType> offerTypeFilters) {
|
|
Set<JobType> selectedJobTypeChips = Set.from(jobTypeFilters);
|
|
Set<OfferType> selectedOfferTypeChips = Set.from(offerTypeFilters);
|
|
|
|
return IconButton(
|
|
icon: Icon(
|
|
Icons.filter_list,
|
|
color: jobTypeFilters.isNotEmpty
|
|
? Theme.of(context).colorScheme.primary
|
|
: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
onPressed: () {
|
|
selectedJobTypeChips = Set.from(jobTypeFilters);
|
|
selectedOfferTypeChips = Set.from(offerTypeFilters);
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
void setDialogState(Set<JobType>? newJobTypeFilters,
|
|
Set<OfferType>? newOfferTypeFilters) {
|
|
if (newJobTypeFilters != null) {
|
|
setState(() {
|
|
selectedJobTypeChips = newJobTypeFilters;
|
|
});
|
|
}
|
|
if (newOfferTypeFilters != null) {
|
|
setState(() {
|
|
selectedOfferTypeChips = newOfferTypeFilters;
|
|
});
|
|
}
|
|
}
|
|
|
|
List<Widget> jobTypeChips = [];
|
|
for (JobType type in JobType.values) {
|
|
jobTypeChips.add(FilterChip(
|
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
showCheckmark: false,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
side: BorderSide(
|
|
color:
|
|
Theme.of(context).colorScheme.secondary)),
|
|
selectedColor: Theme.of(context).colorScheme.secondary,
|
|
label: Text(
|
|
getNameFromJobType(type),
|
|
style: TextStyle(
|
|
color: selectedJobTypeChips.contains(type)
|
|
? Theme.of(context).colorScheme.onSecondary
|
|
: Theme.of(context).colorScheme.onSurface),
|
|
),
|
|
selected: selectedJobTypeChips.contains(type),
|
|
onSelected: (bool selected) {
|
|
if (selected) {
|
|
selectedJobTypeChips.add(type);
|
|
} else {
|
|
selectedJobTypeChips.remove(type);
|
|
}
|
|
setDialogState(selectedJobTypeChips, null);
|
|
}));
|
|
}
|
|
|
|
List<Widget> offerTypeChips = [];
|
|
for (OfferType type in OfferType.values) {
|
|
offerTypeChips.add(FilterChip(
|
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
showCheckmark: false,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
side: BorderSide(
|
|
color:
|
|
Theme.of(context).colorScheme.secondary)),
|
|
selectedColor: Theme.of(context).colorScheme.secondary,
|
|
label: Text(
|
|
getNameFromOfferType(type),
|
|
style: TextStyle(
|
|
color: selectedOfferTypeChips.contains(type)
|
|
? Theme.of(context).colorScheme.onSecondary
|
|
: Theme.of(context).colorScheme.onSurface),
|
|
),
|
|
selected: selectedOfferTypeChips.contains(type),
|
|
onSelected: (bool selected) {
|
|
if (selected) {
|
|
selectedOfferTypeChips.add(type);
|
|
} else {
|
|
selectedOfferTypeChips.remove(type);
|
|
}
|
|
setDialogState(null, selectedOfferTypeChips);
|
|
}));
|
|
}
|
|
|
|
return AlertDialog(
|
|
title: const Text('Filter Options'),
|
|
content: SizedBox(
|
|
width: 400,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text('Job Type Filters:'),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: jobTypeChips,
|
|
),
|
|
),
|
|
const Text('Offer Type Filters:'),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: offerTypeChips,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text('Reset'),
|
|
onPressed: () {
|
|
_setFilters(<JobType>{}, <OfferType>{});
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
TextButton(
|
|
child: const Text('Cancel'),
|
|
onPressed: () {
|
|
// setDialogState(jobTypeFilters, offerTypeFilters);
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
TextButton(
|
|
child: const Text('Apply'),
|
|
onPressed: () {
|
|
_setFilters(
|
|
selectedJobTypeChips, selectedOfferTypeChips);
|
|
Navigator.of(context).pop();
|
|
},
|
|
)
|
|
],
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
Widget? _getFAB(bool widescreen) {
|
|
if (!widescreen && loggedIn) {
|
|
return FloatingActionButton.extended(
|
|
extendedIconLabelSpacing: _extended ? 8.0 : 0,
|
|
extendedPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
icon: const Icon(Icons.add),
|
|
label: AnimatedSize(
|
|
curve: Easing.standard,
|
|
duration: const Duration(milliseconds: 300),
|
|
child: _extended ? const Text('Add Job Listing') : Container(),
|
|
),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const CreateEditJobListing()));
|
|
},
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class JobDisplayPanel extends StatefulWidget {
|
|
final Map<JobType, List<Business>> jobGroupedBusinesses;
|
|
final bool widescreen;
|
|
|
|
const JobDisplayPanel({
|
|
super.key,
|
|
required this.jobGroupedBusinesses,
|
|
required this.widescreen,
|
|
});
|
|
|
|
@override
|
|
State<JobDisplayPanel> createState() => _JobDisplayPanelState();
|
|
}
|
|
|
|
class _JobDisplayPanelState extends State<JobDisplayPanel> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (widget.jobGroupedBusinesses.keys.isEmpty) {
|
|
return const SliverToBoxAdapter(
|
|
child: Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: Text(
|
|
'No results found!\nPlease change your search filters.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 18),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<_JobHeader> headers = [];
|
|
for (JobType jobType in widget.jobGroupedBusinesses.keys) {
|
|
headers.add(_JobHeader(
|
|
jobType: jobType,
|
|
widescreen: widget.widescreen,
|
|
businesses: widget.jobGroupedBusinesses[jobType]!));
|
|
}
|
|
headers.sort((a, b) => a.jobType.index.compareTo(b.jobType.index));
|
|
return MultiSliver(children: headers);
|
|
}
|
|
}
|
|
|
|
class _JobHeader extends StatefulWidget {
|
|
final JobType jobType;
|
|
final List<Business> businesses;
|
|
final bool widescreen;
|
|
|
|
const _JobHeader({
|
|
required this.jobType,
|
|
required this.businesses,
|
|
required this.widescreen,
|
|
});
|
|
|
|
@override
|
|
State<_JobHeader> createState() => _JobHeaderState();
|
|
}
|
|
|
|
class _JobHeaderState extends State<_JobHeader> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SliverStickyHeader(
|
|
header: Container(
|
|
height: 55.0,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
alignment: Alignment.centerLeft,
|
|
child: _getHeaderRow(),
|
|
),
|
|
sliver: _getChildSliver(widget.businesses, widget.widescreen),
|
|
);
|
|
}
|
|
|
|
Widget _getHeaderRow() {
|
|
return Row(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 4.0, right: 12.0),
|
|
child: Icon(
|
|
getIconFromJobType(widget.jobType),
|
|
color: Theme.of(context).colorScheme.onPrimary,
|
|
)),
|
|
Text(getNameFromJobType(widget.jobType),
|
|
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _getChildSliver(List<Business> businesses, bool widescreen) {
|
|
if (widescreen) {
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.all(4),
|
|
sliver: SliverGrid(
|
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
mainAxisExtent: 250.0,
|
|
maxCrossAxisExtent: 400.0,
|
|
mainAxisSpacing: 4.0,
|
|
crossAxisSpacing: 4.0,
|
|
),
|
|
delegate: SliverChildBuilderDelegate(
|
|
childCount: businesses.length,
|
|
(BuildContext context, int index) {
|
|
return _jobBusinessTile(
|
|
businesses[index],
|
|
widget.jobType,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
return SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
childCount: businesses.length,
|
|
(BuildContext context, int index) {
|
|
return _jobBusinessListItem(
|
|
businesses[index],
|
|
widget.jobType,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A desktop widget that displays basic info about a job
|
|
Widget _jobBusinessTile(Business business, JobType jobType) {
|
|
return MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (context) => JobListingDetail(
|
|
listing: business.listings![0],
|
|
fromBusiness: business,
|
|
)));
|
|
},
|
|
child: Card(
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Badge(
|
|
label: Text(
|
|
getLetterFromOfferType(
|
|
business.listings![0].offerType!),
|
|
style: const TextStyle(fontSize: 16),
|
|
),
|
|
largeSize: 24,
|
|
offset: const Offset(12, -3),
|
|
textColor: Colors.white,
|
|
backgroundColor: getColorFromOfferType(
|
|
business.listings![0].offerType!),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(6.0),
|
|
child: Image.network(
|
|
'$apiAddress/logos/${business.id}',
|
|
height: 48,
|
|
width: 48, errorBuilder: (BuildContext context,
|
|
Object exception, StackTrace? stackTrace) {
|
|
return Icon(
|
|
getIconFromJobType(business.listings![0].type!),
|
|
size: 48);
|
|
}),
|
|
),
|
|
)),
|
|
Flexible(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'${business.listings![0].name} (${getNameFromOfferType(business.listings![0].offerType!)})',
|
|
style: const TextStyle(
|
|
fontSize: 18, fontWeight: FontWeight.bold),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
business.listings![0].description,
|
|
maxLines: 5,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
if (business.listings![0].link != null &&
|
|
business.listings![0].link!.isNotEmpty)
|
|
IconButton(
|
|
icon: const Icon(Icons.link),
|
|
onPressed: () {
|
|
launchUrl(Uri.parse(business.listings![0].link!));
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.location_on),
|
|
onPressed: () {
|
|
launchUrl(Uri.parse(Uri.encodeFull(
|
|
'https://www.google.com/maps/search/?api=1&query=${business.locationName}')));
|
|
},
|
|
),
|
|
if (business.contactPhone != null)
|
|
IconButton(
|
|
icon: const Icon(Icons.phone),
|
|
onPressed: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.surface,
|
|
title:
|
|
Text('Contact ${business.contactName}'),
|
|
content: Text(
|
|
'Would you like to call or text ${business.contactName}?'),
|
|
actions: [
|
|
TextButton(
|
|
child: const Text('Text'),
|
|
onPressed: () {
|
|
launchUrl(Uri.parse(
|
|
'sms:${business.contactPhone}'));
|
|
Navigator.of(context).pop();
|
|
}),
|
|
TextButton(
|
|
child: const Text('Call'),
|
|
onPressed: () async {
|
|
launchUrl(Uri.parse(
|
|
'tel:${business.contactPhone}'));
|
|
Navigator.of(context).pop();
|
|
}),
|
|
],
|
|
);
|
|
});
|
|
},
|
|
),
|
|
if (business.contactEmail != null)
|
|
IconButton(
|
|
icon: const Icon(Icons.email),
|
|
onPressed: () {
|
|
launchUrl(
|
|
Uri.parse('mailto:${business.contactEmail}'));
|
|
},
|
|
),
|
|
],
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// A mobile widget that displays basic info about a job
|
|
Widget _jobBusinessListItem(Business business, JobType? jobType) {
|
|
return Card(
|
|
child: ListTile(
|
|
leading: Badge(
|
|
label: Text(getLetterFromOfferType(business.listings![0].offerType!)),
|
|
textColor: Colors.white,
|
|
isLabelVisible: true,
|
|
offset: const Offset(7, -5),
|
|
alignment: Alignment.topRight,
|
|
padding: business.listings![0].offerType! == OfferType.internship
|
|
? const EdgeInsets.symmetric(horizontal: 5)
|
|
: null,
|
|
backgroundColor:
|
|
getColorFromOfferType(business.listings![0].offerType!),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(3.0),
|
|
child: Image.network('$apiAddress/logos/${business.id}',
|
|
height: 24, width: 24, errorBuilder: (BuildContext context,
|
|
Object exception, StackTrace? stackTrace) {
|
|
return Icon(
|
|
getIconFromJobType(business.listings![0].type!),
|
|
);
|
|
})),
|
|
),
|
|
title: Text(
|
|
'${business.listings![0].name} (${getNameFromOfferType(business.listings![0].offerType!)})'),
|
|
subtitle: Text(business.listings![0].description,
|
|
maxLines: 2, overflow: TextOverflow.ellipsis),
|
|
onTap: () {
|
|
Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (context) => JobListingDetail(
|
|
listing: business.listings![0],
|
|
fromBusiness: business,
|
|
)));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|