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
This commit is contained in:
@@ -20,6 +20,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
|
||||
@override
|
||||
Future<bool> getProfileCompletion() async {
|
||||
return true;
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<HomeCubit>.value(
|
||||
value: Modular.get<HomeCubit>()..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<HomeCubit, HomeState>(
|
||||
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<HomeCubit, HomeState>(
|
||||
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<HomeCubit, HomeState>(
|
||||
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<HomeCubit, HomeState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.benefits != current.benefits,
|
||||
builder: (context, state) {
|
||||
return BenefitsWidget(benefits: state.benefits);
|
||||
},
|
||||
),
|
||||
const BenefitsSection(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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<HomeCubit, HomeState>(
|
||||
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),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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<HomeCubit, HomeState>(
|
||||
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],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<HomeCubit, HomeState>(
|
||||
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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<HomeCubit, HomeState>(
|
||||
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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Benefit> 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>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
i18n.title,
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => Modular.to.toBenefits(),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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;
|
||||
}
|
||||
@@ -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: <Widget>[
|
||||
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: <Widget>[
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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: <Widget>[
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Benefit> 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: <Widget>[
|
||||
const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user