From e6b512ee84679e93a85ffb44c042dcfc9a90c0b6 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 12:58:05 -0500 Subject: [PATCH 01/12] feat: Add Headline 1 Bold style and refactor ShiftDetailsPage and FindShiftsTab layout --- .../design_system/lib/src/ui_typography.dart | 8 + .../src/presentation/widgets/shift_card.dart | 7 - .../pages/shift_details_page.dart | 223 +++++++++--------- .../src/presentation/pages/shifts_page.dart | 3 - .../presentation/styles/shifts_styles.dart | 13 - .../widgets/shared/empty_state_view.dart | 1 - .../widgets/tabs/find_shifts_tab.dart | 40 ++-- .../features/staff/shifts/pubspec.yaml | 2 + 8 files changed, 151 insertions(+), 146 deletions(-) delete mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart diff --git a/apps/mobile/packages/design_system/lib/src/ui_typography.dart b/apps/mobile/packages/design_system/lib/src/ui_typography.dart index 6d33312b..b2224b11 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_typography.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_typography.dart @@ -173,6 +173,14 @@ class UiTypography { color: UiColors.textPrimary, ); + /// Headline 1 Bold - Font: Instrument Sans, Size: 26, Height: 1.5 (#121826) + static final TextStyle headline1b = _primaryBase.copyWith( + fontWeight: FontWeight.w600, + fontSize: 26, + height: 1.5, + color: UiColors.textPrimary, + ); + /// Headline 2 Medium - Font: Instrument Sans, Size: 20, Height: 1.5 (#121826) static final TextStyle headline2m = _primaryBase.copyWith( fontWeight: FontWeight.w500, diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart index f8bf4992..f35d97ae 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart @@ -84,13 +84,6 @@ class _ShiftCardState extends State { color: UiColors.white, borderRadius: BorderRadius.circular(UiConstants.radiusBase), border: Border.all(color: UiColors.border), - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], ), child: Row( children: [ 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 48cca943..dc408d94 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 @@ -14,9 +14,13 @@ import '../widgets/shift_location_map.dart'; class ShiftDetailsPage extends StatefulWidget { final String shiftId; - final Shift? shift; + final Shift shift; - const ShiftDetailsPage({super.key, required this.shiftId, this.shift}); + const ShiftDetailsPage({ + super.key, + required this.shiftId, + required this.shift, + }); @override State createState() => _ShiftDetailsPageState(); @@ -86,12 +90,11 @@ class _ShiftDetailsPageState extends State { const SizedBox(height: UiConstants.space2), Text( value, - style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary, - ), - Text( - label, - style: UiTypography.footnote2r.textSecondary, + style: UiTypography.title1m + .copyWith(fontWeight: FontWeight.w700) + .textPrimary, ), + Text(label, style: UiTypography.footnote2r.textSecondary), ], ), ); @@ -109,12 +112,16 @@ class _ShiftDetailsPageState extends State { Text( label, style: UiTypography.footnote2b.copyWith( - color: UiColors.textSecondary, letterSpacing: 0.5), + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), ), const SizedBox(height: UiConstants.space1), Text( _formatTime(time), - style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary, + style: UiTypography.title1m + .copyWith(fontWeight: FontWeight.w700) + .textPrimary, ), ], ), @@ -124,14 +131,10 @@ class _ShiftDetailsPageState extends State { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => - Modular.get() - ..add( - LoadShiftDetailsEvent( - widget.shiftId, - roleId: widget.shift?.roleId, - ), - ), + create: (_) => Modular.get() + ..add( + LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift?.roleId), + ), child: BlocListener( listener: (context, state) { if (state is ShiftActionSuccess || state is ShiftDetailsError) { @@ -164,30 +167,16 @@ class _ShiftDetailsPageState extends State { ); } - Shift? displayShift; - if (state is ShiftDetailsLoaded) { - displayShift = state.shift; - } else { - displayShift = widget.shift; - } + Shift? displayShift = widget.shift; final i18n = Translations.of(context).staff_shifts.shift_details; - if (displayShift == null) { - return Scaffold( - body: Center(child: Text(Translations.of(context).staff_shifts.list.no_shifts)), - ); - } final duration = _calculateDuration(displayShift); final estimatedTotal = displayShift.totalValue ?? (displayShift.hourlyRate * duration); - final openSlots = - (displayShift.requiredSlots ?? 0) - - (displayShift.filledSlots ?? 0); return Scaffold( appBar: UiAppBar( - title: displayShift.title, centerTitle: false, onLeadingPressed: () => Modular.to.toShifts(), ), @@ -199,44 +188,49 @@ class _ShiftDetailsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Vendor Section - Column( + // Role & Client Section + Row( crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space4, children: [ - Text( - i18n.vendor, - style: UiTypography.titleUppercase4b.textSecondary, + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: UiColors.background, + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, + ), + border: Border.all(color: UiColors.border), + ), + child: const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: 24, + ), + ), ), - const SizedBox(height: UiConstants.space2), - Row( - children: [ - SizedBox( - width: 24, - height: 24, - child: displayShift.logoUrl != null - ? ClipRRect( - borderRadius: BorderRadius.circular( - UiConstants.radiusMdValue, - ), - child: Image.network( - displayShift.logoUrl!, - fit: BoxFit.cover, - ), - ) - : const Center( - child: Icon( - UiIcons.briefcase, - color: UiColors.primary, - size: 20, - ), - ), - ), - const SizedBox(width: UiConstants.space2), - Text( - displayShift.clientName, - style: UiTypography.headline5m.textPrimary, - ), - ], + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + displayShift.title, + style: + UiTypography.headline1b.textPrimary, + ), + Text( + displayShift.clientName, + style: UiTypography.body1m.textSecondary, + ), + Text( + displayShift.locationAddress, + style: UiTypography.body2r.textSecondary, + ), + ], + ), ), ], ), @@ -248,7 +242,8 @@ class _ShiftDetailsPageState extends State { children: [ Text( i18n.shift_date, - style: UiTypography.titleUppercase4b.textSecondary, + style: + UiTypography.titleUppercase4b.textSecondary, ), const SizedBox(height: UiConstants.space2), Row( @@ -276,7 +271,7 @@ class _ShiftDetailsPageState extends State { child: _buildStatCard( UiIcons.dollar, "\$${estimatedTotal.toStringAsFixed(0)}", - "Total", + "Total", ), ), const SizedBox(width: UiConstants.space4), @@ -291,7 +286,7 @@ class _ShiftDetailsPageState extends State { Expanded( child: _buildStatCard( UiIcons.clock, - "${duration.toStringAsFixed(1)}", + "${duration.toStringAsFixed(1)}", "Hours", ), ), @@ -319,14 +314,14 @@ class _ShiftDetailsPageState extends State { ), const SizedBox(height: UiConstants.space6), - // Location Section (New with Map) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "LOCATION", - style: UiTypography.titleUppercase4b.textSecondary, + style: + UiTypography.titleUppercase4b.textSecondary, ), const SizedBox(height: UiConstants.space3), Row( @@ -350,13 +345,13 @@ class _ShiftDetailsPageState extends State { ).showSnackBar( SnackBar( content: Text( - displayShift!.locationAddress.isNotEmpty - ? displayShift!.locationAddress - : displayShift!.location, - ), - duration: const Duration( - seconds: 3, + displayShift! + .locationAddress + .isNotEmpty + ? displayShift!.locationAddress + : displayShift!.location, ), + duration: const Duration(seconds: 3), ), ); }, @@ -364,12 +359,9 @@ class _ShiftDetailsPageState extends State { UiIcons.navigation, size: UiConstants.iconXs, ), - label: const Text( - "Get direction", - ), + label: const Text("Get direction"), style: OutlinedButton.styleFrom( - foregroundColor: - UiColors.textPrimary, + foregroundColor: UiColors.textPrimary, side: const BorderSide( color: UiColors.border, ), @@ -401,7 +393,8 @@ class _ShiftDetailsPageState extends State { if ((displayShift.description ?? '').isNotEmpty) ...[ Text( i18n.job_description, - style: UiTypography.titleUppercase4b.textSecondary, + style: + UiTypography.titleUppercase4b.textSecondary, ), const SizedBox(height: UiConstants.space2), Text( @@ -420,7 +413,8 @@ class _ShiftDetailsPageState extends State { UiConstants.space5, UiConstants.space4, UiConstants.space5, - MediaQuery.of(context).padding.bottom + UiConstants.space4, + MediaQuery.of(context).padding.bottom + + UiConstants.space4, ), decoration: BoxDecoration( color: UiColors.white, @@ -444,11 +438,10 @@ class _ShiftDetailsPageState extends State { ); } - void _bookShift( - BuildContext context, - Shift shift, - ) { - final i18n = Translations.of(context).staff_shifts.shift_details.book_dialog; + void _bookShift(BuildContext context, Shift shift) { + final i18n = Translations.of( + context, + ).staff_shifts.shift_details.book_dialog; showDialog( context: context, builder: (ctx) => AlertDialog( @@ -471,10 +464,10 @@ class _ShiftDetailsPageState extends State { ), ); }, - style: TextButton.styleFrom( - foregroundColor: UiColors.success, + style: TextButton.styleFrom(foregroundColor: UiColors.success), + child: Text( + Translations.of(context).staff_shifts.shift_details.apply_now, ), - child: Text(Translations.of(context).staff_shifts.shift_details.apply_now), ), ], ), @@ -482,7 +475,9 @@ class _ShiftDetailsPageState extends State { } void _declineShift(BuildContext context, String id) { - final i18n = Translations.of(context).staff_shifts.shift_details.decline_dialog; + final i18n = Translations.of( + context, + ).staff_shifts.shift_details.decline_dialog; showDialog( context: context, builder: (ctx) => AlertDialog( @@ -499,10 +494,10 @@ class _ShiftDetailsPageState extends State { context, ).add(DeclineShiftDetailsEvent(id)); }, - style: TextButton.styleFrom( - foregroundColor: UiColors.destructive, + style: TextButton.styleFrom(foregroundColor: UiColors.destructive), + child: Text( + Translations.of(context).staff_shifts.shift_details.decline, ), - child: Text(Translations.of(context).staff_shifts.shift_details.decline), ), ], ), @@ -513,7 +508,9 @@ class _ShiftDetailsPageState extends State { if (_actionDialogOpen) return; _actionDialogOpen = true; _isApplying = true; - final i18n = Translations.of(context).staff_shifts.shift_details.applying_dialog; + final i18n = Translations.of( + context, + ).staff_shifts.shift_details.applying_dialog; showDialog( context: context, useRootNavigator: true, @@ -590,10 +587,14 @@ class _ShiftDetailsPageState extends State { children: [ Expanded( child: OutlinedButton( - onPressed: () => BlocProvider.of(context).add(DeclineShiftDetailsEvent(shift.id)), + onPressed: () => BlocProvider.of( + context, + ).add(DeclineShiftDetailsEvent(shift.id)), style: OutlinedButton.styleFrom( foregroundColor: UiColors.destructive, - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), side: const BorderSide(color: UiColors.destructive), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(UiConstants.radiusBase), @@ -605,11 +606,15 @@ class _ShiftDetailsPageState extends State { const SizedBox(width: UiConstants.space4), Expanded( child: ElevatedButton( - onPressed: () => BlocProvider.of(context).add(BookShiftDetailsEvent(shift.id, roleId: shift.roleId)), + onPressed: () => BlocProvider.of( + context, + ).add(BookShiftDetailsEvent(shift.id, roleId: shift.roleId)), style: ElevatedButton.styleFrom( backgroundColor: UiColors.primary, foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), @@ -630,13 +635,18 @@ class _ShiftDetailsPageState extends State { onPressed: () => _declineShift(context, shift.id), style: OutlinedButton.styleFrom( foregroundColor: UiColors.textSecondary, - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), side: const BorderSide(color: UiColors.border), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), ), - child: Text(i18n.decline, style: UiTypography.body2b.textSecondary), + child: Text( + i18n.decline, + style: UiTypography.body2b.textSecondary, + ), ), ), const SizedBox(width: UiConstants.space4), @@ -646,7 +656,9 @@ class _ShiftDetailsPageState extends State { style: ElevatedButton.styleFrom( backgroundColor: UiColors.primary, foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), @@ -661,5 +673,4 @@ class _ShiftDetailsPageState extends State { return const SizedBox(); } - } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart index 3f360efd..1b6e1592 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart @@ -29,7 +29,6 @@ class _ShiftsPageState extends State { super.initState(); _activeTab = widget.initialTab ?? 'myshifts'; _selectedDate = widget.selectedDate; - print('ShiftsPage init: initialTab=$_activeTab'); _prioritizeFind = widget.initialTab == 'find'; if (_prioritizeFind) { _bloc.add(LoadFindFirstEvent()); @@ -37,11 +36,9 @@ class _ShiftsPageState extends State { _bloc.add(LoadShiftsEvent()); } if (_activeTab == 'history') { - print('ShiftsPage init: loading history tab'); _bloc.add(LoadHistoryShiftsEvent()); } if (_activeTab == 'find') { - print('ShiftsPage init: entering find tab (not loaded yet)'); if (!_prioritizeFind) { _bloc.add(LoadAvailableShiftsEvent()); } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart deleted file mode 100644 index 0a9a3675..00000000 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:design_system/design_system.dart'; - -class AppColors { - static const Color krowBlue = UiColors.primary; - static const Color krowYellow = UiColors.accent; - static const Color krowCharcoal = UiColors.textPrimary; - static const Color krowMuted = UiColors.textSecondary; - static const Color krowBorder = UiColors.border; - static const Color krowBackground = UiColors.background; - static const Color white = UiColors.white; - static const Color black = UiColors.black; -} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart index 63569050..42b506cf 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart @@ -1,6 +1,5 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import '../../styles/shifts_styles.dart'; class EmptyStateView extends StatelessWidget { final IconData icon; 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 95d1f7db..bb426fd7 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 @@ -10,10 +10,7 @@ import '../shared/empty_state_view.dart'; class FindShiftsTab extends StatefulWidget { final List availableJobs; - const FindShiftsTab({ - super.key, - required this.availableJobs, - }); + const FindShiftsTab({super.key, required this.availableJobs}); @override State createState() => _FindShiftsTabState(); @@ -42,7 +39,9 @@ class _FindShiftsTabState extends State { child: Text( label, textAlign: TextAlign.center, - style: (isSelected ? UiTypography.footnote2m.white : UiTypography.footnote2m.textSecondary), + style: (isSelected + ? UiTypography.footnote2m.white + : UiTypography.footnote2m.textSecondary), ), ), ); @@ -86,13 +85,15 @@ class _FindShiftsTabState extends State { Expanded( child: Container( height: 48, - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + ), decoration: BoxDecoration( color: UiColors.background, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all( - color: UiColors.border, + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, ), + border: Border.all(color: UiColors.border), ), child: Row( children: [ @@ -123,10 +124,10 @@ class _FindShiftsTabState extends State { width: 48, decoration: BoxDecoration( color: UiColors.white, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all( - color: UiColors.border, + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, ), + border: Border.all(color: UiColors.border), ), child: const Icon( UiIcons.filter, @@ -164,20 +165,27 @@ class _FindShiftsTabState extends State { subtitle: "Check back later", ) : SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + ), child: Column( children: [ const SizedBox(height: UiConstants.space5), ...filteredJobs.map( (shift) => Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space3), + padding: const EdgeInsets.only( + bottom: UiConstants.space3, + ), child: MyShiftCard( shift: shift, onAccept: () { - context.read().add(AcceptShiftEvent(shift.id)); + context.read().add( + AcceptShiftEvent(shift.id), + ); UiSnackbar.show( context, - message: "Shift application submitted!", // Todo: Localization + message: + "Shift application submitted!", // Todo: Localization type: UiSnackbarType.success, ); }, diff --git a/apps/mobile/packages/features/staff/shifts/pubspec.yaml b/apps/mobile/packages/features/staff/shifts/pubspec.yaml index 99360d7a..4a6d7070 100644 --- a/apps/mobile/packages/features/staff/shifts/pubspec.yaml +++ b/apps/mobile/packages/features/staff/shifts/pubspec.yaml @@ -27,6 +27,8 @@ dependencies: path: ../../../data_connect core_localization: path: ../../../core_localization + firebase_auth: ^6.1.4 + firebase_data_connect: ^0.2.2+2 dev_dependencies: flutter_test: From 2a0b39926a4834d7f6e741228777981a496b4b01 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 13:09:10 -0500 Subject: [PATCH 02/12] refactor: Update UI theme and shift details layout for improved consistency --- .../design_system/lib/src/ui_theme.dart | 2 +- .../pages/shift_details_page.dart | 453 +++++++++--------- 2 files changed, 231 insertions(+), 224 deletions(-) diff --git a/apps/mobile/packages/design_system/lib/src/ui_theme.dart b/apps/mobile/packages/design_system/lib/src/ui_theme.dart index 98ee7214..919a78a0 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_theme.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_theme.dart @@ -40,7 +40,7 @@ class UiTheme { dividerTheme: const DividerThemeData( color: UiColors.separatorPrimary, space: 1, - thickness: 1, + thickness: 0.5, ), // Card Theme 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 dc408d94..7b8b05a5 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 @@ -72,9 +72,8 @@ class _ShiftDetailsPageState extends State { 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), + color: UiColors.bgThird, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), child: Column( children: [ @@ -104,8 +103,8 @@ class _ShiftDetailsPageState extends State { return Container( padding: const EdgeInsets.all(UiConstants.space3), decoration: BoxDecoration( - color: UiColors.background, - borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), + color: UiColors.bgThird, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), child: Column( children: [ @@ -184,229 +183,265 @@ class _ShiftDetailsPageState extends State { children: [ Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.all(UiConstants.space5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Role & Client Section - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: UiConstants.space4, - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: UiColors.background, - borderRadius: BorderRadius.circular( - UiConstants.radiusBase, + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space4, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: UiColors.background, + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, + ), + border: Border.all(color: UiColors.border), ), - border: Border.all(color: UiColors.border), - ), - child: const Center( - child: Icon( - UiIcons.briefcase, - color: UiColors.primary, - size: 24, + child: const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: 24, + ), ), ), - ), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + displayShift.title, + style: + UiTypography.headline1b.textPrimary, + ), + Text( + displayShift.clientName, + style: + UiTypography.body1m.textSecondary, + ), + Text( + displayShift.locationAddress, + style: + UiTypography.body2r.textSecondary, + ), + ], + ), + ), + ], + ), + ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const Divider(height: 1, thickness: 0.5), + + // Date Section + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + i18n.shift_date, + style: UiTypography + .titleUppercase4b + .textSecondary, + ), + const SizedBox(height: UiConstants.space2), + Row( children: [ + const Icon( + UiIcons.calendar, + size: 20, + color: UiColors.primary, + ), + const SizedBox(width: UiConstants.space2), Text( - displayShift.title, + _formatDate(displayShift.date), style: - UiTypography.headline1b.textPrimary, - ), - Text( - displayShift.clientName, - style: UiTypography.body1m.textSecondary, - ), - Text( - displayShift.locationAddress, - style: UiTypography.body2r.textSecondary, + UiTypography.headline5m.textPrimary, ), ], ), - ), - ], + ], + ), ), - const SizedBox(height: UiConstants.space6), - // Date Section - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i18n.shift_date, - style: - UiTypography.titleUppercase4b.textSecondary, - ), - const SizedBox(height: UiConstants.space2), - Row( - children: [ - const Icon( - UiIcons.calendar, - size: 20, - color: UiColors.primary, - ), - const SizedBox(width: UiConstants.space2), - Text( - _formatDate(displayShift.date), - style: UiTypography.headline5m.textPrimary, - ), - ], - ), - ], - ), - const SizedBox(height: UiConstants.space6), + const Divider(height: 1, thickness: 0.5), // Stats Row (New) - Row( - children: [ - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${estimatedTotal.toStringAsFixed(0)}", - "Total", + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + children: [ + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + "Total", + ), ), - ), - 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.dollar, + "\$${displayShift.hourlyRate.toStringAsFixed(0)}", + "Hourly Rate", + ), ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildStatCard( - UiIcons.clock, - "${duration.toStringAsFixed(1)}", - "Hours", + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildStatCard( + UiIcons.clock, + duration.toStringAsFixed(1), + "Hours", + ), ), - ), - ], + ], + ), ), - const SizedBox(height: UiConstants.space6), + + const Divider(height: 1, thickness: 0.5), // Time Section (New) - Row( - children: [ - Expanded( - child: _buildTimeBox( - "CLOCK IN TIME", - displayShift.startTime, + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + children: [ + Expanded( + child: _buildTimeBox( + "CLOCK IN TIME", + displayShift.startTime, + ), ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildTimeBox( - "CLOCK OUT TIME", - displayShift.endTime, + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildTimeBox( + "CLOCK OUT TIME", + displayShift.endTime, + ), ), - ), - ], + ], + ), ), - const SizedBox(height: UiConstants.space6), + + const Divider(height: 1, thickness: 0.5), // Location Section (New with Map) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "LOCATION", - style: - UiTypography.titleUppercase4b.textSecondary, - ), - const SizedBox(height: UiConstants.space3), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - displayShift.location.isEmpty - ? "TBD" - : displayShift.location, - style: UiTypography.title1m.textPrimary, - overflow: TextOverflow.ellipsis, + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "LOCATION", + style: UiTypography + .titleUppercase4b + .textSecondary, + ), + const SizedBox(height: UiConstants.space3), + 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, + 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, + ), ), - 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, ), - ); - }, - 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, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, + ), ), + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + vertical: 0, + ), + minimumSize: const Size(0, 32), ), - 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, - ), - ], + ], + ), + const SizedBox(height: UiConstants.space3), + ShiftLocationMap( + shift: displayShift, + height: 160, + borderRadius: UiConstants.radiusBase, + ), + ], + ), ), - const SizedBox(height: UiConstants.space8), + + const Divider(height: 1, thickness: 0.5), // Description / Instructions if ((displayShift.description ?? '').isNotEmpty) ...[ - Text( - i18n.job_description, - style: - UiTypography.titleUppercase4b.textSecondary, + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + i18n.job_description, + style: UiTypography + .titleUppercase4b + .textSecondary, + ), + const SizedBox(height: UiConstants.space2), + Text( + displayShift.description!, + style: UiTypography.body2r.textSecondary, + ), + ], + ), ), - const SizedBox(height: UiConstants.space2), - Text( - displayShift.description!, - style: UiTypography.body2r.textSecondary, - ), - const SizedBox(height: UiConstants.space8), ], ], ), ), ), + // Bottom Action Bar Container( padding: EdgeInsets.fromLTRB( @@ -421,7 +456,7 @@ class _ShiftDetailsPageState extends State { border: Border(top: BorderSide(color: UiColors.border)), boxShadow: [ BoxShadow( - color: UiColors.black.withValues(alpha: 0.05), + color: UiColors.popupShadow.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, -4), ), @@ -628,46 +663,18 @@ class _ShiftDetailsPageState extends State { } if (status == 'open' || status == 'available') { - return Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => _declineShift(context, shift.id), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.textSecondary, - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space4, - ), - side: const BorderSide(color: UiColors.border), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - ), - child: Text( - i18n.decline, - style: UiTypography.body2b.textSecondary, - ), - ), + return ElevatedButton( + onPressed: () => _bookShift(context, shift), + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + foregroundColor: UiColors.white, + padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: ElevatedButton( - onPressed: () => _bookShift(context, shift), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space4, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - elevation: 0, - ), - child: Text(i18n.apply_now, style: UiTypography.body2b.white), - ), - ), - ], + elevation: 0, + ), + child: Text(i18n.apply_now, style: UiTypography.body2b.white), ); } From 9b6cad3bdecfda73d3a4514f03b3828359262f82 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 13:26:04 -0500 Subject: [PATCH 03/12] feat(breaks): Implement break functionality with Break entity and adapter --- .../packages/domain/lib/krow_domain.dart | 2 + .../adapters/shifts/break/break_adapter.dart | 39 ++++++++ .../lib/src/entities/shifts/break/break.dart | 47 ++++++++++ .../domain/lib/src/entities/shifts/shift.dart | 71 ++++++++------- .../shifts_repository_impl.dart | 24 +++++ .../pages/shift_details_page.dart | 90 ++++++------------- .../connector/application/queries.gql | 7 +- 7 files changed, 180 insertions(+), 100 deletions(-) create mode 100644 apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 3dc41679..bbe513ae 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -30,6 +30,8 @@ export 'src/entities/events/work_session.dart'; // Shifts export 'src/entities/shifts/shift.dart'; export 'src/adapters/shifts/shift_adapter.dart'; +export 'src/entities/shifts/break/break.dart'; +export 'src/adapters/shifts/break/break_adapter.dart'; // Orders & Requests export 'src/entities/orders/order_type.dart'; diff --git a/apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart new file mode 100644 index 00000000..59f46949 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart @@ -0,0 +1,39 @@ +import '../../../entities/shifts/break/break.dart'; + +/// Adapter for Break related data. +class BreakAdapter { + /// Maps break data to a Break entity. + /// + /// [isPaid] whether the break is paid. + /// [breakTime] the string representation of the break duration (e.g., 'MIN_10', 'MIN_30'). + static Break fromData({ + required bool isPaid, + required String? breakTime, + }) { + return Break( + isBreakPaid: isPaid, + duration: _parseDuration(breakTime), + ); + } + + static BreakDuration _parseDuration(String? breakTime) { + if (breakTime == null) return BreakDuration.none; + + switch (breakTime.toUpperCase()) { + case 'MIN_10': + return BreakDuration.ten; + case 'MIN_15': + return BreakDuration.fifteen; + case 'MIN_20': + return BreakDuration.twenty; + case 'MIN_30': + return BreakDuration.thirty; + case 'MIN_45': + return BreakDuration.fortyFive; + case 'MIN_60': + return BreakDuration.sixty; + default: + return BreakDuration.none; + } + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart new file mode 100644 index 00000000..b90750bd --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart @@ -0,0 +1,47 @@ +import 'package:equatable/equatable.dart'; + +/// Enum representing common break durations in minutes. +enum BreakDuration { + /// No break. + none(0), + + /// 10 minutes break. + ten(10), + + /// 15 minutes break. + fifteen(15), + + /// 20 minutes break. + twenty(20), + + /// 30 minutes break. + thirty(30), + + /// 45 minutes break. + fortyFive(45), + + /// 60 minutes break. + sixty(60); + + /// The duration in minutes. + final int minutes; + + const BreakDuration(this.minutes); +} + +/// Represents a break configuration for a shift. +class Break extends Equatable { + const Break({ + required this.duration, + required this.isBreakPaid, + }); + + /// The duration of the break. + final BreakDuration duration; + + /// Whether the break is paid or unpaid. + final bool isBreakPaid; + + @override + List get props => [duration, isBreakPaid]; +} 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 5ef22e1e..e24d6477 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:krow_domain/src/entities/shifts/break/break.dart'; class Shift extends Equatable { final String id; @@ -29,6 +30,7 @@ class Shift extends Equatable { final String? roleId; final bool? hasApplied; final double? totalValue; + final Break? breakInfo; const Shift({ required this.id, @@ -59,48 +61,49 @@ class Shift extends Equatable { this.roleId, this.hasApplied, this.totalValue, + this.breakInfo, }); @override - List get props => [ - id, - title, - clientName, - logoUrl, - hourlyRate, - location, - locationAddress, - date, - startTime, - endTime, - createdDate, - tipsAvailable, - travelTime, - mealProvided, - parkingAvailable, - gasCompensation, - description, - instructions, - managers, - latitude, - longitude, - status, - durationDays, - requiredSlots, - filledSlots, - roleId, - hasApplied, - totalValue, - ]; + List get props => [ + id, + title, + clientName, + logoUrl, + hourlyRate, + location, + locationAddress, + date, + startTime, + endTime, + createdDate, + tipsAvailable, + travelTime, + mealProvided, + parkingAvailable, + gasCompensation, + description, + instructions, + managers, + latitude, + longitude, + status, + durationDays, + requiredSlots, + filledSlots, + roleId, + hasApplied, + totalValue, + breakInfo, + ]; } class ShiftManager extends Equatable { + const ShiftManager({required this.name, required this.phone, this.avatar}); + final String name; final String phone; final String? avatar; - - const ShiftManager({required this.name, required this.phone, this.avatar}); - @override - List get props => [name, phone, avatar]; + List get props => [name, phone, avatar]; } 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 a1588633..5700c60a 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 @@ -141,6 +141,10 @@ class ShiftsRepositoryImpl requiredSlots: app.shiftRole.count, filledSlots: app.shiftRole.assigned ?? 0, hasApplied: true, + breakInfo: BreakAdapter.fromData( + isPaid: app.shiftRole.isBreakPaid ?? false, + breakTime: app.shiftRole.breakType?.stringValue, + ), ), ); } @@ -208,6 +212,10 @@ class ShiftsRepositoryImpl requiredSlots: app.shiftRole.count, filledSlots: app.shiftRole.assigned ?? 0, hasApplied: true, + breakInfo: BreakAdapter.fromData( + isPaid: app.shiftRole.isBreakPaid ?? false, + breakTime: app.shiftRole.breakType?.stringValue, + ), ), ); } @@ -277,6 +285,10 @@ class ShiftsRepositoryImpl durationDays: sr.shift.durationDays, requiredSlots: sr.count, filledSlots: sr.assigned ?? 0, + breakInfo: BreakAdapter.fromData( + isPaid: sr.isBreakPaid ?? false, + breakTime: sr.breakType?.stringValue, + ), ), ); } @@ -350,6 +362,10 @@ class ShiftsRepositoryImpl filledSlots: sr.assigned ?? 0, hasApplied: hasApplied, totalValue: sr.totalValue, + breakInfo: BreakAdapter.fromData( + isPaid: sr.isBreakPaid ?? false, + breakTime: sr.breakType?.stringValue, + ), ); } @@ -360,6 +376,7 @@ class ShiftsRepositoryImpl int? required; int? filled; + Break? breakInfo; try { final rolesRes = await executeProtected(() => _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute()); @@ -370,6 +387,12 @@ class ShiftsRepositoryImpl required = (required ?? 0) + r.count; filled = (filled ?? 0) + (r.assigned ?? 0); } + // Use the first role's break info as a representative + final firstRole = rolesRes.data.shiftRoles.first; + breakInfo = BreakAdapter.fromData( + isPaid: firstRole.isBreakPaid ?? false, + breakTime: firstRole.breakType?.stringValue, + ); } } catch (_) {} @@ -394,6 +417,7 @@ class ShiftsRepositoryImpl durationDays: s.durationDays, requiredSlots: required, filledSlots: filled, + breakInfo: breakInfo, ); } 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 7b8b05a5..7ca8f8ca 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 @@ -132,7 +132,7 @@ class _ShiftDetailsPageState extends State { return BlocProvider( create: (_) => Modular.get() ..add( - LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift?.roleId), + LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId), ), child: BlocListener( listener: (context, state) { @@ -148,7 +148,7 @@ class _ShiftDetailsPageState extends State { ); Modular.to.toShifts(selectedDate: state.shiftDate); } else if (state is ShiftDetailsError) { - if (_isApplying || widget.shift == null) { + if (_isApplying) { UiSnackbar.show( context, message: translateErrorKey(state.message), @@ -240,7 +240,7 @@ class _ShiftDetailsPageState extends State { const Divider(height: 1, thickness: 0.5), - // Date Section + // Date & Time Section Padding( padding: const EdgeInsets.all(UiConstants.space5), child: Column( @@ -248,8 +248,7 @@ class _ShiftDetailsPageState extends State { children: [ Text( i18n.shift_date, - style: UiTypography - .titleUppercase4b + style: UiTypography.titleUppercase4b .textSecondary, ), const SizedBox(height: UiConstants.space2), @@ -268,6 +267,24 @@ class _ShiftDetailsPageState extends State { ), ], ), + const SizedBox(height: UiConstants.space4), + Row( + children: [ + Expanded( + child: _buildTimeBox( + "CLOCK IN TIME", + displayShift.startTime, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildTimeBox( + "CLOCK OUT TIME", + displayShift.endTime, + ), + ), + ], + ), ], ), ), @@ -308,30 +325,6 @@ class _ShiftDetailsPageState extends State { const Divider(height: 1, thickness: 0.5), - // Time Section (New) - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - children: [ - Expanded( - child: _buildTimeBox( - "CLOCK IN TIME", - displayShift.startTime, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildTimeBox( - "CLOCK OUT TIME", - displayShift.endTime, - ), - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - // Location Section (New with Map) Padding( padding: const EdgeInsets.all(UiConstants.space5), @@ -344,7 +337,6 @@ class _ShiftDetailsPageState extends State { .titleUppercase4b .textSecondary, ), - const SizedBox(height: UiConstants.space3), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -366,12 +358,10 @@ class _ShiftDetailsPageState extends State { ).showSnackBar( SnackBar( content: Text( - displayShift! - .locationAddress + displayShift.locationAddress .isNotEmpty - ? displayShift! - .locationAddress - : displayShift!.location, + ? displayShift.locationAddress + : displayShift.location, ), duration: const Duration( seconds: 3, @@ -509,36 +499,6 @@ class _ShiftDetailsPageState extends State { ); } - void _declineShift(BuildContext context, String id) { - final i18n = Translations.of( - context, - ).staff_shifts.shift_details.decline_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: () { - BlocProvider.of( - context, - ).add(DeclineShiftDetailsEvent(id)); - }, - style: TextButton.styleFrom(foregroundColor: UiColors.destructive), - child: Text( - Translations.of(context).staff_shifts.shift_details.decline, - ), - ), - ], - ), - ); - } - void _showApplyingDialog(BuildContext context, Shift shift) { if (_actionDialogOpen) return; _actionDialogOpen = true; diff --git a/backend/dataconnect/connector/application/queries.gql b/backend/dataconnect/connector/application/queries.gql index 44c66795..6d57438f 100644 --- a/backend/dataconnect/connector/application/queries.gql +++ b/backend/dataconnect/connector/application/queries.gql @@ -52,6 +52,8 @@ query listApplications @auth(level: USER) { startTime endTime hours + breakType + isBreakPaid totalValue role { id @@ -341,6 +343,8 @@ query getApplicationsByStaffId( startTime endTime hours + breakType + isBreakPaid totalValue role { id @@ -352,7 +356,6 @@ query getApplicationsByStaffId( } } - query vaidateDayStaffApplication( $staffId: UUID! $offset: Int @@ -692,6 +695,8 @@ query listCompletedApplicationsByStaffId( startTime endTime hours + breakType + isBreakPaid totalValue role { From 55f62207a815c9196a8115e3d92b7aee0e6340a6 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 13:31:21 -0500 Subject: [PATCH 04/12] feat(breaks): Add break duration and payment status to shift details --- .../lib/src/l10n/en.i18n.json | 3 + .../lib/src/l10n/es.i18n.json | 3 + .../pages/shift_details_page.dart | 104 ++++++++++++------ 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 40b07667..2dc0b1e5 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -999,6 +999,9 @@ "est_total": "Est. Total", "hours_label": "$count hours", "location": "LOCATION", + "break": "BREAK", + "paid": "Paid", + "unpaid": "Unpaid", "open_in_maps": "Open in Maps", "job_description": "JOB DESCRIPTION", "cancel_shift": "CANCEL SHIFT", diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 7627b0e3..2c70eb24 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -999,6 +999,9 @@ "est_total": "Total est.", "hours_label": "$count horas", "location": "UBICACIÓN", + "break": "DESCANSO", + "paid": "Pagado", + "unpaid": "No pagado", "open_in_maps": "Abrir en Mapas", "job_description": "DESCRIPCIÓN DEL TRABAJO", "cancel_shift": "CANCELAR TURNO", 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 7ca8f8ca..b00740a9 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 @@ -240,6 +240,40 @@ class _ShiftDetailsPageState extends State { const Divider(height: 1, thickness: 0.5), + // Stats Row (New) + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + children: [ + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + "Total", + ), + ), + 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 Divider(height: 1, thickness: 0.5), + // Date & Time Section Padding( padding: const EdgeInsets.all(UiConstants.space5), @@ -248,7 +282,8 @@ class _ShiftDetailsPageState extends State { children: [ Text( i18n.shift_date, - style: UiTypography.titleUppercase4b + style: UiTypography + .titleUppercase4b .textSecondary, ), const SizedBox(height: UiConstants.space2), @@ -288,42 +323,44 @@ class _ShiftDetailsPageState extends State { ], ), ), - + const Divider(height: 1, thickness: 0.5), - // Stats Row (New) - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - children: [ - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${estimatedTotal.toStringAsFixed(0)}", - "Total", + // Break Section + if (displayShift.breakInfo != null && + displayShift.breakInfo!.duration != + BreakDuration.none) ...[ + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "BREAK", + style: UiTypography.titleUppercase4b + .textSecondary, ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${displayShift.hourlyRate.toStringAsFixed(0)}", - "Hourly Rate", + const SizedBox(height: UiConstants.space2), + Row( + children: [ + const Icon( + UiIcons.breakIcon, + size: 20, + color: UiColors.primary, + ), + const SizedBox(width: UiConstants.space2), + Text( + "${displayShift.breakInfo!.duration.minutes} min (${displayShift.breakInfo!.isBreakPaid ? 'Paid' : 'Unpaid'})", + style: + UiTypography.headline5m.textPrimary, + ), + ], ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildStatCard( - UiIcons.clock, - duration.toStringAsFixed(1), - "Hours", - ), - ), - ], + ], + ), ), - ), - - const Divider(height: 1, thickness: 0.5), + const Divider(height: 1, thickness: 0.5), + ], // Location Section (New with Map) Padding( @@ -358,7 +395,8 @@ class _ShiftDetailsPageState extends State { ).showSnackBar( SnackBar( content: Text( - displayShift.locationAddress + displayShift + .locationAddress .isNotEmpty ? displayShift.locationAddress : displayShift.location, From 2f9b2788f8e5c224c2db79ffe4cf913a73239d6b Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 13:33:22 -0500 Subject: [PATCH 05/12] feat(localization): Update English and Spanish translations for shift details and breaks --- .../lib/src/l10n/en.i18n.json | 7 ++++++- .../lib/src/l10n/es.i18n.json | 7 ++++++- .../pages/shift_details_page.dart | 20 +++++++++---------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 2dc0b1e5..0241ab37 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -999,9 +999,14 @@ "est_total": "Est. Total", "hours_label": "$count hours", "location": "LOCATION", - "break": "BREAK", + "tbd": "TBD", + "get_direction": "Get direction", + "break_title": "BREAK", "paid": "Paid", "unpaid": "Unpaid", + "min": "min", + "hourly_rate": "Hourly Rate", + "hours": "Hours", "open_in_maps": "Open in Maps", "job_description": "JOB DESCRIPTION", "cancel_shift": "CANCEL SHIFT", diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 2c70eb24..ee54965e 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -999,9 +999,14 @@ "est_total": "Total est.", "hours_label": "$count horas", "location": "UBICACIÓN", - "break": "DESCANSO", + "tbd": "TBD", + "get_direction": "Obtener dirección", + "break_title": "DESCANSO", "paid": "Pagado", "unpaid": "No pagado", + "min": "min", + "hourly_rate": "Tarifa por hora", + "hours": "Horas", "open_in_maps": "Abrir en Mapas", "job_description": "DESCRIPCIÓN DEL TRABAJO", "cancel_shift": "CANCELAR TURNO", 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 b00740a9..daab8913 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 @@ -249,7 +249,7 @@ class _ShiftDetailsPageState extends State { child: _buildStatCard( UiIcons.dollar, "\$${estimatedTotal.toStringAsFixed(0)}", - "Total", + i18n.est_total, ), ), const SizedBox(width: UiConstants.space4), @@ -257,7 +257,7 @@ class _ShiftDetailsPageState extends State { child: _buildStatCard( UiIcons.dollar, "\$${displayShift.hourlyRate.toStringAsFixed(0)}", - "Hourly Rate", + i18n.hourly_rate, ), ), const SizedBox(width: UiConstants.space4), @@ -265,7 +265,7 @@ class _ShiftDetailsPageState extends State { child: _buildStatCard( UiIcons.clock, duration.toStringAsFixed(1), - "Hours", + i18n.hours, ), ), ], @@ -307,14 +307,14 @@ class _ShiftDetailsPageState extends State { children: [ Expanded( child: _buildTimeBox( - "CLOCK IN TIME", + i18n.start_time, displayShift.startTime, ), ), const SizedBox(width: UiConstants.space4), Expanded( child: _buildTimeBox( - "CLOCK OUT TIME", + i18n.end_time, displayShift.endTime, ), ), @@ -336,7 +336,7 @@ class _ShiftDetailsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "BREAK", + i18n.break_title, style: UiTypography.titleUppercase4b .textSecondary, ), @@ -350,7 +350,7 @@ class _ShiftDetailsPageState extends State { ), const SizedBox(width: UiConstants.space2), Text( - "${displayShift.breakInfo!.duration.minutes} min (${displayShift.breakInfo!.isBreakPaid ? 'Paid' : 'Unpaid'})", + "${displayShift.breakInfo!.duration.minutes} ${i18n.min} (${displayShift.breakInfo!.isBreakPaid ? i18n.paid : i18n.unpaid})", style: UiTypography.headline5m.textPrimary, ), @@ -369,7 +369,7 @@ class _ShiftDetailsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "LOCATION", + i18n.location, style: UiTypography .titleUppercase4b .textSecondary, @@ -381,7 +381,7 @@ class _ShiftDetailsPageState extends State { Expanded( child: Text( displayShift.location.isEmpty - ? "TBD" + ? i18n.tbd : displayShift.location, style: UiTypography.title1m.textPrimary, overflow: TextOverflow.ellipsis, @@ -411,7 +411,7 @@ class _ShiftDetailsPageState extends State { UiIcons.navigation, size: UiConstants.iconXs, ), - label: const Text("Get direction"), + label: Text(i18n.get_direction), style: OutlinedButton.styleFrom( foregroundColor: UiColors.textPrimary, side: const BorderSide( From 0b787dbc128e14ebd60e602a2654577d3b02644e Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 13:38:39 -0500 Subject: [PATCH 06/12] feat(shift-details): Refactor ShiftDetailsPage layout and implement new sections for breaks, date/time, description, and location --- .../pages/shift_details_page.dart | 574 +++--------------- .../shift_details/shift_break_section.dart | 62 ++ .../shift_date_time_section.dart | 135 ++++ .../shift_description_section.dart | 41 ++ .../shift_details_bottom_bar.dart | 137 +++++ .../shift_details/shift_details_header.dart | 65 ++ .../shift_details/shift_location_section.dart | 94 +++ .../shift_details/shift_stats_row.dart | 99 +++ 8 files changed, 727 insertions(+), 480 deletions(-) create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_break_section.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_description_section.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_bottom_bar.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_header.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_location_section.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_stats_row.dart 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 daab8913..e4563de1 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 @@ -10,7 +10,13 @@ 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'; +import '../widgets/shift_details/shift_break_section.dart'; +import '../widgets/shift_details/shift_date_time_section.dart'; +import '../widgets/shift_details/shift_description_section.dart'; +import '../widgets/shift_details/shift_details_bottom_bar.dart'; +import '../widgets/shift_details/shift_details_header.dart'; +import '../widgets/shift_details/shift_location_section.dart'; +import '../widgets/shift_details/shift_stats_row.dart'; class ShiftDetailsPage extends StatefulWidget { final String shiftId; @@ -68,65 +74,6 @@ class _ShiftDetailsPageState extends State { } } - Widget _buildStatCard(IconData icon, String value, String label) { - return Container( - padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.bgThird, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - 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.bgThird, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - 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, - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { return BlocProvider( @@ -134,7 +81,7 @@ class _ShiftDetailsPageState extends State { ..add( LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId), ), - child: BlocListener( + child: BlocConsumer( listener: (context, state) { if (state is ShiftActionSuccess || state is ShiftDetailsError) { _closeActionDialog(context); @@ -158,345 +105,99 @@ class _ShiftDetailsPageState extends State { _isApplying = false; } }, - child: BlocBuilder( - builder: (context, state) { - if (state is ShiftDetailsLoading) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), - ); - } + builder: (context, state) { + if (state is ShiftDetailsLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } - Shift? displayShift = widget.shift; + Shift displayShift = widget.shift; + final i18n = Translations.of(context).staff_shifts.shift_details; - final i18n = Translations.of(context).staff_shifts.shift_details; + final duration = _calculateDuration(displayShift); + final estimatedTotal = + displayShift.totalValue ?? (displayShift.hourlyRate * duration); - final duration = _calculateDuration(displayShift); - final estimatedTotal = - displayShift.totalValue ?? (displayShift.hourlyRate * duration); - - return Scaffold( - appBar: UiAppBar( - centerTitle: false, - onLeadingPressed: () => Modular.to.toShifts(), - ), - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Role & Client Section - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: UiConstants.space4, - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: UiColors.background, - borderRadius: BorderRadius.circular( - UiConstants.radiusBase, - ), - border: Border.all(color: UiColors.border), - ), - child: const Center( - child: Icon( - UiIcons.briefcase, - color: UiColors.primary, - size: 24, - ), - ), - ), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - displayShift.title, - style: - UiTypography.headline1b.textPrimary, - ), - Text( - displayShift.clientName, - style: - UiTypography.body1m.textSecondary, - ), - Text( - displayShift.locationAddress, - style: - UiTypography.body2r.textSecondary, - ), - ], - ), - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Stats Row (New) - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - children: [ - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${estimatedTotal.toStringAsFixed(0)}", - i18n.est_total, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${displayShift.hourlyRate.toStringAsFixed(0)}", - i18n.hourly_rate, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildStatCard( - UiIcons.clock, - duration.toStringAsFixed(1), - i18n.hours, - ), - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Date & Time Section - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i18n.shift_date, - style: UiTypography - .titleUppercase4b - .textSecondary, - ), - const SizedBox(height: UiConstants.space2), - Row( - children: [ - const Icon( - UiIcons.calendar, - size: 20, - color: UiColors.primary, - ), - const SizedBox(width: UiConstants.space2), - Text( - _formatDate(displayShift.date), - style: - UiTypography.headline5m.textPrimary, - ), - ], - ), - const SizedBox(height: UiConstants.space4), - Row( - children: [ - Expanded( - child: _buildTimeBox( - i18n.start_time, - displayShift.startTime, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildTimeBox( - i18n.end_time, - displayShift.endTime, - ), - ), - ], - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Break Section - if (displayShift.breakInfo != null && - displayShift.breakInfo!.duration != - BreakDuration.none) ...[ - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i18n.break_title, - style: UiTypography.titleUppercase4b - .textSecondary, - ), - const SizedBox(height: UiConstants.space2), - Row( - children: [ - const Icon( - UiIcons.breakIcon, - size: 20, - color: UiColors.primary, - ), - const SizedBox(width: UiConstants.space2), - Text( - "${displayShift.breakInfo!.duration.minutes} ${i18n.min} (${displayShift.breakInfo!.isBreakPaid ? i18n.paid : i18n.unpaid})", - style: - UiTypography.headline5m.textPrimary, - ), - ], - ), - ], - ), - ), - const Divider(height: 1, thickness: 0.5), - ], - - // Location Section (New with Map) - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i18n.location, - style: UiTypography - .titleUppercase4b - .textSecondary, - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - displayShift.location.isEmpty - ? i18n.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, - ), - label: Text(i18n.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, - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Description / Instructions - if ((displayShift.description ?? '').isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i18n.job_description, - style: UiTypography - .titleUppercase4b - .textSecondary, - ), - const SizedBox(height: UiConstants.space2), - Text( - displayShift.description!, - style: UiTypography.body2r.textSecondary, - ), - ], - ), - ), - ], - ], - ), - ), - ), - - // Bottom Action Bar - Container( - padding: EdgeInsets.fromLTRB( - UiConstants.space5, - UiConstants.space4, - UiConstants.space5, - MediaQuery.of(context).padding.bottom + - UiConstants.space4, - ), - decoration: BoxDecoration( - color: UiColors.white, - border: Border(top: BorderSide(color: UiColors.border)), - boxShadow: [ - BoxShadow( - color: UiColors.popupShadow.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, -4), + return Scaffold( + appBar: UiAppBar( + centerTitle: false, + onLeadingPressed: () => Modular.to.toShifts(), + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShiftDetailsHeader(shift: displayShift), + const Divider(height: 1, thickness: 0.5), + ShiftStatsRow( + estimatedTotal: estimatedTotal, + hourlyRate: displayShift.hourlyRate, + duration: duration, + totalLabel: i18n.est_total, + hourlyRateLabel: i18n.hourly_rate, + hoursLabel: i18n.hours, ), + const Divider(height: 1, thickness: 0.5), + ShiftDateTimeSection( + date: displayShift.date, + startTime: displayShift.startTime, + endTime: displayShift.endTime, + shiftDateLabel: i18n.shift_date, + clockInLabel: i18n.start_time, + clockOutLabel: i18n.end_time, + ), + const Divider(height: 1, thickness: 0.5), + if (displayShift.breakInfo != null && + displayShift.breakInfo!.duration != + BreakDuration.none) ...[ + ShiftBreakSection( + breakInfo: displayShift.breakInfo!, + breakTitle: i18n.break_title, + paidLabel: i18n.paid, + unpaidLabel: i18n.unpaid, + minLabel: i18n.min, + ), + const Divider(height: 1, thickness: 0.5), + ], + ShiftLocationSection( + shift: displayShift, + locationLabel: i18n.location, + tbdLabel: i18n.tbd, + getDirectionLabel: i18n.get_direction, + ), + const Divider(height: 1, thickness: 0.5), + if (displayShift.description != null && + displayShift.description!.isNotEmpty) + ShiftDescriptionSection( + description: displayShift.description!, + descriptionLabel: i18n.job_description, + ), ], ), - child: _buildBottomButton(displayShift, context), ), - ], - ), - ); - }, - ), + ), + ShiftDetailsBottomBar( + shift: displayShift, + onApply: () => _bookShift(context, displayShift), + onDecline: () => BlocProvider.of( + context, + ).add(DeclineShiftDetailsEvent(displayShift.id)), + onAccept: () => + BlocProvider.of(context).add( + BookShiftDetailsEvent( + displayShift.id, + roleId: displayShift.roleId, + ), + ), + ), + ], + ), + ); + }, ), ); } @@ -591,91 +292,4 @@ class _ShiftDetailsPageState extends State { Navigator.of(context, rootNavigator: true).pop(); _actionDialogOpen = false; } - - Widget _buildBottomButton(Shift shift, BuildContext context) { - final String status = shift.status ?? 'open'; - - final i18n = Translations.of(context).staff_shifts.shift_details; - if (status == 'confirmed') { - 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, - ), - child: Text(i18n.clock_in, style: UiTypography.body2b.white), - ), - ); - } - - if (status == 'pending') { - return Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => BlocProvider.of( - context, - ).add(DeclineShiftDetailsEvent(shift.id)), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.destructive, - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space4, - ), - side: const BorderSide(color: UiColors.destructive), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - ), - child: Text(i18n.decline, style: UiTypography.body2b.textError), - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: ElevatedButton( - onPressed: () => BlocProvider.of( - context, - ).add(BookShiftDetailsEvent(shift.id, roleId: shift.roleId)), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space4, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - elevation: 0, - ), - child: Text(i18n.accept_shift, style: UiTypography.body2b.white), - ), - ), - ], - ); - } - - if (status == 'open' || status == 'available') { - return ElevatedButton( - onPressed: () => _bookShift(context, shift), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - elevation: 0, - ), - child: Text(i18n.apply_now, style: UiTypography.body2b.white), - ); - } - - return const SizedBox(); - } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_break_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_break_section.dart new file mode 100644 index 00000000..50288460 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_break_section.dart @@ -0,0 +1,62 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// A section displaying shift break details (duration and payment status). +class ShiftBreakSection extends StatelessWidget { + /// The break information. + final Break breakInfo; + + /// Localization string for break section title. + final String breakTitle; + + /// Localization string for paid status. + final String paidLabel; + + /// Localization string for unpaid status. + final String unpaidLabel; + + /// Localization string for minutes ("min"). + final String minLabel; + + /// Creates a [ShiftBreakSection]. + const ShiftBreakSection({ + super.key, + required this.breakInfo, + required this.breakTitle, + required this.paidLabel, + required this.unpaidLabel, + required this.minLabel, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + breakTitle, + style: UiTypography.titleUppercase4b.textSecondary, + ), + const SizedBox(height: UiConstants.space2), + Row( + children: [ + const Icon( + UiIcons.breakIcon, + size: 20, + color: UiColors.primary, + ), + const SizedBox(width: UiConstants.space2), + Text( + "${breakInfo.duration.minutes} $minLabel (${breakInfo.isBreakPaid ? paidLabel : unpaidLabel})", + style: UiTypography.headline5m.textPrimary, + ), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart new file mode 100644 index 00000000..47eded2f --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart @@ -0,0 +1,135 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// A section displaying the date and the shift's start/end times. +class ShiftDateTimeSection extends StatelessWidget { + /// The ISO string of the date. + final String date; + + /// The start time string (HH:mm). + final String startTime; + + /// The end time string (HH:mm). + final String endTime; + + /// Localization string for shift date. + final String shiftDateLabel; + + /// Localization string for clock in time. + final String clockInLabel; + + /// Localization string for clock out time. + final String clockOutLabel; + + /// Creates a [ShiftDateTimeSection]. + const ShiftDateTimeSection({ + super.key, + required this.date, + required this.startTime, + required this.endTime, + required this.shiftDateLabel, + required this.clockInLabel, + required this.clockOutLabel, + }); + + 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]); + final dt = DateTime(2022, 1, 1, hour, minute); + return DateFormat('h:mm a').format(dt); + } catch (e) { + return time; + } + } + + 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; + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + shiftDateLabel, + style: UiTypography.titleUppercase4b.textSecondary, + ), + const SizedBox(height: UiConstants.space2), + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 20, + color: UiColors.primary, + ), + const SizedBox(width: UiConstants.space2), + Text( + _formatDate(date), + style: UiTypography.headline5m.textPrimary, + ), + ], + ), + const SizedBox(height: UiConstants.space4), + Row( + children: [ + Expanded( + child: _buildTimeBox( + clockInLabel, + startTime, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildTimeBox( + clockOutLabel, + endTime, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildTimeBox(String label, String time) { + return Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.bgThird, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + 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_details/shift_description_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_description_section.dart new file mode 100644 index 00000000..770fc3f9 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_description_section.dart @@ -0,0 +1,41 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A section displaying the job description for the shift. +class ShiftDescriptionSection extends StatelessWidget { + /// The description text. + final String description; + + /// Localization string for description section title. + final String descriptionLabel; + + /// Creates a [ShiftDescriptionSection]. + const ShiftDescriptionSection({ + super.key, + required this.description, + required this.descriptionLabel, + }); + + @override + Widget build(BuildContext context) { + if (description.isEmpty) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + descriptionLabel, + style: UiTypography.titleUppercase4b.textSecondary, + ), + const SizedBox(height: UiConstants.space2), + Text( + description, + style: UiTypography.body2r.textSecondary, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_bottom_bar.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_bottom_bar.dart new file mode 100644 index 00000000..00eb9578 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_bottom_bar.dart @@ -0,0 +1,137 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:core_localization/core_localization.dart'; +import 'package:krow_core/core.dart'; + +/// A bottom action bar containing contextual buttons based on shift status. +class ShiftDetailsBottomBar extends StatelessWidget { + /// The current shift. + final Shift shift; + + /// Callback for applying/booking a shift. + final VoidCallback onApply; + + /// Callback for declining a shift. + final VoidCallback onDecline; + + /// Callback for accepting a shift. + final VoidCallback onAccept; + + /// Creates a [ShiftDetailsBottomBar]. + const ShiftDetailsBottomBar({ + super.key, + required this.shift, + required this.onApply, + required this.onDecline, + required this.onAccept, + }); + + @override + Widget build(BuildContext context) { + final String status = shift.status ?? 'open'; + final i18n = Translations.of(context).staff_shifts.shift_details; + + return Container( + padding: EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space4, + UiConstants.space5, + MediaQuery.of(context).padding.bottom + UiConstants.space4, + ), + decoration: BoxDecoration( + color: UiColors.white, + border: Border(top: BorderSide(color: UiColors.border)), + boxShadow: [ + BoxShadow( + color: UiColors.popupShadow.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, -4), + ), + ], + ), + child: _buildButtons(status, i18n, context), + ); + } + + Widget _buildButtons(String status, dynamic i18n, BuildContext context) { + if (status == 'confirmed') { + 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, + ), + child: Text(i18n.clock_in, style: UiTypography.body2b.white), + ), + ); + } + + if (status == 'pending') { + return Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: onDecline, + style: OutlinedButton.styleFrom( + foregroundColor: UiColors.destructive, + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), + side: const BorderSide(color: UiColors.destructive), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + ), + child: Text(i18n.decline, style: UiTypography.body2b.textError), + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: ElevatedButton( + onPressed: onAccept, + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + foregroundColor: UiColors.white, + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + elevation: 0, + ), + child: Text(i18n.accept_shift, style: UiTypography.body2b.white), + ), + ), + ], + ); + } + + if (status == 'open' || status == 'available') { + return ElevatedButton( + onPressed: onApply, + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + foregroundColor: UiColors.white, + padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + elevation: 0, + ), + child: Text(i18n.apply_now, style: UiTypography.body2b.white), + ); + } + + return const SizedBox.shrink(); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_header.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_header.dart new file mode 100644 index 00000000..ea8d70db --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_header.dart @@ -0,0 +1,65 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// A header widget for the shift details page displaying the role, client name, and address. +class ShiftDetailsHeader extends StatelessWidget { + /// The shift entity containing the header information. + final Shift shift; + + /// Creates a [ShiftDetailsHeader]. + const ShiftDetailsHeader({ + super.key, + required this.shift, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space4, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: UiColors.background, + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, + ), + border: Border.all(color: UiColors.border), + ), + child: const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: 24, + ), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + shift.title, + style: UiTypography.headline1b.textPrimary, + ), + Text( + shift.clientName, + style: UiTypography.body1m.textSecondary, + ), + Text( + shift.locationAddress, + style: UiTypography.body2r.textSecondary, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_location_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_location_section.dart new file mode 100644 index 00000000..35656dfd --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_location_section.dart @@ -0,0 +1,94 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../shift_location_map.dart'; + +/// A section displaying the shift's location, address, map, and "Get direction" action. +class ShiftLocationSection extends StatelessWidget { + /// The shift entity containing location data. + final Shift shift; + + /// Localization string for location section title. + final String locationLabel; + + /// Localization string for "TBD". + final String tbdLabel; + + /// Localization string for "Get direction". + final String getDirectionLabel; + + /// Creates a [ShiftLocationSection]. + const ShiftLocationSection({ + super.key, + required this.shift, + required this.locationLabel, + required this.tbdLabel, + required this.getDirectionLabel, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + locationLabel, + style: UiTypography.titleUppercase4b.textSecondary, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + shift.location.isEmpty ? tbdLabel : shift.location, + style: UiTypography.title1m.textPrimary, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: UiConstants.space3), + OutlinedButton.icon( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + shift.locationAddress.isNotEmpty + ? shift.locationAddress + : shift.location, + ), + duration: const Duration(seconds: 3), + ), + ); + }, + icon: const Icon( + UiIcons.navigation, + size: UiConstants.iconXs, + ), + label: Text(getDirectionLabel), + 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: shift, + height: 160, + borderRadius: UiConstants.radiusBase, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_stats_row.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_stats_row.dart new file mode 100644 index 00000000..49d8a8c6 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_stats_row.dart @@ -0,0 +1,99 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A row of statistic cards for shift details (Total Pay, Rate, Hours). +class ShiftStatsRow extends StatelessWidget { + /// Estimated total pay for the shift. + final double estimatedTotal; + + /// Hourly rate for the shift. + final double hourlyRate; + + /// Total duration of the shift in hours. + final double duration; + + /// Localization string for total. + final String totalLabel; + + /// Localization string for hourly rate. + final String hourlyRateLabel; + + /// Localization string for hours. + final String hoursLabel; + + /// Creates a [ShiftStatsRow]. + const ShiftStatsRow({ + super.key, + required this.estimatedTotal, + required this.hourlyRate, + required this.duration, + required this.totalLabel, + required this.hourlyRateLabel, + required this.hoursLabel, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + children: [ + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + totalLabel, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${hourlyRate.toStringAsFixed(0)}", + hourlyRateLabel, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildStatCard( + UiIcons.clock, + duration.toStringAsFixed(1), + hoursLabel, + ), + ), + ], + ), + ); + } + + Widget _buildStatCard(IconData icon, String value, String label) { + return Container( + padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.bgThird, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + 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), + ], + ), + ); + } +} From 6ed12a05192bf55057a5c7afc1b9ed0e72668ddb Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 13:46:01 -0500 Subject: [PATCH 07/12] feat(api-keys): Replace Google Places API key with Google Maps API key across the application --- apps/mobile/config.dev.json | 3 +-- apps/mobile/packages/core/lib/src/config/app_config.dart | 5 +---- .../lib/src/data/repositories_impl/hub_repository_impl.dart | 2 +- .../src/presentation/widgets/hub_address_autocomplete.dart | 2 +- .../src/data/repositories_impl/place_repository_impl.dart | 4 +++- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/mobile/config.dev.json b/apps/mobile/config.dev.json index 214cb535..4c630e51 100644 --- a/apps/mobile/config.dev.json +++ b/apps/mobile/config.dev.json @@ -1,4 +1,3 @@ { - "GOOGLE_PLACES_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU", - "GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0" + "GOOGLE_MAPS_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU" } \ 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 727638a4..9bf56394 100644 --- a/apps/mobile/packages/core/lib/src/config/app_config.dart +++ b/apps/mobile/packages/core/lib/src/config/app_config.dart @@ -4,9 +4,6 @@ class AppConfig { 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. + /// The Google Maps API key. static const String googleMapsApiKey = String.fromEnvironment('GOOGLE_MAPS_API_KEY'); } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart index 7d7a24f0..2fbd8aac 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart @@ -262,7 +262,7 @@ class HubRepositoryImpl { 'place_id': placeId, 'fields': 'address_component', - 'key': AppConfig.googlePlacesApiKey, + 'key': AppConfig.googleMapsApiKey, }, ); try { diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart index 138bb9e7..66f14d11 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart @@ -25,7 +25,7 @@ class HubAddressAutocomplete extends StatelessWidget { return GooglePlaceAutoCompleteTextField( textEditingController: controller, focusNode: focusNode, - googleAPIKey: AppConfig.googlePlacesApiKey, + googleAPIKey: AppConfig.googleMapsApiKey, debounceTime: 500, countries: HubsConstants.supportedCountries, isLatLngRequired: true, diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/place_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/place_repository_impl.dart index 9f8e99ad..6d6512b5 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/place_repository_impl.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/place_repository_impl.dart @@ -1,6 +1,8 @@ import 'dart:convert'; + import 'package:http/http.dart' as http; import 'package:krow_core/core.dart'; + import '../../domain/repositories/place_repository.dart'; class PlaceRepositoryImpl implements PlaceRepository { @@ -18,7 +20,7 @@ class PlaceRepositoryImpl implements PlaceRepository { { 'input': query, 'types': '(cities)', - 'key': AppConfig.googlePlacesApiKey, + 'key': AppConfig.googleMapsApiKey, }, ); From 888cf83c18d77803bdbdaef04b5bb4146ee6df20 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 13:50:02 -0500 Subject: [PATCH 08/12] feat(breaks): Implement Google Maps API key integration and decode Dart defines for both client and staff apps --- .../apps/client/android/app/build.gradle.kts | 16 ++++++++++++ .../android/app/src/main/AndroidManifest.xml | 3 +++ .../apps/client/ios/Runner/AppDelegate.swift | 25 +++++++++++++++++++ apps/mobile/apps/client/ios/Runner/Info.plist | 6 +++-- .../apps/staff/android/app/build.gradle.kts | 16 ++++++++++++ .../android/app/src/main/AndroidManifest.xml | 3 +++ .../apps/staff/ios/Runner/AppDelegate.swift | 25 +++++++++++++++++++ apps/mobile/apps/staff/ios/Runner/Info.plist | 6 +++-- 8 files changed, 96 insertions(+), 4 deletions(-) diff --git a/apps/mobile/apps/client/android/app/build.gradle.kts b/apps/mobile/apps/client/android/app/build.gradle.kts index 202bc20b..593af2c7 100644 --- a/apps/mobile/apps/client/android/app/build.gradle.kts +++ b/apps/mobile/apps/client/android/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Base64 + plugins { id("com.android.application") id("kotlin-android") @@ -6,6 +8,18 @@ plugins { id("com.google.gms.google-services") } +val dartDefinesString = project.findProperty("dart-defines") as? String ?: "" +val dartEnvironmentVariables = mutableMapOf() +dartDefinesString.split(",").forEach { + if (it.isNotEmpty()) { + val decoded = String(Base64.getDecoder().decode(it)) + val components = decoded.split("=") + if (components.size == 2) { + dartEnvironmentVariables[components[0]] = components[1] + } + } +} + android { namespace = "com.krowwithus.client" compileSdk = flutter.compileSdkVersion @@ -29,6 +43,8 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + + manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: "" } buildTypes { diff --git a/apps/mobile/apps/client/android/app/src/main/AndroidManifest.xml b/apps/mobile/apps/client/android/app/src/main/AndroidManifest.xml index 5bf8f125..da643f20 100644 --- a/apps/mobile/apps/client/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/apps/client/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,9 @@ +