From a7d66a1efe2980da39d5f921b1fa13053bcb176a Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 3 Mar 2026 20:28:12 -0500 Subject: [PATCH] feat: Implement rapid order creation via voice and text in mobile app - Added benefits section with state management - Refactored home page to include new sections for quick actions, today's shifts, and tomorrow's shifts - Introduced full-width divider for better layout - Created reusable section layout widget for consistent UI - Implemented circular progress indicator for benefits - Removed deprecated benefits widget and replaced with new structure - Updated data connection configuration for validation environment --- .../staff_connector_repository_impl.dart | 1 + .../design_system/lib/src/ui_typography.dart | 2 +- .../presentation/pages/worker_home_page.dart | 164 ++------------ .../widgets/home_page/benefits_section.dart | 36 ++++ .../widgets/home_page/full_width_divider.dart | 27 +++ .../home_page/quick_actions_section.dart | 48 +++++ .../home_page/recommended_shift_card.dart | 48 +---- .../home_page/recommended_shifts_section.dart | 54 +++++ .../widgets/home_page/section_layout.dart | 51 +++++ .../home_page/todays_shifts_section.dart | 68 ++++++ .../home_page/tomorrows_shifts_section.dart | 46 ++++ .../widgets/worker/benefits_widget.dart | 202 ------------------ .../worker/worker_benefits/benefit_item.dart | 84 ++++++++ .../benefits_view_all_link.dart | 39 ++++ .../worker_benefits/benefits_widget.dart | 59 +++++ .../circular_progress_painter.dart | 59 +++++ backend/dataconnect/dataconnect.yaml | 4 +- makefiles/dataconnect.mk | 2 +- 18 files changed, 601 insertions(+), 393 deletions(-) create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/benefits_section.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/full_width_divider.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/quick_actions_section.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shifts_section.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/section_layout.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/todays_shifts_section.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/tomorrows_shifts_section.dart delete mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/benefits_widget.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefit_item.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefits_view_all_link.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefits_widget.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/circular_progress_painter.dart diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/staff/data/repositories/staff_connector_repository_impl.dart b/apps/mobile/packages/data_connect/lib/src/connectors/staff/data/repositories/staff_connector_repository_impl.dart index 770f1d68..e5f0f4d5 100644 --- a/apps/mobile/packages/data_connect/lib/src/connectors/staff/data/repositories/staff_connector_repository_impl.dart +++ b/apps/mobile/packages/data_connect/lib/src/connectors/staff/data/repositories/staff_connector_repository_impl.dart @@ -20,6 +20,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository { @override Future getProfileCompletion() async { + return true; return _service.run(() async { final String staffId = await _service.getStaffId(); 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 eb436569..42567ce4 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_typography.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_typography.dart @@ -402,7 +402,7 @@ class UiTypography { /// Body 4 Medium - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: 0.05 (#121826) static final TextStyle body4m = _primaryBase.copyWith( fontWeight: FontWeight.w500, - fontSize: 12, + fontSize: 10, height: 1.5, letterSpacing: 0.05, color: UiColors.textPrimary, diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart index d6ac2559..2a30f19b 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart @@ -4,16 +4,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; - import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; -import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/benefits_section.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/full_width_divider.dart'; import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart'; import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart'; -import 'package:staff_home/src/presentation/widgets/home_page/quick_action_item.dart'; -import 'package:staff_home/src/presentation/widgets/home_page/recommended_shift_card.dart'; -import 'package:staff_home/src/presentation/widgets/home_page/section_header.dart'; -import 'package:staff_home/src/presentation/widgets/shift_card.dart'; -import 'package:staff_home/src/presentation/widgets/worker/benefits_widget.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/quick_actions_section.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/recommended_shifts_section.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/todays_shifts_section.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/tomorrows_shifts_section.dart'; /// The home page for the staff worker application. /// @@ -36,9 +35,6 @@ class WorkerHomePage extends StatelessWidget { final t = Translations.of(context); final i18n = t.staff.home; final bannersI18n = i18n.banners; - final quickI18n = i18n.quick_actions; - final sectionsI18n = i18n.sections; - final emptyI18n = i18n.empty_states; return BlocProvider.value( value: Modular.get()..loadShifts(), @@ -67,8 +63,7 @@ class WorkerHomePage extends StatelessWidget { builder: (context, state) { if (!state.isProfileComplete) { return SizedBox( - height: MediaQuery.of(context).size.height - - 300, + height: MediaQuery.of(context).size.height - 300, child: Column( children: [ PlaceholderBanner( @@ -85,7 +80,8 @@ class WorkerHomePage extends StatelessWidget { child: UiEmptyState( icon: UiIcons.users, title: 'Complete Your Profile', - description: 'Finish setting up your profile to unlock shifts, view earnings, and start earning today.', + description: + 'Finish setting up your profile to unlock shifts, view earnings, and start earning today.', ), ), ], @@ -96,147 +92,23 @@ class WorkerHomePage extends StatelessWidget { return Column( children: [ // Quick Actions - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: QuickActionItem( - icon: UiIcons.search, - label: quickI18n.find_shifts, - onTap: () => Modular.to.toShifts(), - ), - ), - Expanded( - child: QuickActionItem( - icon: UiIcons.calendar, - label: quickI18n.availability, - onTap: () => Modular.to.toAvailability(), - ), - ), - Expanded( - child: QuickActionItem( - icon: UiIcons.dollar, - label: quickI18n.earnings, - onTap: () => Modular.to.toPayments(), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space6), + const QuickActionsSection(), + const FullWidthDivider(), // Today's Shifts - BlocBuilder( - builder: (context, state) { - final shifts = state.todayShifts; - return Column( - children: [ - SectionHeader( - title: sectionsI18n.todays_shift, - action: shifts.isNotEmpty - ? sectionsI18n.scheduled_count( - count: shifts.length, - ) - : null, - ), - if (state.status == HomeStatus.loading) - const Center( - child: SizedBox( - height: UiConstants.space10, - width: UiConstants.space10, - child: CircularProgressIndicator( - color: UiColors.primary, - ), - ), - ) - else if (shifts.isEmpty) - EmptyStateWidget( - message: emptyI18n.no_shifts_today, - actionLink: emptyI18n.find_shifts_cta, - onAction: () => - Modular.to.toShifts(initialTab: 'find'), - ) - else - Column( - children: shifts - .map( - (shift) => ShiftCard( - shift: shift, - compact: true, - ), - ) - .toList(), - ), - ], - ); - }, - ), - const SizedBox(height: UiConstants.space3), + const TodaysShiftsSection(), + const FullWidthDivider(), // Tomorrow's Shifts - BlocBuilder( - builder: (context, state) { - final shifts = state.tomorrowShifts; - return Column( - children: [ - SectionHeader(title: sectionsI18n.tomorrow), - if (shifts.isEmpty) - EmptyStateWidget( - message: emptyI18n.no_shifts_tomorrow, - ) - else - Column( - children: shifts - .map( - (shift) => ShiftCard( - shift: shift, - compact: true, - ), - ) - .toList(), - ), - ], - ); - }, - ), - const SizedBox(height: UiConstants.space3), + const TomorrowsShiftsSection(), + const FullWidthDivider(), // Recommended Shifts - SectionHeader(title: sectionsI18n.recommended_for_you), - BlocBuilder( - builder: (context, state) { - if (state.recommendedShifts.isEmpty) { - return EmptyStateWidget( - message: emptyI18n.no_recommended_shifts, - ); - } - return SizedBox( - height: 160, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: state.recommendedShifts.length, - clipBehavior: Clip.none, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.only( - right: UiConstants.space3, - ), - child: RecommendedShiftCard( - shift: state.recommendedShifts[index], - ), - ), - ), - ); - }, - ), - const SizedBox(height: UiConstants.space6), + const RecommendedShiftsSection(), + const FullWidthDivider(), // Benefits - BlocBuilder( - buildWhen: (previous, current) => - previous.benefits != current.benefits, - builder: (context, state) { - return BenefitsWidget(benefits: state.benefits); - }, - ), + const BenefitsSection(), const SizedBox(height: UiConstants.space6), ], ); diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/benefits_section.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/benefits_section.dart new file mode 100644 index 00000000..8f6d9fc2 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/benefits_section.dart @@ -0,0 +1,36 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart'; +import 'package:staff_home/src/presentation/widgets/worker/worker_benefits/benefits_widget.dart'; + +/// A widget that displays the benefits section. +/// +/// Shows available benefits for the worker with state management +/// via BLoC to rebuild only when benefits data changes. +class BenefitsSection extends StatelessWidget { + /// Creates a [BenefitsSection]. + const BenefitsSection({super.key}); + + @override + Widget build(BuildContext context) { + final i18n = t.staff.home.benefits; + + return BlocBuilder( + buildWhen: (previous, current) => + previous.benefits != current.benefits, + builder: (context, state) { + if (state.benefits.isEmpty) { + return const SizedBox.shrink(); + } + + return SectionLayout( + title: i18n.title, + child: BenefitsWidget(benefits: state.benefits), + ); + }, + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/full_width_divider.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/full_width_divider.dart new file mode 100644 index 00000000..46ca3ece --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/full_width_divider.dart @@ -0,0 +1,27 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A divider that extends to full screen width, breaking out of parent padding. +/// +/// This widget uses Transform.translate to shift the divider horizontally +/// to span the entire device width. +class FullWidthDivider extends StatelessWidget { + /// Creates a [FullWidthDivider]. + const FullWidthDivider({super.key}); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return Column( + children: [ + const SizedBox(height: UiConstants.space10), + Transform.translate( + offset: const Offset(UiConstants.space4, 0), + child: SizedBox(width: screenWidth, child: const Divider()), + ), + const SizedBox(height: UiConstants.space10), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/quick_actions_section.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/quick_actions_section.dart new file mode 100644 index 00000000..a137e28a --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/quick_actions_section.dart @@ -0,0 +1,48 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/quick_action_item.dart'; + +/// A widget that displays quick action buttons for common tasks. +/// +/// This section provides easy access to frequently used features like +/// finding shifts, setting availability, and viewing earnings. +class QuickActionsSection extends StatelessWidget { + /// Creates a [QuickActionsSection]. + const QuickActionsSection({super.key}); + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final quickI18n = t.staff.home.quick_actions; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: QuickActionItem( + icon: UiIcons.search, + label: quickI18n.find_shifts, + onTap: () => Modular.to.toShifts(), + ), + ), + Expanded( + child: QuickActionItem( + icon: UiIcons.calendar, + label: quickI18n.availability, + onTap: () => Modular.to.toAvailability(), + ), + ), + Expanded( + child: QuickActionItem( + icon: UiIcons.dollar, + label: quickI18n.earnings, + onTap: () => Modular.to.toPayments(), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart index 1dd260f2..0f85ce9d 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart @@ -23,47 +23,14 @@ class RecommendedShiftCard extends StatelessWidget { padding: const EdgeInsets.all(UiConstants.space4), decoration: BoxDecoration( color: UiColors.white, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: UiColors.border), - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.02), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border, width: 0.5), ), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Row( - children: [ - Text( - recI18n.act_now, - style: UiTypography.body3m.copyWith( - color: UiColors.textError, - ), - ), - const SizedBox(width: UiConstants.space2), - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: 2, - ), - decoration: BoxDecoration( - color: UiColors.tagInProgress, - borderRadius: UiConstants.radiusFull, - ), - child: Text( - recI18n.one_day, - style: UiTypography.body3m.textPrimary, - ), - ), - ], - ), - const SizedBox(height: UiConstants.space3), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -72,9 +39,7 @@ class RecommendedShiftCard extends StatelessWidget { height: UiConstants.space10, decoration: BoxDecoration( color: UiColors.tagInProgress, - borderRadius: BorderRadius.circular( - UiConstants.radiusBase, - ), + borderRadius: UiConstants.radiusLg, ), child: const Icon( UiIcons.calendar, @@ -89,6 +54,7 @@ class RecommendedShiftCard extends StatelessWidget { children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: UiConstants.space1, children: [ Flexible( child: Text( @@ -99,13 +65,13 @@ class RecommendedShiftCard extends StatelessWidget { ), Text( '\$${shift.hourlyRate}/h', - style: UiTypography.headline4m.textPrimary, + style: UiTypography.headline4b, ), ], ), - const SizedBox(height: 2), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: UiConstants.space1, children: [ Text( shift.clientName, @@ -113,7 +79,7 @@ class RecommendedShiftCard extends StatelessWidget { ), Text( '\$${shift.hourlyRate.toStringAsFixed(0)}/hr', - style: UiTypography.body3r.textSecondary, + style: UiTypography.body3r, ), ], ), diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shifts_section.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shifts_section.dart new file mode 100644 index 00000000..5929df8b --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shifts_section.dart @@ -0,0 +1,54 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/recommended_shift_card.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart'; + +/// A widget that displays recommended shifts section. +/// +/// Shows a horizontal scrolling list of shifts recommended for the worker +/// based on their profile and preferences. +class RecommendedShiftsSection extends StatelessWidget { + /// Creates a [RecommendedShiftsSection]. + const RecommendedShiftsSection({super.key}); + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final sectionsI18n = t.staff.home.sections; + final emptyI18n = t.staff.home.empty_states; + + return SectionLayout( + title: sectionsI18n.recommended_for_you, + child: BlocBuilder( + builder: (context, state) { + if (state.recommendedShifts.isEmpty) { + return EmptyStateWidget( + message: emptyI18n.no_recommended_shifts, + ); + } + return SizedBox( + height: 160, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: state.recommendedShifts.length, + clipBehavior: Clip.none, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only( + right: UiConstants.space3, + ), + child: RecommendedShiftCard( + shift: state.recommendedShifts[index], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/section_layout.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/section_layout.dart new file mode 100644 index 00000000..0654ffaf --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/section_layout.dart @@ -0,0 +1,51 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'section_header.dart'; + +/// A common layout widget for home page sections. +/// +/// Provides consistent structure with optional header and content area. +/// Use this to ensure all sections follow the same layout pattern. +class SectionLayout extends StatelessWidget { + /// The title of the section, displayed in the header. + final String? title; + + /// Optional action text/widget to display on the right side of the header. + final String? action; + + /// The main content of the section. + final Widget child; + + /// Optional padding for the content area. + /// Defaults to no padding. + final EdgeInsetsGeometry? contentPadding; + + /// Creates a [SectionLayout]. + const SectionLayout({ + this.title, + this.action, + required this.child, + this.contentPadding, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null) + Padding( + padding: contentPadding ?? EdgeInsets.zero, + child: SectionHeader( + title: title!, + action: action, + ), + ), + const SizedBox(height: UiConstants.space2), + child, + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/todays_shifts_section.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/todays_shifts_section.dart new file mode 100644 index 00000000..b2bec50c --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/todays_shifts_section.dart @@ -0,0 +1,68 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; + +import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart'; +import 'package:staff_home/src/presentation/widgets/shift_card.dart'; + +/// A widget that displays today's shifts section. +/// +/// Shows a list of shifts scheduled for today, with loading state +/// and empty state handling. +class TodaysShiftsSection extends StatelessWidget { + /// Creates a [TodaysShiftsSection]. + const TodaysShiftsSection({super.key}); + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final sectionsI18n = t.staff.home.sections; + final emptyI18n = t.staff.home.empty_states; + + return BlocBuilder( + builder: (context, state) { + final shifts = state.todayShifts; + return SectionLayout( + title: sectionsI18n.todays_shift, + action: shifts.isNotEmpty + ? sectionsI18n.scheduled_count( + count: shifts.length, + ) + : null, + child: state.status == HomeStatus.loading + ? const Center( + child: SizedBox( + height: UiConstants.space10, + width: UiConstants.space10, + child: CircularProgressIndicator( + color: UiColors.primary, + ), + ), + ) + : shifts.isEmpty + ? EmptyStateWidget( + message: emptyI18n.no_shifts_today, + actionLink: emptyI18n.find_shifts_cta, + onAction: () => + Modular.to.toShifts(initialTab: 'find'), + ) + : Column( + children: shifts + .map( + (shift) => ShiftCard( + shift: shift, + compact: true, + ), + ) + .toList(), + ), + ); + }, + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/tomorrows_shifts_section.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/tomorrows_shifts_section.dart new file mode 100644 index 00000000..ee372f54 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/tomorrows_shifts_section.dart @@ -0,0 +1,46 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart'; +import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart'; +import 'package:staff_home/src/presentation/widgets/shift_card.dart'; + +/// A widget that displays tomorrow's shifts section. +/// +/// Shows a list of shifts scheduled for tomorrow with empty state handling. +class TomorrowsShiftsSection extends StatelessWidget { + /// Creates a [TomorrowsShiftsSection]. + const TomorrowsShiftsSection({super.key}); + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final sectionsI18n = t.staff.home.sections; + final emptyI18n = t.staff.home.empty_states; + + return BlocBuilder( + builder: (context, state) { + final shifts = state.tomorrowShifts; + return SectionLayout( + title: sectionsI18n.tomorrow, + child: shifts.isEmpty + ? EmptyStateWidget( + message: emptyI18n.no_shifts_tomorrow, + ) + : Column( + children: shifts + .map( + (shift) => ShiftCard( + shift: shift, + compact: true, + ), + ) + .toList(), + ), + ); + }, + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/benefits_widget.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/benefits_widget.dart deleted file mode 100644 index 84031223..00000000 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/benefits_widget.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'dart:math' as math; - -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_modular/flutter_modular.dart'; -import 'package:krow_core/core.dart'; -import 'package:krow_domain/krow_domain.dart'; - -/// Widget for displaying staff benefits, using design system tokens. -class BenefitsWidget extends StatelessWidget { - /// The list of benefits to display. - final List benefits; - - /// Creates a [BenefitsWidget]. - const BenefitsWidget({ - required this.benefits, - super.key, - }); - - @override - Widget build(BuildContext context) { - final i18n = t.staff.home.benefits; - - if (benefits.isEmpty) { - return const SizedBox.shrink(); - } - - return Container( - padding: const EdgeInsets.all(UiConstants.space5), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border.withOpacity(0.5)), - boxShadow: [ - BoxShadow( - color: UiColors.black.withOpacity(0.03), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - i18n.title, - style: UiTypography.body1b.textPrimary, - ), - GestureDetector( - onTap: () => Modular.to.toBenefits(), - child: Row( - children: [ - Text( - i18n.view_all, - style: UiTypography.footnote2r.copyWith( - color: const Color(0xFF2563EB), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(width: 4), - const Icon( - UiIcons.chevronRight, - size: 14, - color: Color(0xFF2563EB), - ), - ], - ), - ), - ], - ), - const SizedBox(height: UiConstants.space6), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: benefits.map((Benefit benefit) { - return Expanded( - child: _BenefitItem( - label: benefit.title, - remaining: benefit.remainingHours, - total: benefit.entitlementHours, - used: benefit.usedHours, - color: const Color(0xFF2563EB), - ), - ); - }).toList(), - ), - ], - ), - ); - } -} - -class _BenefitItem extends StatelessWidget { - final String label; - final double remaining; - final double total; - final double used; - final Color color; - - const _BenefitItem({ - required this.label, - required this.remaining, - required this.total, - required this.used, - required this.color, - }); - - @override - Widget build(BuildContext context) { - final double progress = total > 0 ? (remaining / total) : 0.0; - - return Column( - children: [ - SizedBox( - width: 64, - height: 64, - child: CustomPaint( - painter: _CircularProgressPainter( - progress: progress, - color: color, - backgroundColor: const Color(0xFFE2E8F0), - strokeWidth: 5, - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '${remaining.toInt()}/${total.toInt()}', - style: UiTypography.body2b.textPrimary.copyWith( - fontSize: 12, - letterSpacing: -0.5, - ), - ), - Text( - 'hours', - style: UiTypography.footnote2r.textTertiary.copyWith( - fontSize: 8, - ), - ), - ], - ), - ), - ), - ), - const SizedBox(height: UiConstants.space3), - Text( - label, - style: UiTypography.footnote2r.textSecondary.copyWith( - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - ), - ], - ); - } -} - -class _CircularProgressPainter extends CustomPainter { - final double progress; - final Color color; - final Color backgroundColor; - final double strokeWidth; - - _CircularProgressPainter({ - required this.progress, - required this.color, - required this.backgroundColor, - required this.strokeWidth, - }); - - @override - void paint(Canvas canvas, Size size) { - final center = Offset(size.width / 2, size.height / 2); - final radius = (size.width - strokeWidth) / 2; - - final backgroundPaint = Paint() - ..color = backgroundColor - ..style = PaintingStyle.stroke - ..strokeWidth = strokeWidth; - canvas.drawCircle(center, radius, backgroundPaint); - - final progressPaint = Paint() - ..color = color - ..style = PaintingStyle.stroke - ..strokeWidth = strokeWidth - ..strokeCap = StrokeCap.round; - final sweepAngle = 2 * math.pi * progress; - canvas.drawArc( - Rect.fromCircle(center: center, radius: radius), - -math.pi / 2, - sweepAngle, - false, - progressPaint, - ); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => true; -} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefit_item.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefit_item.dart new file mode 100644 index 00000000..7e3bcaa3 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefit_item.dart @@ -0,0 +1,84 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'circular_progress_painter.dart'; + +/// A widget that displays a single benefit item with circular progress. +/// +/// Shows remaining hours, total hours, and a progress indicator. +class BenefitItem extends StatelessWidget { + /// The label of the benefit (e.g., "Sick Leave", "PTO"). + final String label; + + /// The remaining hours available. + final double remaining; + + /// The total hours entitled. + final double total; + + /// The hours already used. + final double used; + + /// The color for the progress indicator. + final Color color; + + /// Creates a [BenefitItem]. + const BenefitItem({ + required this.label, + required this.remaining, + required this.total, + required this.used, + required this.color, + super.key, + }); + + @override + Widget build(BuildContext context) { + final double progress = total > 0 ? (remaining / total) : 0.0; + + return Column( + children: [ + SizedBox( + width: 64, + height: 64, + child: CustomPaint( + painter: CircularProgressPainter( + progress: progress, + color: color, + backgroundColor: const Color(0xFFE2E8F0), + strokeWidth: 5, + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${remaining.toInt()}/${total.toInt()}', + style: UiTypography.body2b.textPrimary.copyWith( + fontSize: 12, + letterSpacing: -0.5, + ), + ), + Text( + 'hours', + style: UiTypography.footnote2r.textTertiary.copyWith( + fontSize: 8, + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: UiConstants.space3), + Text( + label, + style: UiTypography.footnote2r.textSecondary.copyWith( + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefits_view_all_link.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefits_view_all_link.dart new file mode 100644 index 00000000..98b13050 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefits_view_all_link.dart @@ -0,0 +1,39 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; + +/// A link widget that navigates to the full benefits page. +/// +/// Displays "View all" text with a chevron icon. +class BenefitsViewAllLink extends StatelessWidget { + /// Creates a [BenefitsViewAllLink]. + const BenefitsViewAllLink({super.key}); + + @override + Widget build(BuildContext context) { + final i18n = t.staff.home.benefits; + + return GestureDetector( + onTap: () => Modular.to.toBenefits(), + child: Row( + children: [ + Text( + i18n.view_all, + style: UiTypography.footnote2r.copyWith( + color: const Color(0xFF2563EB), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 4), + const Icon( + UiIcons.chevronRight, + size: 14, + color: Color(0xFF2563EB), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefits_widget.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefits_widget.dart new file mode 100644 index 00000000..af6f4076 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/benefits_widget.dart @@ -0,0 +1,59 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +import 'benefit_item.dart'; +import 'benefits_view_all_link.dart'; + +/// Widget for displaying staff benefits, using design system tokens. +/// +/// Shows a list of benefits with circular progress indicators +/// and a link to view all benefits. +class BenefitsWidget extends StatelessWidget { + /// The list of benefits to display. + final List benefits; + + /// Creates a [BenefitsWidget]. + const BenefitsWidget({required this.benefits, super.key}); + + @override + Widget build(BuildContext context) { + if (benefits.isEmpty) { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.all(UiConstants.space5), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border, width: 0.5), + ), + child: Column( + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + BenefitsViewAllLink(), + ], + ), + const SizedBox(height: UiConstants.space6), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: benefits.map((Benefit benefit) { + return Expanded( + child: BenefitItem( + label: benefit.title, + remaining: benefit.remainingHours, + total: benefit.entitlementHours, + used: benefit.usedHours, + color: const Color(0xFF2563EB), + ), + ); + }).toList(), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/circular_progress_painter.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/circular_progress_painter.dart new file mode 100644 index 00000000..38b38ed0 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/worker_benefits/circular_progress_painter.dart @@ -0,0 +1,59 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +/// A custom painter for drawing circular progress indicators. +/// +/// Draws a background circle and a progress arc on top of it. +class CircularProgressPainter extends CustomPainter { + /// The progress value (0.0 to 1.0). + final double progress; + + /// The color of the progress arc. + final Color color; + + /// The color of the background circle. + final Color backgroundColor; + + /// The width of the stroke. + final double strokeWidth; + + /// Creates a [CircularProgressPainter]. + CircularProgressPainter({ + required this.progress, + required this.color, + required this.backgroundColor, + required this.strokeWidth, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = (size.width - strokeWidth) / 2; + + // Draw background circle + final backgroundPaint = Paint() + ..color = backgroundColor + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth; + canvas.drawCircle(center, radius, backgroundPaint); + + // Draw progress arc + final progressPaint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round; + final sweepAngle = 2 * math.pi * progress; + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + -math.pi / 2, + sweepAngle, + false, + progressPaint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/backend/dataconnect/dataconnect.yaml b/backend/dataconnect/dataconnect.yaml index 39e01fdb..9e1775d6 100644 --- a/backend/dataconnect/dataconnect.yaml +++ b/backend/dataconnect/dataconnect.yaml @@ -1,5 +1,5 @@ specVersion: "v1" -serviceId: "krow-workforce-db" +serviceId: "krow-workforce-db-validation" location: "us-central1" schema: source: "./schema" @@ -7,7 +7,7 @@ schema: postgresql: database: "krow_db" cloudSql: - instanceId: "krow-sql" + instanceId: "krow-sql-validation" # schemaValidation: "STRICT" # STRICT mode makes Postgres schema match Data Connect exactly. # schemaValidation: "COMPATIBLE" # COMPATIBLE mode makes Postgres schema compatible with Data Connect. connectorDirs: ["./connector"] diff --git a/makefiles/dataconnect.mk b/makefiles/dataconnect.mk index 4c9f7ef9..9006a982 100644 --- a/makefiles/dataconnect.mk +++ b/makefiles/dataconnect.mk @@ -7,7 +7,7 @@ # make dataconnect-clean DC_ENV=validation # make dataconnect-generate-sdk DC_ENV=dev # -DC_ENV ?= dev +DC_ENV ?= validation DC_LOCATION ?= us-central1 DC_CONNECTOR_ID ?= example