Refactor: Move detailed shift UI from card to ShiftDetailsPage

This commit is contained in:
2026-02-16 20:28:43 +05:30
parent 690d4f4213
commit 40fa4ebdfa
2 changed files with 339 additions and 686 deletions

View File

@@ -1,5 +1,5 @@
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
@@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart';
import '../blocs/shift_details/shift_details_bloc.dart'; import '../blocs/shift_details/shift_details_bloc.dart';
import '../blocs/shift_details/shift_details_event.dart'; import '../blocs/shift_details/shift_details_event.dart';
import '../blocs/shift_details/shift_details_state.dart'; import '../blocs/shift_details/shift_details_state.dart';
import '../widgets/shift_location_map.dart';
class ShiftDetailsPage extends StatefulWidget { class ShiftDetailsPage extends StatefulWidget {
final String shiftId; final String shiftId;
@@ -65,10 +66,10 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
Widget _buildStatCard(IconData icon, String value, String label) { Widget _buildStatCard(IconData icon, String value, String label) {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.background, color: UiColors.background,
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
border: Border.all(color: UiColors.border), border: Border.all(color: UiColors.border),
), ),
child: Column( child: Column(
@@ -80,12 +81,12 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
color: UiColors.white, color: UiColors.white,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon(icon, size: 20, color: UiColors.iconSecondary), child: Icon(icon, size: 20, color: UiColors.textSecondary),
), ),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
Text( Text(
value, value,
style: UiTypography.title1m.textPrimary, style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary,
), ),
Text( Text(
label, label,
@@ -98,21 +99,22 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
Widget _buildTimeBox(String label, String time) { Widget _buildTimeBox(String label, String time) {
return Container( return Container(
padding: const EdgeInsets.all(UiConstants.space4), padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.background, color: UiColors.background,
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
), ),
child: Column( child: Column(
children: [ children: [
Text( Text(
label, label,
style: UiTypography.titleUppercase4b.textSecondary, style: UiTypography.footnote2b.copyWith(
color: UiColors.textSecondary, letterSpacing: 0.5),
), ),
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
Text( Text(
_formatTime(time), _formatTime(time),
style: UiTypography.headline2m.textPrimary, style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary,
), ),
], ],
), ),
@@ -267,45 +269,49 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
), ),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
// Worker Capacity / Open Slots // Stats Row (New)
if ((displayShift.requiredSlots ?? 0) > 0) Row(
Container( children: [
padding: const EdgeInsets.all(UiConstants.space4), Expanded(
decoration: BoxDecoration( child: _buildStatCard(
color: UiColors.success.withValues(alpha: 0.1), UiIcons.dollar,
borderRadius: BorderRadius.circular(UiConstants.radiusBase), "\$${estimatedTotal.toStringAsFixed(0)}",
"Total",
),
), ),
child: Row( const SizedBox(width: UiConstants.space4),
children: [ Expanded(
const Icon( child: _buildStatCard(
UiIcons.users, UiIcons.dollar,
size: 16, "\$${displayShift.hourlyRate.toStringAsFixed(0)}",
color: UiColors.success, "Hourly Rate",
), ),
const SizedBox(width: UiConstants.space2),
Text(
i18n.slots_remaining(count: openSlots),
style: UiTypography.footnote1m.textSuccess,
),
],
), ),
), const SizedBox(width: UiConstants.space4),
Expanded(
child: _buildStatCard(
UiIcons.clock,
"${duration.toStringAsFixed(1)}",
"Hours",
),
),
],
),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
// Time Section // Time Section (New)
Row( Row(
children: [ children: [
Expanded( Expanded(
child: _buildTimeBox( child: _buildTimeBox(
i18n.start_time, "CLOCK IN TIME",
displayShift.startTime, displayShift.startTime,
), ),
), ),
const SizedBox(width: UiConstants.space4), const SizedBox(width: UiConstants.space4),
Expanded( Expanded(
child: _buildTimeBox( child: _buildTimeBox(
i18n.end_time, "CLOCK OUT TIME",
displayShift.endTime, displayShift.endTime,
), ),
), ),
@@ -313,97 +319,79 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
), ),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
// Quick Info Grid
Row(
children: [
Expanded(
child: _buildStatCard(
UiIcons.dollar,
"\$${displayShift.hourlyRate.toStringAsFixed(0)}/hr",
i18n.base_rate,
),
),
const SizedBox(width: UiConstants.space4),
Expanded(
child: _buildStatCard(
UiIcons.clock,
i18n.hours_label(count: duration.toInt()),
i18n.duration,
),
),
const SizedBox(width: UiConstants.space4),
Expanded(
child: _buildStatCard(
UiIcons.wallet,
"\$${estimatedTotal.toStringAsFixed(0)}",
i18n.est_total,
),
),
],
),
const SizedBox(height: UiConstants.space8),
// Location Section // Location Section (New with Map)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
i18n.location, "LOCATION",
style: UiTypography.titleUppercase4b.textSecondary, style: UiTypography.titleUppercase4b.textSecondary,
), ),
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
Container( Row(
padding: const EdgeInsets.all(UiConstants.space4), mainAxisAlignment:
decoration: BoxDecoration( MainAxisAlignment.spaceBetween,
color: UiColors.white, children: [
borderRadius: BorderRadius.circular(UiConstants.radiusBase), Expanded(
border: Border.all(color: UiColors.border), child: Text(
), displayShift.location.isEmpty
child: Column( ? "TBD"
children: [ : displayShift.location,
Row( style: UiTypography.title1m.textPrimary,
children: [ overflow: TextOverflow.ellipsis,
const Icon( ),
UiIcons.mapPin, ),
color: UiColors.primary, const SizedBox(width: UiConstants.space3),
size: 20, OutlinedButton.icon(
), onPressed: () {
const SizedBox(width: UiConstants.space3), ScaffoldMessenger.of(
Expanded( context,
child: Column( ).showSnackBar(
crossAxisAlignment: SnackBar(
CrossAxisAlignment.start, content: Text(
children: [ displayShift!.locationAddress.isNotEmpty
Text( ? displayShift!.locationAddress
displayShift.location, : displayShift!.location,
style: UiTypography.body2b.textPrimary, ),
), duration: const Duration(
Text( seconds: 3,
displayShift.locationAddress,
style: UiTypography.body3r.textSecondary,
),
],
), ),
), ),
], );
},
icon: const Icon(
UiIcons.navigation,
size: UiConstants.iconXs,
), ),
const SizedBox(height: UiConstants.space4), label: const Text(
const Divider(), "Get direction",
const SizedBox(height: UiConstants.space2),
TextButton.icon(
onPressed: () {},
icon: const Icon(
UiIcons.arrowRight,
size: 16,
),
label: Text(i18n.open_in_maps),
style: TextButton.styleFrom(
foregroundColor: UiColors.primary,
padding: EdgeInsets.zero,
),
), ),
], style: OutlinedButton.styleFrom(
), foregroundColor:
UiColors.textPrimary,
side: const BorderSide(
color: UiColors.border,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
),
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: 0,
),
minimumSize: const Size(0, 32),
),
),
],
),
const SizedBox(height: UiConstants.space3),
ShiftLocationMap(
shift: displayShift,
height: 160,
borderRadius: UiConstants.radiusBase,
), ),
], ],
), ),

View File

@@ -4,7 +4,6 @@ import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import 'shift_location_map.dart';
import 'package:krow_core/core.dart'; // For modular navigation import 'package:krow_core/core.dart'; // For modular navigation
class MyShiftCard extends StatefulWidget { class MyShiftCard extends StatefulWidget {
@@ -27,8 +26,7 @@ class MyShiftCard extends StatefulWidget {
State<MyShiftCard> createState() => _MyShiftCardState(); State<MyShiftCard> createState() => _MyShiftCardState();
} }
class _MyShiftCardState extends State<MyShiftCard> with TickerProviderStateMixin { class _MyShiftCardState extends State<MyShiftCard> {
bool _isExpanded = false;
String _formatTime(String time) { String _formatTime(String time) {
if (time.isEmpty) return ''; if (time.isEmpty) return '';
@@ -104,7 +102,6 @@ class _MyShiftCardState extends State<MyShiftCard> with TickerProviderStateMixin
IconData? statusIcon; IconData? statusIcon;
// Fallback localization if keys missing // Fallback localization if keys missing
// Assuming t.staff_shifts.status.* exists as per previous file content
try { try {
if (status == 'confirmed') { if (status == 'confirmed') {
statusText = t.staff_shifts.status.confirmed; statusText = t.staff_shifts.status.confirmed;
@@ -137,9 +134,13 @@ class _MyShiftCardState extends State<MyShiftCard> with TickerProviderStateMixin
} }
return GestureDetector( return GestureDetector(
onTap: () => setState(() => _isExpanded = !_isExpanded), onTap: () {
child: AnimatedContainer( Modular.to.pushNamed(
duration: const Duration(milliseconds: 300), StaffPaths.shiftDetails(widget.shift.id),
arguments: widget.shift,
);
},
child: Container(
margin: const EdgeInsets.only(bottom: UiConstants.space3), margin: const EdgeInsets.only(bottom: UiConstants.space3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.white, color: UiColors.white,
@@ -153,582 +154,246 @@ class _MyShiftCardState extends State<MyShiftCard> with TickerProviderStateMixin
), ),
], ],
), ),
child: Column( child: Padding(
children: [ padding: const EdgeInsets.all(UiConstants.space4),
// Collapsed Content child: Column(
Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.all(UiConstants.space4), children: [
child: Column( // Status Badge
crossAxisAlignment: CrossAxisAlignment.start, if (statusText.isNotEmpty)
children: [ Padding(
// Status Badge padding: const EdgeInsets.only(bottom: UiConstants.space2),
if (statusText.isNotEmpty) child: Row(
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Row(
children: [
if (statusIcon != null)
Padding(
padding: const EdgeInsets.only(right: UiConstants.space2),
child: Icon(
statusIcon,
size: UiConstants.iconXs,
color: statusColor,
),
)
else
Container(
width: 8,
height: 8,
margin: const EdgeInsets.only(right: UiConstants.space2),
decoration: BoxDecoration(
color: statusBg,
shape: BoxShape.circle,
),
),
Text(
statusText,
style: UiTypography.footnote2b.copyWith(
color: statusColor,
letterSpacing: 0.5,
),
),
// Shift Type Badge for available/pending shifts
if (status == 'open' || status == 'pending') ...[
const SizedBox(width: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: 2,
),
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.1),
borderRadius: UiConstants.radiusSm,
),
child: Text(
_getShiftType(),
style: UiTypography.footnote2m.copyWith(
color: UiColors.primary,
),
),
),
],
],
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Logo if (statusIcon != null)
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
UiColors.primary.withValues(alpha: 0.09),
UiColors.primary.withValues(alpha: 0.03),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(
color: UiColors.primary.withValues(alpha: 0.09),
),
),
child: widget.shift.logoUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
child: Image.network(
widget.shift.logoUrl!,
fit: BoxFit.contain,
),
)
: const Center(
child: Icon(
UiIcons.briefcase,
color: UiColors.primary,
size: UiConstants.iconMd,
),
),
),
const SizedBox(width: UiConstants.space3),
// Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
widget.shift.title,
style: UiTypography.body2m.textPrimary,
overflow: TextOverflow.ellipsis,
),
Text(
widget.shift.clientName,
style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: UiConstants.space2),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"\$${estimatedTotal.toStringAsFixed(0)}",
style: UiTypography.title1m.textPrimary,
),
Text(
"\$${widget.shift.hourlyRate.toInt()}/hr · ${duration.toInt()}h",
style: UiTypography.footnote2r.textSecondary,
),
],
),
],
),
const SizedBox(height: UiConstants.space2),
// Date & Time - Multi-Day or Single Day
if (widget.shift.durationDays != null &&
widget.shift.durationDays! > 1) ...[
// Multi-Day Schedule Display
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.primary,
),
const SizedBox(width: UiConstants.space1),
Text(
t.staff_shifts.details.days(
days: widget.shift.durationDays!,
),
style: UiTypography.footnote2m.copyWith(
color: UiColors.primary,
),
),
],
),
const SizedBox(height: UiConstants.space1),
// Mock loop for demo purposes, as we don't have all schedule dates in the model
// In real app, we might need to fetch schedule or iterate if model changes
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} ${_formatTime(widget.shift.endTime)}',
style: UiTypography.footnote2r.copyWith(color: UiColors.primary),
),
),
if (widget.shift.durationDays! > 1)
Text(
'... +${widget.shift.durationDays! - 1} more days',
style: UiTypography.footnote2r.copyWith(color: UiColors.primary.withOpacity(0.7)),
)
],
),
] else ...[
// Single Day Display
Row(
children: [
const Icon(
UiIcons.calendar,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
_formatDate(widget.shift.date),
style: UiTypography.footnote1r.textSecondary,
),
const SizedBox(width: UiConstants.space3),
const Icon(
UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
"${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}",
style: UiTypography.footnote1r.textSecondary,
),
],
),
],
const SizedBox(height: UiConstants.space1),
// Location
Row(
children: [
const Icon(
UiIcons.mapPin,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Expanded(
child: Text(
widget.shift.locationAddress.isNotEmpty
? widget.shift.locationAddress
: widget.shift.location,
style: UiTypography.footnote1r.textSecondary,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
],
),
],
),
),
// Expanded Content
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: _isExpanded
? Column(
children: [
const Divider(height: 1, color: UiColors.border),
Padding( Padding(
padding: const EdgeInsets.all(UiConstants.space4), padding: const EdgeInsets.only(right: UiConstants.space2),
child: Column( child: Icon(
crossAxisAlignment: CrossAxisAlignment.start, statusIcon,
children: [ size: UiConstants.iconXs,
// Stats Row color: statusColor,
Row( ),
children: [ )
Expanded( else
child: _buildStatCard( Container(
UiIcons.dollar, width: 8,
"\$${estimatedTotal.toStringAsFixed(0)}", height: 8,
"Total", margin: const EdgeInsets.only(right: UiConstants.space2),
), decoration: BoxDecoration(
), color: statusBg,
const SizedBox(width: UiConstants.space3), shape: BoxShape.circle,
Expanded( ),
child: _buildStatCard( ),
UiIcons.dollar, Text(
"\$${widget.shift.hourlyRate}", statusText,
"Hourly Rate", style: UiTypography.footnote2b.copyWith(
), color: statusColor,
), letterSpacing: 0.5,
const SizedBox(width: UiConstants.space3), ),
Expanded( ),
child: _buildStatCard( // Shift Type Badge
UiIcons.clock, if (status == 'open' || status == 'pending') ...[
"${duration}", const SizedBox(width: UiConstants.space2),
"Hours", Container(
), padding: const EdgeInsets.symmetric(
), horizontal: UiConstants.space2,
], vertical: 2,
), ),
const SizedBox(height: UiConstants.space5), decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.1),
// In/Out Time borderRadius: UiConstants.radiusSm,
Row( ),
children: [ child: Text(
Expanded( _getShiftType(),
child: _buildTimeBox( style: UiTypography.footnote2m.copyWith(
"CLOCK IN TIME", color: UiColors.primary,
widget.shift.startTime, ),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _buildTimeBox(
"CLOCK OUT TIME",
widget.shift.endTime,
),
),
],
),
const SizedBox(height: UiConstants.space5),
// Location
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"LOCATION",
style: UiTypography.footnote2b.copyWith(
color: UiColors.textSecondary,
letterSpacing: 0.5),
),
const SizedBox(height: UiConstants.space2),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
widget.shift.location.isEmpty
? "TBD"
: widget.shift.location,
style: UiTypography.title1m.textPrimary,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: UiConstants.space3),
OutlinedButton.icon(
onPressed: () {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
widget.shift.locationAddress ??
widget.shift.location,
),
duration: const Duration(
seconds: 3,
),
),
);
},
icon: const Icon(
UiIcons.navigation,
size: UiConstants.iconXs,
),
label: const Text(
"Get direction",
),
style: OutlinedButton.styleFrom(
foregroundColor:
UiColors.textPrimary,
side: const BorderSide(
color: UiColors.border,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
),
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: 0,
),
minimumSize: const Size(0, 32),
),
),
],
),
const SizedBox(height: UiConstants.space3),
ShiftLocationMap(
shift: widget.shift,
height: 128,
borderRadius: UiConstants.radiusBase,
),
],
),
const SizedBox(height: UiConstants.space5),
// Additional Info
if (widget.shift.description != null) ...[
SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"ADDITIONAL INFO",
style: UiTypography.footnote2b.copyWith(
color: UiColors.textSecondary,
letterSpacing: 0.5),
),
const SizedBox(height: UiConstants.space2),
Text(
widget.shift.description!,
style: UiTypography.body2m.textPrimary,
),
],
),
),
const SizedBox(height: UiConstants.space5),
],
// Actions
if (!widget.historyMode)
Padding(
padding: const EdgeInsets.only(top: UiConstants.space2),
child: _buildActions(status),
),
],
), ),
), ),
], ],
) ],
: const SizedBox.shrink(), ),
), ),
],
),
),
);
}
Widget _buildActions(String? status) { Row(
if (status == 'confirmed') { crossAxisAlignment: CrossAxisAlignment.start,
return SizedBox( children: [
width: double.infinity, // Logo
height: 48, Container(
child: OutlinedButton.icon( width: 44,
onPressed: widget.onRequestSwap, height: 44,
icon: const Icon( decoration: BoxDecoration(
UiIcons.swap, gradient: LinearGradient(
size: UiConstants.iconSm, colors: [
), UiColors.primary.withValues(alpha: 0.09),
label: const Text("Request Swap"), UiColors.primary.withValues(alpha: 0.03),
style: OutlinedButton.styleFrom( ],
foregroundColor: UiColors.primary, begin: Alignment.topLeft,
side: const BorderSide( end: Alignment.bottomRight,
color: UiColors.primary, ),
), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
shape: RoundedRectangleBorder( border: Border.all(
borderRadius: BorderRadius.circular( color: UiColors.primary.withValues(alpha: 0.09),
UiConstants.radiusBase, ),
),
),
),
),
);
} else if (status == 'swap') {
return Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: UiColors.tagPending,
border: Border.all(
color: UiColors.textWarning,
),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(
UiIcons.swap,
size: UiConstants.iconSm,
color: UiColors.textWarning,
),
const SizedBox(width: UiConstants.space2),
Text(
"Swap Pending",
style: UiTypography.body2b.copyWith(
color: UiColors.textWarning,
),
),
],
),
);
} else {
// status == 'open' || status == 'pending' or others
return Column(
children: [
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: widget.onAccept,
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
foregroundColor: UiColors.white,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(UiConstants.radiusBase),
),
),
child: widget.onAccept == null
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)
) // Loading state if callback null? or just Text
: const Text(
"Book Shift",
style: TextStyle(
fontWeight: FontWeight.w600,
), ),
), child: widget.shift.logoUrl != null
), ? ClipRRect(
), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
], child: Image.network(
); widget.shift.logoUrl!,
} fit: BoxFit.contain,
} ),
)
: const Center(
child: Icon(
UiIcons.briefcase,
color: UiColors.primary,
size: UiConstants.iconMd,
),
),
),
const SizedBox(width: UiConstants.space3),
Widget _buildStatCard(IconData icon, String value, String label) { // Consensed Details
return Container( Expanded(
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), child: Column(
decoration: BoxDecoration( crossAxisAlignment: CrossAxisAlignment.start,
color: UiColors.background, children: [
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), Row(
border: Border.all(color: UiColors.border), mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: [
child: Column( Expanded(
children: [ child: Column(
Container( crossAxisAlignment:
width: 40, CrossAxisAlignment.start,
height: 40, children: [
decoration: const BoxDecoration( Text(
color: UiColors.white, widget.shift.title,
shape: BoxShape.circle, style: UiTypography.body2m.textPrimary,
), overflow: TextOverflow.ellipsis,
child: Icon(icon, size: 20, color: UiColors.textSecondary), ),
), Text(
const SizedBox(height: UiConstants.space2), widget.shift.clientName,
Text( style: UiTypography.body3r.textSecondary,
value, overflow: TextOverflow.ellipsis,
style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary, ),
), ],
Text( ),
label, ),
style: UiTypography.footnote2r.textSecondary, const SizedBox(width: UiConstants.space2),
), Column(
], crossAxisAlignment: CrossAxisAlignment.end,
), children: [
); Text(
} "\$${estimatedTotal.toStringAsFixed(0)}",
style: UiTypography.title1m.textPrimary,
),
Text(
"\$${widget.shift.hourlyRate.toInt()}/hr · ${duration.toInt()}h",
style: UiTypography.footnote2r.textSecondary,
),
],
),
],
),
const SizedBox(height: UiConstants.space2),
Widget _buildTimeBox(String label, String time) { // Date & Time
return Container( if (widget.shift.durationDays != null &&
padding: const EdgeInsets.all(UiConstants.space3), widget.shift.durationDays! > 1) ...[
decoration: BoxDecoration( Column(
color: UiColors.background, crossAxisAlignment: CrossAxisAlignment.start,
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), children: [
), Row(
child: Column( children: [
children: [ const Icon(
Text( UiIcons.clock,
label, size: UiConstants.iconXs,
style: UiTypography.footnote2b.copyWith( color: UiColors.primary,
color: UiColors.textSecondary, letterSpacing: 0.5), ),
const SizedBox(width: UiConstants.space1),
Text(
t.staff_shifts.details.days(
days: widget.shift.durationDays!,
),
style: UiTypography.footnote2m.copyWith(
color: UiColors.primary,
),
),
],
),
const SizedBox(height: UiConstants.space1),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} ${_formatTime(widget.shift.endTime)}',
style: UiTypography.footnote2r.copyWith(color: UiColors.primary),
),
),
if (widget.shift.durationDays! > 1)
Text(
'... +${widget.shift.durationDays! - 1} more days',
style: UiTypography.footnote2r.copyWith(color: UiColors.primary.withOpacity(0.7)),
)
],
),
] else ...[
Row(
children: [
const Icon(
UiIcons.calendar,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
_formatDate(widget.shift.date),
style: UiTypography.footnote1r.textSecondary,
),
const SizedBox(width: UiConstants.space3),
const Icon(
UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
"${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}",
style: UiTypography.footnote1r.textSecondary,
),
],
),
],
const SizedBox(height: UiConstants.space1),
// Location
Row(
children: [
const Icon(
UiIcons.mapPin,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Expanded(
child: Text(
widget.shift.locationAddress.isNotEmpty
? widget.shift.locationAddress
: widget.shift.location,
style: UiTypography.footnote1r.textSecondary,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
],
),
],
), ),
const SizedBox(height: UiConstants.space1), ),
Text(
_formatTime(time),
style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary,
),
],
), ),
); );
} }