feat: Update AvailableOrderCard to include client name, address, and estimated total pay

This commit is contained in:
Achintha Isuru
2026-03-19 13:46:22 -04:00
parent 833cb99f6b
commit 24ff8816d2

View File

@@ -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()}'
: '';