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..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, ), ], ), @@ -579,40 +567,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 +662,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..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,14 +4,22 @@ 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 '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 @@ -19,12 +27,14 @@ class MyShiftCard extends StatefulWidget { } class _MyShiftCardState extends State { + 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 +75,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,36 +101,44 @@ 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 + 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); + Modular.to.pushNamed( + StaffPaths.shiftDetails(widget.shift.id), + arguments: widget.shift, + ); }, child: Container( margin: const EdgeInsets.only(bottom: UiConstants.space3), @@ -131,246 +154,245 @@ class _MyShiftCardState extends State { ), ], ), - 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: UiConstants.radiusMdValue, - height: UiConstants.radiusMdValue, - 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, + if (statusIcon != null) + Padding( + padding: const EdgeInsets.only(right: UiConstants.space2), + child: Icon( + statusIcon, + size: UiConstants.iconXs, + color: statusColor, ), - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all( - color: UiColors.primary.withValues(alpha: 0.09), + ) + else + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(right: UiConstants.space2), + decoration: BoxDecoration( + color: statusBg, + shape: BoxShape.circle, ), ), - 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, - ), - ), + Text( + statusText, + style: UiTypography.footnote2b.copyWith( + color: statusColor, + letterSpacing: 0.5, + ), ), - const SizedBox(width: UiConstants.space3), + // 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, + ), + ), + ), + ], + ], + ), + ), - // Details - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + 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), + + // Consensed Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, 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, - ), - ], + 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, ), - 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, - ), - ], + Text( + "\$${widget.shift.hourlyRate.toInt()}/hr · ${duration.toInt()}h", + style: UiTypography.footnote2r.textSecondary, ), ], ), - const SizedBox(height: UiConstants.space2), + ], + ), + 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, + // Date & Time + if (widget.shift.durationDays != null && + widget.shift.durationDays! > 1) ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( 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 Icon( + UiIcons.clock, + size: UiConstants.iconXs, + color: UiColors.primary, ), - const SizedBox(height: UiConstants.space1), + const SizedBox(width: UiConstants.space1), Text( - "Showing first schedule...", - style: UiTypography.footnote2r.copyWith( + t.staff_shifts.details.days( + days: widget.shift.durationDays!, + ), + style: UiTypography.footnote2m.copyWith( color: UiColors.primary, ), ), ], ), - ] else ...[ - // Single Day Display - Row( - children: [ - const Icon( - UiIcons.calendar, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, + 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), ), - const SizedBox(width: UiConstants.space1), + ), + if (widget.shift.durationDays! > 1) 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, - ), - ], + '... +${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), + ), + ], + 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, - ), - ), - ], + // 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, + ), ), ], ), - ), - ], + ], + ), ), ], ), - ), - ], + ], + ), ), ), ); 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, + ); + }, + ), ), ), ],