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. /// 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,176 +114,59 @@ 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(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
order.roleName,
style: UiTypography.body2m.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}',
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( child: Text(
order.locationAddress, order.roleName,
style: UiTypography.footnote2r.textSecondary, style: UiTypography.body1m.textPrimary,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1,
), ),
), ),
), Text(
'\$${estimatedTotal.toStringAsFixed(0)}',
style: UiTypography.title1m.textPrimary,
// 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(
children: [
const Icon(
UiIcons.calendar,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
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,
),
],
), ),
], ],
), ),
const SizedBox(height: UiConstants.space1), // Time subtitle + hourly rate
Row(
// Total shifts count mainAxisAlignment: MainAxisAlignment.spaceBetween,
Text( spacing: UiConstants.space1,
t.available_orders.shifts_count( children: <Widget>[
count: schedule.totalShifts, Text(
), timeRange,
style: UiTypography.footnote2r.textSecondary, style: UiTypography.body3r.textSecondary,
),
Text(
'\$${order.hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h',
style: UiTypography.footnote2r.textSecondary,
),
],
), ),
], ],
), ),
@@ -277,6 +175,87 @@ class AvailableOrderCard extends StatelessWidget {
), ),
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
// -- Date --
Row(
children: <Widget>[
const Icon(
UiIcons.calendar,
size: UiConstants.space3,
color: UiColors.mutedForeground,
),
const SizedBox(width: UiConstants.space1),
Text(
dateRange,
style: UiTypography.body3r.textSecondary,
),
],
),
const SizedBox(height: UiConstants.space1),
// -- 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,
),
style: UiTypography.footnote2r.textSecondary,
),
],
const SizedBox(height: UiConstants.space3),
// -- Action button -- // -- Action button --
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
@@ -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()}'
: ''; : '';