diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart index 4998c45b..2c6081ed 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart @@ -24,6 +24,8 @@ class Shift extends Equatable { final double? longitude; final String? status; final int? durationDays; // For multi-day shifts + final int? requiredSlots; + final int? filledSlots; const Shift({ required this.id, @@ -49,6 +51,8 @@ class Shift extends Equatable { this.longitude, this.status, this.durationDays, + this.requiredSlots, + this.filledSlots, }); @override @@ -76,6 +80,8 @@ class Shift extends Equatable { longitude, status, durationDays, + requiredSlots, + filledSlots, ]; } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index 3d40e268..1b604078 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -127,6 +127,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { status: _mapStatus(status), description: shift.description, durationDays: shift.durationDays, + requiredSlots: shift.requiredSlots, + filledSlots: shift.filledSlots, )); } } @@ -182,6 +184,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { status: s.status?.stringValue.toLowerCase() ?? 'open', description: s.description, durationDays: s.durationDays, + requiredSlots: null, // Basic list doesn't fetch detailed role stats yet + filledSlots: null, )); } @@ -210,6 +214,20 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { final s = result.data.shift; if (s == null) return null; + int? required; + int? filled; + try { + final rolesRes = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute(); + if (rolesRes.data.shiftRoles.isNotEmpty) { + required = 0; + filled = 0; + for(var r in rolesRes.data.shiftRoles) { + required = (required ?? 0) + r.count; + filled = (filled ?? 0) + (r.assigned ?? 0); + } + } + } catch (_) {} + final startDt = _toDateTime(s.startTime); final endDt = _toDateTime(s.endTime); final createdDt = _toDateTime(s.createdAt); @@ -229,6 +247,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { status: s.status?.stringValue ?? 'OPEN', description: s.description, durationDays: s.durationDays, + requiredSlots: required, + filledSlots: filled, ); } catch (e) { return null; 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 7ade6d31..14889f20 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 @@ -3,22 +3,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic -import 'package:intl/intl.dart'; +import 'package:intl/intl.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 '../styles/shifts_styles.dart'; -import '../widgets/my_shift_card.dart'; class ShiftDetailsPage extends StatelessWidget { final String shiftId; final Shift? shift; - const ShiftDetailsPage({ - super.key, - required this.shiftId, - this.shift, - }); + const ShiftDetailsPage({super.key, required this.shiftId, this.shift}); String _formatTime(String time) { if (time.isEmpty) return ''; @@ -33,6 +27,16 @@ class ShiftDetailsPage extends StatelessWidget { } } + String _formatDate(String dateStr) { + if (dateStr.isEmpty) return ''; + try { + final date = DateTime.parse(dateStr); + return DateFormat('EEEE, MMMM d, y').format(date); + } catch (e) { + return dateStr; + } + } + double _calculateDuration(Shift shift) { if (shift.startTime.isEmpty || shift.endTime.isEmpty) { return 0; @@ -70,9 +74,7 @@ class ShiftDetailsPage extends StatelessWidget { const SizedBox(height: 8), Text( value, - style: UiTypography.title1m.copyWith( - color: UiColors.textPrimary, - ), + style: UiTypography.title1m.copyWith(color: UiColors.textPrimary), ), Text( label, @@ -118,9 +120,9 @@ class ShiftDetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => Modular.get() - ..add(LoadShiftDetailsEvent(shiftId)), + return BlocProvider( + create: (_) => + Modular.get()..add(LoadShiftDetailsEvent(shiftId)), child: BlocListener( listener: (context, state) { if (state is ShiftActionSuccess) { @@ -163,213 +165,322 @@ class ShiftDetailsPage extends StatelessWidget { final duration = _calculateDuration(displayShift); final estimatedTotal = (displayShift.hourlyRate) * duration; + final openSlots = + (displayShift.requiredSlots ?? 0) - + (displayShift.filledSlots ?? 0); return Scaffold( - backgroundColor: AppColors.krowBackground, - appBar: AppBar( - title: const Text("Shift Details"), - backgroundColor: Colors.white, - foregroundColor: AppColors.krowCharcoal, - elevation: 0.5, + appBar: UiAppBar( + title: displayShift.title, + showBackButton: true, + centerTitle: false, ), - body: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - MyShiftCard( - shift: displayShift, - // No direct actions on the card, handled by page buttons - ), - const SizedBox(height: 24), - - // Stats Row - Row( - children: [ - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${estimatedTotal.toStringAsFixed(0)}", - "Total", - ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Vendor Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "VENDOR", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${displayShift.hourlyRate.toInt()}", - "Hourly Rate", - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - UiIcons.clock, - "${duration.toInt()}", - "Hours", - ), - ), - ], - ), - const SizedBox(height: 24), - - // In/Out Time - Row( - children: [ - Expanded( - child: _buildTimeBox( - "CLOCK IN TIME", - displayShift.startTime, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildTimeBox( - "CLOCK OUT TIME", - displayShift.endTime, - ), - ), - ], - ), - const SizedBox(height: 24), - - // Location - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "LOCATION", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - displayShift.location.isEmpty - ? "TBD" - : displayShift.location, - style: UiTypography.title1m.copyWith( - color: UiColors.textPrimary, - ), - ), - OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(displayShift!.locationAddress), - duration: const Duration(seconds: 3), + ), + const SizedBox(height: 8), + Row( + children: [ + Container( + width: 24, + height: 24, + child: displayShift.logoUrl != null + ? ClipRRect( + borderRadius: BorderRadius.circular( + 6, + ), + child: Image.network( + displayShift.logoUrl!, + fit: BoxFit.cover, + ), + ) + : const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: 20, + ), ), - ); - }, - icon: const Icon(UiIcons.navigation, size: 14), - label: const Text( - "Get direction", - style: TextStyle(fontSize: 12), - ), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.textPrimary, - side: const BorderSide(color: UiColors.border), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), - minimumSize: const Size(0, 32), - ), - ), - ], - ), - const SizedBox(height: 12), - Container( - height: 128, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), ), - child: const Center( - child: Icon( - UiIcons.mapPin, - color: UiColors.iconSecondary, - size: 32, + const SizedBox(width: 8), + Text( + displayShift.clientName, + style: UiTypography.headline5m.copyWith( + color: UiColors.textPrimary, ), ), - // Placeholder for Map - ), - ], - ), - const SizedBox(height: 24), + ], + ), + ], + ), + const SizedBox(height: 24), - // Additional Info - if (displayShift.description != null) ...[ - SizedBox( - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "ADDITIONAL INFO", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), + // Date Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "SHIFT DATE", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 20, + color: UiColors.primary, + ), + const SizedBox(width: 8), + Text( + _formatDate(displayShift.date), + style: UiTypography.headline5m.copyWith( + color: UiColors.textPrimary, ), - const SizedBox(height: 8), - Text( - displayShift.description!, - style: UiTypography.body2m.copyWith( - color: UiColors.textPrimary, - ), + ), + ], + ), + ], + ), + const SizedBox(height: 24), + + // Worker Capacity / Open Slots + if ((displayShift.requiredSlots ?? 0) > 0) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF0FDF4), // green-50 + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFBBF7D0), + ), // green-200 + ), + child: Row( + children: [ + const Icon( + Icons.people_alt_outlined, + size: 20, + color: Color(0xFF15803D), + ), // green-700, using Material Icon as generic fallback + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "$openSlots spots remaining", + style: UiTypography.body2b.copyWith( + color: const Color(0xFF15803D), + ), + ), + Text( + "${displayShift.filledSlots ?? 0} filled out of ${displayShift.requiredSlots}", + style: UiTypography.body3r.copyWith( + color: const Color(0xFF166534), + ), + ), + ], ), - ], + ), + SizedBox( + width: 60, + child: LinearProgressIndicator( + value: (displayShift.requiredSlots! > 0) + ? (displayShift.filledSlots ?? 0) / + displayShift.requiredSlots! + : 0, + backgroundColor: Colors.white, + color: const Color(0xFF15803D), + minHeight: 6, + borderRadius: BorderRadius.circular(3), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Stats Grid + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + crossAxisSpacing: 12, + childAspectRatio: 0.85, + children: [ + _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + "Total Pay", + ), + _buildStatCard( + UiIcons.dollar, + "\$${displayShift.hourlyRate.toInt()}", + "Per Hour", + ), + _buildStatCard( + UiIcons.clock, + "${duration.toInt()}h", + "Duration", + ), + ], + ), + const SizedBox(height: 24), + + // Shift Timing + Row( + children: [ + Expanded( + child: _buildTimeBox( + "START TIME", + displayShift.startTime, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildTimeBox( + "END TIME", + displayShift.endTime, ), ), ], + ), + const SizedBox(height: 24), + + // Location + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "LOCATION", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + displayShift.location.isEmpty + ? "TBD" + : displayShift.location, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + Text( + displayShift.location.isEmpty + ? "TBD" + : displayShift.locationAddress, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 24), + + // Additional Info + if (displayShift.description != null) ...[ + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "ADDITIONAL INFO", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Text( + displayShift.description!, + style: UiTypography.body2m.copyWith( + color: UiColors.textPrimary, + ), + ), + ], + ), + ), ], - ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => + _declineShift(context, displayShift!.id), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + side: const BorderSide( + color: Color(0xFFEF4444), + ), + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + child: const Text("Decline"), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () => + _bookShift(context, displayShift!.id), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF10B981), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + child: const Text("Book Shift"), + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).padding.bottom + 10, + ), + ], ), ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => _declineShift(context, displayShift!.id), - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFFEF4444), - side: const BorderSide(color: Color(0xFFEF4444)), - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: const Text("Decline"), - ), - ), - const SizedBox(width: 16), - Expanded( - child: ElevatedButton( - onPressed: () => _bookShift(context, displayShift!.id), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF10B981), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: const Text("Book Shift"), - ), - ), - ], - ), - SizedBox(height: MediaQuery.of(context).padding.bottom + 10), - ], - ), + ), + ], ), ); }, @@ -392,7 +503,9 @@ class ShiftDetailsPage extends StatelessWidget { TextButton( onPressed: () { Navigator.of(ctx).pop(); - BlocProvider.of(context).add(BookShiftDetailsEvent(id)); + BlocProvider.of( + context, + ).add(BookShiftDetailsEvent(id)); }, style: TextButton.styleFrom( foregroundColor: const Color(0xFF10B981), @@ -410,7 +523,8 @@ class ShiftDetailsPage extends StatelessWidget { builder: (ctx) => AlertDialog( title: const Text('Decline Shift'), content: const Text( - 'Are you sure you want to decline this shift? It will be hidden from your available jobs.'), + 'Are you sure you want to decline this shift? It will be hidden from your available jobs.', + ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), @@ -419,7 +533,9 @@ class ShiftDetailsPage extends StatelessWidget { TextButton( onPressed: () { Navigator.of(ctx).pop(); - BlocProvider.of(context).add(DeclineShiftDetailsEvent(id)); + BlocProvider.of( + context, + ).add(DeclineShiftDetailsEvent(id)); }, style: TextButton.styleFrom( foregroundColor: const Color(0xFFEF4444),