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.
|
||||
///
|
||||
/// 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 {
|
||||
/// Creates an [AvailableOrderCard].
|
||||
const AvailableOrderCard({
|
||||
@@ -25,6 +26,10 @@ class AvailableOrderCard extends StatelessWidget {
|
||||
/// Whether a booking request is currently in progress.
|
||||
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".
|
||||
String _formatDateShort(String dateStr) {
|
||||
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.
|
||||
String _orderTypeLabel(OrderType type) {
|
||||
switch (type) {
|
||||
@@ -70,12 +85,12 @@ class AvailableOrderCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final AvailableOrderSchedule schedule = order.schedule;
|
||||
final int spotsLeft = order.requiredWorkerCount - order.filledCount;
|
||||
final String hourlyDisplay =
|
||||
'\$${order.hourlyRate.toStringAsFixed(order.hourlyRate.truncateToDouble() == order.hourlyRate ? 0 : 2)}';
|
||||
final double durationHours = _durationHours();
|
||||
final double estimatedTotal = order.hourlyRate * durationHours;
|
||||
final String dateRange =
|
||||
'${_formatDateShort(schedule.startDate)} - ${_formatDateShort(schedule.endDate)}';
|
||||
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(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
@@ -89,8 +104,8 @@ class AvailableOrderCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// -- Badge row: order type, instant book, dispatch team --
|
||||
_buildBadgeRow(),
|
||||
// -- Badge row --
|
||||
_buildBadgeRow(spotsLeft),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
|
||||
// -- Main content row: icon + details + pay --
|
||||
@@ -99,171 +114,138 @@ class AvailableOrderCard extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
// Role icon
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
width: UiConstants.space10,
|
||||
height: UiConstants.space10,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: <Color>[
|
||||
UiColors.primary.withValues(alpha: 0.09),
|
||||
UiColors.primary.withValues(alpha: 0.03),
|
||||
],
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
color: UiColors.tagInProgress,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
UiIcons.briefcase,
|
||||
color: UiColors.primary,
|
||||
size: UiConstants.iconMd,
|
||||
size: UiConstants.space5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
|
||||
// Details
|
||||
// Details + pay
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Role name + hourly rate
|
||||
// Role name + estimated total
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: UiConstants.space1,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
Flexible(
|
||||
child: Text(
|
||||
order.roleName,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (order.clientName.isNotEmpty)
|
||||
),
|
||||
Text(
|
||||
order.clientName,
|
||||
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}',
|
||||
'\$${estimatedTotal.toStringAsFixed(0)}',
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
'${order.filledCount}/${order.requiredWorkerCount} ${t.available_orders.spots_left(count: spotsLeft)}',
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
// Time subtitle + hourly rate
|
||||
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(
|
||||
UiIcons.calendar,
|
||||
size: UiConstants.iconXs,
|
||||
color: UiColors.iconSecondary,
|
||||
size: UiConstants.space3,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
dateRange,
|
||||
style: UiTypography.footnote1r.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,
|
||||
),
|
||||
],
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
t.available_orders.shifts_count(
|
||||
count: schedule.totalShifts,
|
||||
@@ -271,10 +253,7 @@ class AvailableOrderCard extends StatelessWidget {
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
|
||||
// -- Action button --
|
||||
@@ -322,7 +301,7 @@ class AvailableOrderCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Builds the horizontal row of badge chips at the top of the card.
|
||||
Widget _buildBadgeRow() {
|
||||
Widget _buildBadgeRow(int spotsLeft) {
|
||||
return Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space1,
|
||||
@@ -335,6 +314,15 @@ class AvailableOrderCard extends StatelessWidget {
|
||||
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
|
||||
if (order.instantBook)
|
||||
_buildBadge(
|
||||
@@ -393,7 +381,6 @@ class AvailableOrderCard extends StatelessWidget {
|
||||
|
||||
/// Builds a small chip showing a day-of-week abbreviation.
|
||||
Widget _buildDayChip(DayOfWeek day) {
|
||||
// Display as 3-letter capitalised abbreviation (e.g. "MON" -> "Mon").
|
||||
final String label = day.value.isNotEmpty
|
||||
? '${day.value[0]}${day.value.substring(1).toLowerCase()}'
|
||||
: '';
|
||||
|
||||
Reference in New Issue
Block a user