From 690d4f4213e0dfd6ef81948e5ccaa7cdd15806ca Mon Sep 17 00:00:00 2001 From: Suriya Date: Mon, 16 Feb 2026 15:57:27 +0530 Subject: [PATCH 1/2] feat(staff): Refactor Shift Cards & Integrate Google Maps Refactors MyShiftCard to match prototype design with expandable details, bold typography, and Google Static Maps integration. Updates AppConfig for API keys. --- apps/mobile/config.dev.json | 5 +- .../core/lib/src/config/app_config.dart | 3 + .../pages/shift_details_page.dart | 73 +-- .../presentation/widgets/my_shift_card.dart | 445 ++++++++++++++++-- .../widgets/shift_location_map.dart | 103 ++++ .../widgets/tabs/find_shifts_tab.dart | 10 + .../widgets/tabs/my_shifts_tab.dart | 12 +- 7 files changed, 544 insertions(+), 107 deletions(-) create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_location_map.dart diff --git a/apps/mobile/config.dev.json b/apps/mobile/config.dev.json index d2d5fa4c..214cb535 100644 --- a/apps/mobile/config.dev.json +++ b/apps/mobile/config.dev.json @@ -1,3 +1,4 @@ { - "GOOGLE_PLACES_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU" -} + "GOOGLE_PLACES_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU", + "GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0" +} \ No newline at end of file diff --git a/apps/mobile/packages/core/lib/src/config/app_config.dart b/apps/mobile/packages/core/lib/src/config/app_config.dart index ac1ccfc7..727638a4 100644 --- a/apps/mobile/packages/core/lib/src/config/app_config.dart +++ b/apps/mobile/packages/core/lib/src/config/app_config.dart @@ -6,4 +6,7 @@ class AppConfig { /// The Google Places API key used for address autocomplete functionality. static const String googlePlacesApiKey = String.fromEnvironment('GOOGLE_PLACES_API_KEY'); + + /// The Google Maps Static API key used for location preview images. + static const String googleMapsApiKey = String.fromEnvironment('GOOGLE_MAPS_API_KEY'); } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index 819293cc..6c83272b 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -579,40 +579,21 @@ class _ShiftDetailsPageState extends State { final i18n = Translations.of(context).staff_shifts.shift_details; if (status == 'confirmed') { - return Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () => _openCancelDialog(context), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.destructive, - foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - elevation: 0, - ), - child: Text(i18n.cancel_shift, style: UiTypography.body2b.white), + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Modular.to.toClockIn(), + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.success, + foregroundColor: UiColors.white, + padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), + elevation: 0, ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: ElevatedButton( - onPressed: () => Modular.to.toClockIn(), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.success, - foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - elevation: 0, - ), - child: Text(i18n.clock_in, style: UiTypography.body2b.white), - ), - ), - ], + child: Text(i18n.clock_in, style: UiTypography.body2b.white), + ), ); } @@ -693,32 +674,4 @@ class _ShiftDetailsPageState extends State { return const SizedBox(); } - void _openCancelDialog(BuildContext context) { - final i18n = Translations.of(context).staff_shifts.shift_details.cancel_dialog; - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: Text(i18n.title), - content: Text(i18n.message), - actions: [ - TextButton( - onPressed: () => Modular.to.pop(), - child: Text(Translations.of(context).common.cancel), - ), - TextButton( - onPressed: () { - Modular.to.pop(); - BlocProvider.of(context).add( - DeclineShiftDetailsEvent(widget.shiftId), - ); - }, - style: TextButton.styleFrom( - foregroundColor: UiColors.destructive, - ), - child: Text(Translations.of(context).common.ok), - ), - ], - ), - ); - } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart index d6ee5fa1..bbf5eb35 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart @@ -4,27 +4,39 @@ import 'package:intl/intl.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:design_system/design_system.dart'; import 'package:core_localization/core_localization.dart'; -import 'package:krow_core/core.dart'; +import 'shift_location_map.dart'; +import 'package:krow_core/core.dart'; // For modular navigation class MyShiftCard extends StatefulWidget { final Shift shift; + final bool historyMode; + final VoidCallback? onAccept; + final VoidCallback? onDecline; + final VoidCallback? onRequestSwap; const MyShiftCard({ super.key, required this.shift, + this.historyMode = false, + this.onAccept, + this.onDecline, + this.onRequestSwap, }); @override State createState() => _MyShiftCardState(); } -class _MyShiftCardState extends State { +class _MyShiftCardState extends State with TickerProviderStateMixin { + bool _isExpanded = false; + String _formatTime(String time) { if (time.isEmpty) return ''; try { final parts = time.split(':'); final hour = int.parse(parts[0]); final minute = int.parse(parts[1]); + // Date doesn't matter for time formatting final dt = DateTime(2022, 1, 1, hour, minute); return DateFormat('h:mm a').format(dt); } catch (e) { @@ -65,13 +77,18 @@ class _MyShiftCardState extends State { } String _getShiftType() { - if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) { - return t.staff_shifts.filter.long_term; + // Handling potential localization key availability + try { + if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) { + return t.staff_shifts.filter.long_term; + } + if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) { + return t.staff_shifts.filter.multi_day; + } + return t.staff_shifts.filter.one_day; + } catch (_) { + return "One Day"; } - if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) { - return t.staff_shifts.filter.multi_day; - } - return t.staff_shifts.filter.one_day; } @override @@ -86,38 +103,43 @@ class _MyShiftCardState extends State { String statusText = ''; IconData? statusIcon; - if (status == 'confirmed') { - statusText = t.staff_shifts.status.confirmed; - statusColor = UiColors.textLink; - statusBg = UiColors.primary; - } else if (status == 'checked_in') { - statusText = 'Checked in'; - statusColor = UiColors.textSuccess; - statusBg = UiColors.iconSuccess; - } else if (status == 'pending' || status == 'open') { - statusText = t.staff_shifts.status.act_now; - statusColor = UiColors.destructive; - statusBg = UiColors.destructive; - } else if (status == 'swap') { - statusText = t.staff_shifts.status.swap_requested; - statusColor = UiColors.textWarning; - statusBg = UiColors.textWarning; - statusIcon = UiIcons.swap; - } else if (status == 'completed') { - statusText = t.staff_shifts.status.completed; - statusColor = UiColors.textSuccess; - statusBg = UiColors.iconSuccess; - } else if (status == 'no_show') { - statusText = t.staff_shifts.status.no_show; - statusColor = UiColors.destructive; - statusBg = UiColors.destructive; + // Fallback localization if keys missing + // Assuming t.staff_shifts.status.* exists as per previous file content + try { + if (status == 'confirmed') { + statusText = t.staff_shifts.status.confirmed; + statusColor = UiColors.textLink; + statusBg = UiColors.primary; + } else if (status == 'checked_in') { + statusText = 'Checked in'; + statusColor = UiColors.textSuccess; + statusBg = UiColors.iconSuccess; + } else if (status == 'pending' || status == 'open') { + statusText = t.staff_shifts.status.act_now; + statusColor = UiColors.destructive; + statusBg = UiColors.destructive; + } else if (status == 'swap') { + statusText = t.staff_shifts.status.swap_requested; + statusColor = UiColors.textWarning; + statusBg = UiColors.textWarning; + statusIcon = UiIcons.swap; + } else if (status == 'completed') { + statusText = t.staff_shifts.status.completed; + statusColor = UiColors.textSuccess; + statusBg = UiColors.iconSuccess; + } else if (status == 'no_show') { + statusText = t.staff_shifts.status.no_show; + statusColor = UiColors.destructive; + statusBg = UiColors.destructive; + } + } catch (_) { + statusText = status?.toUpperCase() ?? ""; } return GestureDetector( - onTap: () { - Modular.to.pushShiftDetails(widget.shift); - }, - child: Container( + onTap: () => setState(() => _isExpanded = !_isExpanded), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), margin: const EdgeInsets.only(bottom: UiConstants.space3), decoration: BoxDecoration( color: UiColors.white, @@ -156,8 +178,8 @@ class _MyShiftCardState extends State { ) else Container( - width: UiConstants.radiusMdValue, - height: UiConstants.radiusMdValue, + width: 8, + height: 8, margin: const EdgeInsets.only(right: UiConstants.space2), decoration: BoxDecoration( color: statusBg, @@ -304,12 +326,20 @@ class _MyShiftCardState extends State { ], ), const SizedBox(height: UiConstants.space1), - Text( - "Showing first schedule...", - style: UiTypography.footnote2r.copyWith( - color: UiColors.primary, - ), + // 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 ...[ @@ -370,9 +400,336 @@ class _MyShiftCardState extends State { ], ), ), + + // Expanded Content + AnimatedSize( + duration: const Duration(milliseconds: 300), + child: _isExpanded + ? Column( + children: [ + const Divider(height: 1, color: UiColors.border), + Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Stats Row + Row( + children: [ + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + "Total", + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${widget.shift.hourlyRate}", + "Hourly Rate", + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: _buildStatCard( + UiIcons.clock, + "${duration}", + "Hours", + ), + ), + ], + ), + const SizedBox(height: UiConstants.space5), + + // In/Out Time + Row( + children: [ + Expanded( + child: _buildTimeBox( + "CLOCK IN TIME", + 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) { + if (status == 'confirmed') { + return SizedBox( + width: double.infinity, + height: 48, + child: OutlinedButton.icon( + onPressed: widget.onRequestSwap, + icon: const Icon( + UiIcons.swap, + size: UiConstants.iconSm, + ), + label: const Text("Request Swap"), + style: OutlinedButton.styleFrom( + foregroundColor: UiColors.primary, + side: const BorderSide( + color: UiColors.primary, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 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, + ), + ), + ), + ), + ], + ); + } + } + + Widget _buildStatCard(IconData icon, String value, String label) { + return Container( + padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.background, + borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), + border: Border.all(color: UiColors.border), + ), + child: Column( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: UiColors.white, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 20, color: UiColors.textSecondary), + ), + const SizedBox(height: UiConstants.space2), + Text( + value, + style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary, + ), + Text( + label, + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + ); + } + + Widget _buildTimeBox(String label, String time) { + return Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.background, + borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), + ), + child: Column( + children: [ + Text( + label, + style: UiTypography.footnote2b.copyWith( + color: UiColors.textSecondary, letterSpacing: 0.5), + ), + const SizedBox(height: UiConstants.space1), + Text( + _formatTime(time), + style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary, + ), + ], + ), + ); + } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_location_map.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_location_map.dart new file mode 100644 index 00000000..d5f8dc35 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_location_map.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:design_system/design_system.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:krow_core/core.dart'; // Import AppConfig from krow_core + +class ShiftLocationMap extends StatelessWidget { + final Shift shift; + final double height; + final double borderRadius; + + const ShiftLocationMap({ + super.key, + required this.shift, + this.height = 120, + this.borderRadius = 8, + }); + + @override + Widget build(BuildContext context) { + if (AppConfig.googleMapsApiKey.isEmpty) { + return _buildPlaceholder(context, "Config Map Key"); + } + + final String mapUrl = _generateStaticMapUrl(); + + return Container( + height: height, + width: double.infinity, + decoration: BoxDecoration( + color: UiColors.background, + borderRadius: BorderRadius.circular(borderRadius), + ), + clipBehavior: Clip.antiAlias, + child: Image.network( + mapUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _buildPlaceholder(context, "Map unavailable"); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ), + ); + } + + String _generateStaticMapUrl() { + // Base URL + const String baseUrl = "https://maps.googleapis.com/maps/api/staticmap"; + + // Parameters + String center; + if (shift.latitude != null && shift.longitude != null) { + center = "${shift.latitude},${shift.longitude}"; + } else { + center = Uri.encodeComponent(shift.locationAddress.isNotEmpty + ? shift.locationAddress + : shift.location); + } + + // Construct URL + // scale=2 for retina displays + return "$baseUrl?center=$center&zoom=15&size=600x300&maptype=roadmap&markers=color:red%7C$center&key=${AppConfig.googleMapsApiKey}&scale=2"; + } + + Widget _buildPlaceholder(BuildContext context, String message) { + return Container( + height: height, + width: double.infinity, + decoration: BoxDecoration( + color: UiColors.secondary, + borderRadius: BorderRadius.circular(borderRadius), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + UiIcons.mapPin, + size: 32, + color: UiColors.iconSecondary, + ), + if (message.isNotEmpty) ...[ + const SizedBox(height: UiConstants.space2), + Text( + message, + style: UiTypography.footnote2r.textSecondary, + ), + ], + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart index 6422c312..95d1f7db 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -2,6 +2,8 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../blocs/shifts/shifts_bloc.dart'; import '../my_shift_card.dart'; import '../shared/empty_state_view.dart'; @@ -171,6 +173,14 @@ class _FindShiftsTabState extends State { padding: const EdgeInsets.only(bottom: UiConstants.space3), child: MyShiftCard( shift: shift, + onAccept: () { + context.read().add(AcceptShiftEvent(shift.id)); + UiSnackbar.show( + context, + message: "Shift application submitted!", // Todo: Localization + type: UiSnackbarType.success, + ); + }, ), ), ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart index e17654e3..51472ec9 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart @@ -382,7 +382,17 @@ class _MyShiftsTabState extends State { ...visibleMyShifts.map( (shift) => Padding( padding: const EdgeInsets.only(bottom: UiConstants.space3), - child: MyShiftCard(shift: shift), + child: MyShiftCard( + shift: shift, + onDecline: () => _declineShift(shift.id), + onRequestSwap: () { + UiSnackbar.show( + context, + message: "Swap functionality coming soon!", // Todo: Localization + type: UiSnackbarType.message, + ); + }, + ), ), ), ], From 40fa4ebdfa39a8b61245d0ef28bee4094ea84323 Mon Sep 17 00:00:00 2001 From: Suriya Date: Mon, 16 Feb 2026 20:28:43 +0530 Subject: [PATCH 2/2] Refactor: Move detailed shift UI from card to ShiftDetailsPage --- .../pages/shift_details_page.dart | 214 +++-- .../presentation/widgets/my_shift_card.dart | 811 +++++------------- 2 files changed, 339 insertions(+), 686 deletions(-) diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index 6c83272b..48cca943 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -1,5 +1,5 @@ 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_bloc/flutter_bloc.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_event.dart'; import '../blocs/shift_details/shift_details_state.dart'; +import '../widgets/shift_location_map.dart'; class ShiftDetailsPage extends StatefulWidget { final String shiftId; @@ -65,10 +66,10 @@ class _ShiftDetailsPageState extends State { Widget _buildStatCard(IconData icon, String value, String label) { return Container( - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), decoration: BoxDecoration( color: UiColors.background, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), + borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), border: Border.all(color: UiColors.border), ), child: Column( @@ -80,12 +81,12 @@ class _ShiftDetailsPageState extends State { color: UiColors.white, shape: BoxShape.circle, ), - child: Icon(icon, size: 20, color: UiColors.iconSecondary), + child: Icon(icon, size: 20, color: UiColors.textSecondary), ), const SizedBox(height: UiConstants.space2), Text( value, - style: UiTypography.title1m.textPrimary, + style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary, ), Text( label, @@ -98,21 +99,22 @@ class _ShiftDetailsPageState extends State { Widget _buildTimeBox(String label, String time) { return Container( - padding: const EdgeInsets.all(UiConstants.space4), + padding: const EdgeInsets.all(UiConstants.space3), decoration: BoxDecoration( color: UiColors.background, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), + borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), ), child: Column( children: [ Text( label, - style: UiTypography.titleUppercase4b.textSecondary, + style: UiTypography.footnote2b.copyWith( + color: UiColors.textSecondary, letterSpacing: 0.5), ), const SizedBox(height: UiConstants.space1), Text( _formatTime(time), - style: UiTypography.headline2m.textPrimary, + style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary, ), ], ), @@ -267,45 +269,49 @@ class _ShiftDetailsPageState extends State { ), const SizedBox(height: UiConstants.space6), - // Worker Capacity / Open Slots - if ((displayShift.requiredSlots ?? 0) > 0) - Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.success.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(UiConstants.radiusBase), + // Stats Row (New) + Row( + children: [ + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + "Total", + ), ), - child: Row( - children: [ - const Icon( - UiIcons.users, - size: 16, - color: UiColors.success, - ), - const SizedBox(width: UiConstants.space2), - Text( - i18n.slots_remaining(count: openSlots), - style: UiTypography.footnote1m.textSuccess, - ), - ], + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${displayShift.hourlyRate.toStringAsFixed(0)}", + "Hourly Rate", + ), ), - ), - + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildStatCard( + UiIcons.clock, + "${duration.toStringAsFixed(1)}", + "Hours", + ), + ), + ], + ), const SizedBox(height: UiConstants.space6), - // Time Section + // Time Section (New) Row( children: [ Expanded( child: _buildTimeBox( - i18n.start_time, + "CLOCK IN TIME", displayShift.startTime, ), ), const SizedBox(width: UiConstants.space4), Expanded( child: _buildTimeBox( - i18n.end_time, + "CLOCK OUT TIME", displayShift.endTime, ), ), @@ -313,97 +319,79 @@ class _ShiftDetailsPageState extends State { ), 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( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - i18n.location, + "LOCATION", style: UiTypography.titleUppercase4b.textSecondary, ), const SizedBox(height: UiConstants.space3), - Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: UiColors.border), - ), - child: Column( - children: [ - Row( - children: [ - const Icon( - UiIcons.mapPin, - color: UiColors.primary, - size: 20, - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - displayShift.location, - style: UiTypography.body2b.textPrimary, - ), - Text( - displayShift.locationAddress, - style: UiTypography.body3r.textSecondary, - ), - ], + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + displayShift.location.isEmpty + ? "TBD" + : displayShift.location, + style: UiTypography.title1m.textPrimary, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: UiConstants.space3), + OutlinedButton.icon( + onPressed: () { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + displayShift!.locationAddress.isNotEmpty + ? displayShift!.locationAddress + : displayShift!.location, + ), + duration: const Duration( + seconds: 3, ), ), - ], + ); + }, + icon: const Icon( + UiIcons.navigation, + size: UiConstants.iconXs, ), - const SizedBox(height: UiConstants.space4), - const Divider(), - 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, - ), + 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: displayShift, + height: 160, + borderRadius: UiConstants.radiusBase, ), ], ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart index bbf5eb35..86352524 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart @@ -4,7 +4,6 @@ import 'package:intl/intl.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:design_system/design_system.dart'; import 'package:core_localization/core_localization.dart'; -import 'shift_location_map.dart'; import 'package:krow_core/core.dart'; // For modular navigation class MyShiftCard extends StatefulWidget { @@ -27,8 +26,7 @@ class MyShiftCard extends StatefulWidget { State createState() => _MyShiftCardState(); } -class _MyShiftCardState extends State with TickerProviderStateMixin { - bool _isExpanded = false; +class _MyShiftCardState extends State { String _formatTime(String time) { if (time.isEmpty) return ''; @@ -104,7 +102,6 @@ class _MyShiftCardState extends State with TickerProviderStateMixin IconData? statusIcon; // Fallback localization if keys missing - // Assuming t.staff_shifts.status.* exists as per previous file content try { if (status == 'confirmed') { statusText = t.staff_shifts.status.confirmed; @@ -137,9 +134,13 @@ class _MyShiftCardState extends State with TickerProviderStateMixin } return GestureDetector( - onTap: () => setState(() => _isExpanded = !_isExpanded), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), + onTap: () { + Modular.to.pushNamed( + StaffPaths.shiftDetails(widget.shift.id), + arguments: widget.shift, + ); + }, + child: Container( margin: const EdgeInsets.only(bottom: UiConstants.space3), decoration: BoxDecoration( color: UiColors.white, @@ -153,582 +154,246 @@ class _MyShiftCardState extends State with TickerProviderStateMixin ), ], ), - child: Column( - children: [ - // Collapsed Content - Padding( - padding: const EdgeInsets.all(UiConstants.space4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Status Badge - if (statusText.isNotEmpty) - 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, + child: Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status Badge + if (statusText.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space2), + child: Row( children: [ - // Logo - 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), + if (statusIcon != null) Padding( - padding: const EdgeInsets.all(UiConstants.space4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Stats Row - Row( - children: [ - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${estimatedTotal.toStringAsFixed(0)}", - "Total", - ), - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${widget.shift.hourlyRate}", - "Hourly Rate", - ), - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: _buildStatCard( - UiIcons.clock, - "${duration}", - "Hours", - ), - ), - ], - ), - const SizedBox(height: UiConstants.space5), - - // In/Out Time - Row( - children: [ - Expanded( - child: _buildTimeBox( - "CLOCK IN TIME", - 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), - ), - ], + 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 + 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, + ), ), ), ], - ) - : const SizedBox.shrink(), - ), - ], - ), - ), - ); - } + ], + ), + ), - Widget _buildActions(String? status) { - if (status == 'confirmed') { - return SizedBox( - width: double.infinity, - height: 48, - child: OutlinedButton.icon( - onPressed: widget.onRequestSwap, - icon: const Icon( - UiIcons.swap, - size: UiConstants.iconSm, - ), - label: const Text("Request Swap"), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.primary, - side: const BorderSide( - color: UiColors.primary, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 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, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Logo + 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), - Widget _buildStatCard(IconData icon, String value, String label) { - return Container( - padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.background, - borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), - border: Border.all(color: UiColors.border), - ), - child: Column( - children: [ - Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: UiColors.white, - shape: BoxShape.circle, - ), - child: Icon(icon, size: 20, color: UiColors.textSecondary), - ), - const SizedBox(height: UiConstants.space2), - Text( - value, - style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary, - ), - Text( - label, - style: UiTypography.footnote2r.textSecondary, - ), - ], - ), - ); - } + // Consensed 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), - Widget _buildTimeBox(String label, String time) { - return Container( - padding: const EdgeInsets.all(UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.background, - borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), - ), - child: Column( - children: [ - Text( - label, - style: UiTypography.footnote2b.copyWith( - color: UiColors.textSecondary, letterSpacing: 0.5), + // Date & Time + if (widget.shift.durationDays != null && + widget.shift.durationDays! > 1) ...[ + 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), + 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, - ), - ], + ), ), ); }