feat: Update AvailableOrderCard to include client name, address, and estimated total pay
This commit is contained in:
@@ -6,7 +6,8 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
|
|
||||||
/// Card displaying an [AvailableOrder] from the staff marketplace.
|
/// Card displaying an [AvailableOrder] from the staff marketplace.
|
||||||
///
|
///
|
||||||
/// Shows role, location, schedule, pay rate, and a booking/apply action.
|
/// Shows role, pay (total + hourly), time, date, client, location,
|
||||||
|
/// schedule chips, and a booking/apply action.
|
||||||
class AvailableOrderCard extends StatelessWidget {
|
class AvailableOrderCard extends StatelessWidget {
|
||||||
/// Creates an [AvailableOrderCard].
|
/// Creates an [AvailableOrderCard].
|
||||||
const AvailableOrderCard({
|
const AvailableOrderCard({
|
||||||
@@ -25,6 +26,10 @@ class AvailableOrderCard extends StatelessWidget {
|
|||||||
/// Whether a booking request is currently in progress.
|
/// Whether a booking request is currently in progress.
|
||||||
final bool bookingInProgress;
|
final bool bookingInProgress;
|
||||||
|
|
||||||
|
String _formatTime(DateTime time) {
|
||||||
|
return DateFormat('h:mma').format(time).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
/// Formats a date-only string (e.g. "2026-03-24") to "Mar 24".
|
/// Formats a date-only string (e.g. "2026-03-24") to "Mar 24".
|
||||||
String _formatDateShort(String dateStr) {
|
String _formatDateShort(String dateStr) {
|
||||||
if (dateStr.isEmpty) return '';
|
if (dateStr.isEmpty) return '';
|
||||||
@@ -36,6 +41,16 @@ class AvailableOrderCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Computes the duration in hours from the first shift start to end.
|
||||||
|
double _durationHours() {
|
||||||
|
final int minutes = order.schedule.lastShiftEndsAt
|
||||||
|
.difference(order.schedule.firstShiftStartsAt)
|
||||||
|
.inMinutes;
|
||||||
|
double hours = minutes / 60;
|
||||||
|
if (hours < 0) hours += 24;
|
||||||
|
return hours.roundToDouble();
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a human-readable label for the order type.
|
/// Returns a human-readable label for the order type.
|
||||||
String _orderTypeLabel(OrderType type) {
|
String _orderTypeLabel(OrderType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -70,12 +85,12 @@ class AvailableOrderCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AvailableOrderSchedule schedule = order.schedule;
|
final AvailableOrderSchedule schedule = order.schedule;
|
||||||
final int spotsLeft = order.requiredWorkerCount - order.filledCount;
|
final int spotsLeft = order.requiredWorkerCount - order.filledCount;
|
||||||
final String hourlyDisplay =
|
final double durationHours = _durationHours();
|
||||||
'\$${order.hourlyRate.toStringAsFixed(order.hourlyRate.truncateToDouble() == order.hourlyRate ? 0 : 2)}';
|
final double estimatedTotal = order.hourlyRate * durationHours;
|
||||||
final String dateRange =
|
final String dateRange =
|
||||||
'${_formatDateShort(schedule.startDate)} - ${_formatDateShort(schedule.endDate)}';
|
'${_formatDateShort(schedule.startDate)} - ${_formatDateShort(schedule.endDate)}';
|
||||||
final String timeRange =
|
final String timeRange =
|
||||||
'${DateFormat('h:mm a').format(schedule.firstShiftStartsAt)} - ${DateFormat('h:mm a').format(schedule.lastShiftEndsAt)}';
|
'${_formatTime(schedule.firstShiftStartsAt)} - ${_formatTime(schedule.lastShiftEndsAt)}';
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
@@ -89,8 +104,8 @@ class AvailableOrderCard extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// -- Badge row: order type, instant book, dispatch team --
|
// -- Badge row --
|
||||||
_buildBadgeRow(),
|
_buildBadgeRow(spotsLeft),
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
// -- Main content row: icon + details + pay --
|
// -- Main content row: icon + details + pay --
|
||||||
@@ -99,171 +114,138 @@ class AvailableOrderCard extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Role icon
|
// Role icon
|
||||||
Container(
|
Container(
|
||||||
width: 44,
|
width: UiConstants.space10,
|
||||||
height: 44,
|
height: UiConstants.space10,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
color: UiColors.tagInProgress,
|
||||||
colors: <Color>[
|
borderRadius: UiConstants.radiusLg,
|
||||||
UiColors.primary.withValues(alpha: 0.09),
|
|
||||||
UiColors.primary.withValues(alpha: 0.03),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
UiIcons.briefcase,
|
UiIcons.briefcase,
|
||||||
color: UiColors.primary,
|
color: UiColors.primary,
|
||||||
size: UiConstants.iconMd,
|
size: UiConstants.space5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space3),
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
|
||||||
// Details
|
// Details + pay
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Role name + hourly rate
|
// Role name + estimated total
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
spacing: UiConstants.space1,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Flexible(
|
||||||
child: Column(
|
child: Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
order.roleName,
|
order.roleName,
|
||||||
style: UiTypography.body2m.textPrimary,
|
style: UiTypography.body1m.textPrimary,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
if (order.clientName.isNotEmpty)
|
),
|
||||||
Text(
|
Text(
|
||||||
order.clientName,
|
'\$${estimatedTotal.toStringAsFixed(0)}',
|
||||||
style: UiTypography.body3r.textSecondary,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
'$hourlyDisplay${t.available_orders.per_hour}',
|
|
||||||
style: UiTypography.title1m.textPrimary,
|
style: UiTypography.title1m.textPrimary,
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
'${order.filledCount}/${order.requiredWorkerCount} ${t.available_orders.spots_left(count: spotsLeft)}',
|
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
// Time subtitle + hourly rate
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
|
|
||||||
// Location
|
|
||||||
if (order.location.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.only(bottom: UiConstants.space1),
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
const Icon(
|
|
||||||
UiIcons.mapPin,
|
|
||||||
size: UiConstants.iconXs,
|
|
||||||
color: UiColors.iconSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space1),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
order.location,
|
|
||||||
style: UiTypography.footnote1r.textSecondary,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Address
|
|
||||||
if (order.locationAddress.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.only(bottom: UiConstants.space4),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: UiConstants.iconXs + UiConstants.space1,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
order.locationAddress,
|
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
|
|
||||||
// Schedule: days of week chips
|
|
||||||
if (schedule.daysOfWeek.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.only(bottom: UiConstants.space2),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: UiConstants.space1,
|
|
||||||
runSpacing: UiConstants.space1,
|
|
||||||
children: schedule.daysOfWeek
|
|
||||||
.map(
|
|
||||||
(DayOfWeek day) => _buildDayChip(day),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Date range + time + shifts count
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
spacing: UiConstants.space1,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
timeRange,
|
||||||
|
style: UiTypography.body3r.textSecondary,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'\$${order.hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h',
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// -- Date --
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
const Icon(
|
const Icon(
|
||||||
UiIcons.calendar,
|
UiIcons.calendar,
|
||||||
size: UiConstants.iconXs,
|
size: UiConstants.space3,
|
||||||
color: UiColors.iconSecondary,
|
color: UiColors.mutedForeground,
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space1),
|
const SizedBox(width: UiConstants.space1),
|
||||||
Text(
|
Text(
|
||||||
dateRange,
|
dateRange,
|
||||||
style: UiTypography.footnote1r.textSecondary,
|
style: UiTypography.body3r.textSecondary,
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
UiIcons.clock,
|
|
||||||
size: UiConstants.iconXs,
|
|
||||||
color: UiColors.iconSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space1),
|
|
||||||
Text(
|
|
||||||
timeRange,
|
|
||||||
style: UiTypography.footnote1r.textSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
|
||||||
// Total shifts count
|
// -- Client name --
|
||||||
|
if (order.clientName.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.building,
|
||||||
|
size: UiConstants.space3,
|
||||||
|
color: UiColors.mutedForeground,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space1),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
order.clientName,
|
||||||
|
style: UiTypography.body3r.textSecondary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// -- Address --
|
||||||
|
if (order.locationAddress.isNotEmpty) ...<Widget>[
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.mapPin,
|
||||||
|
size: UiConstants.space3,
|
||||||
|
color: UiColors.mutedForeground,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space1),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
order.locationAddress,
|
||||||
|
style: UiTypography.body3r.textSecondary,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
// -- Schedule: days of week chips --
|
||||||
|
if (schedule.daysOfWeek.isNotEmpty) ...<Widget>[
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
Wrap(
|
||||||
|
spacing: UiConstants.space1,
|
||||||
|
runSpacing: UiConstants.space1,
|
||||||
|
children: schedule.daysOfWeek
|
||||||
|
.map((DayOfWeek day) => _buildDayChip(day))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
Text(
|
Text(
|
||||||
t.available_orders.shifts_count(
|
t.available_orders.shifts_count(
|
||||||
count: schedule.totalShifts,
|
count: schedule.totalShifts,
|
||||||
@@ -271,10 +253,7 @@ class AvailableOrderCard extends StatelessWidget {
|
|||||||
style: UiTypography.footnote2r.textSecondary,
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
// -- Action button --
|
// -- Action button --
|
||||||
@@ -322,7 +301,7 @@ class AvailableOrderCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the horizontal row of badge chips at the top of the card.
|
/// Builds the horizontal row of badge chips at the top of the card.
|
||||||
Widget _buildBadgeRow() {
|
Widget _buildBadgeRow(int spotsLeft) {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: UiConstants.space2,
|
spacing: UiConstants.space2,
|
||||||
runSpacing: UiConstants.space1,
|
runSpacing: UiConstants.space1,
|
||||||
@@ -335,6 +314,15 @@ class AvailableOrderCard extends StatelessWidget {
|
|||||||
borderColor: UiColors.border,
|
borderColor: UiColors.border,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Spots left badge
|
||||||
|
if (spotsLeft > 0)
|
||||||
|
_buildBadge(
|
||||||
|
label: t.available_orders.spots_left(count: spotsLeft),
|
||||||
|
backgroundColor: UiColors.tagPending,
|
||||||
|
textColor: UiColors.textWarning,
|
||||||
|
borderColor: UiColors.textWarning.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
|
||||||
// Instant book badge
|
// Instant book badge
|
||||||
if (order.instantBook)
|
if (order.instantBook)
|
||||||
_buildBadge(
|
_buildBadge(
|
||||||
@@ -393,7 +381,6 @@ class AvailableOrderCard extends StatelessWidget {
|
|||||||
|
|
||||||
/// Builds a small chip showing a day-of-week abbreviation.
|
/// Builds a small chip showing a day-of-week abbreviation.
|
||||||
Widget _buildDayChip(DayOfWeek day) {
|
Widget _buildDayChip(DayOfWeek day) {
|
||||||
// Display as 3-letter capitalised abbreviation (e.g. "MON" -> "Mon").
|
|
||||||
final String label = day.value.isNotEmpty
|
final String label = day.value.isNotEmpty
|
||||||
? '${day.value[0]}${day.value.substring(1).toLowerCase()}'
|
? '${day.value[0]}${day.value.substring(1).toLowerCase()}'
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
Reference in New Issue
Block a user