feat: Refactor home cubit and add benefits overview functionality

- Updated import paths for home_cubit.dart to reflect new structure.
- Introduced BenefitsOverviewCubit to manage benefits overview page state.
- Created BenefitsOverviewState to handle loading, loaded, and error states for benefits.
- Implemented HomeCubit to manage home page state, including shifts and benefits.
- Added new widgets for benefits overview: BenefitCard, BenefitCardHeader, AccordionHistory, ComplianceBanner, StatChip, and BenefitsOverviewBody.
- Implemented custom painter for circular progress indicators.
- Enhanced UI components for displaying benefits and their statuses.
This commit is contained in:
Achintha Isuru
2026-03-03 21:15:04 -05:00
parent 4474a732c2
commit 85936e9b94
19 changed files with 614 additions and 418 deletions

View File

@@ -0,0 +1,42 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_home/src/domain/repositories/home_repository.dart';
part 'benefits_overview_state.dart';
/// Cubit to manage benefits overview page state.
class BenefitsOverviewCubit extends Cubit<BenefitsOverviewState>
with BlocErrorHandler<BenefitsOverviewState> {
final HomeRepository _repository;
BenefitsOverviewCubit({required HomeRepository repository})
: _repository = repository,
super(const BenefitsOverviewState.initial());
Future<void> loadBenefits() async {
if (isClosed) return;
emit(state.copyWith(status: BenefitsOverviewStatus.loading));
await handleError(
emit: emit,
action: () async {
final benefits = await _repository.getBenefits();
if (isClosed) return;
emit(
state.copyWith(
status: BenefitsOverviewStatus.loaded,
benefits: benefits,
),
);
},
onError: (String errorKey) {
if (isClosed) return state;
return state.copyWith(
status: BenefitsOverviewStatus.error,
errorMessage: errorKey,
);
},
);
}
}

View File

@@ -0,0 +1,33 @@
part of 'benefits_overview_cubit.dart';
enum BenefitsOverviewStatus { initial, loading, loaded, error }
class BenefitsOverviewState extends Equatable {
final BenefitsOverviewStatus status;
final List<Benefit> benefits;
final String? errorMessage;
const BenefitsOverviewState({
required this.status,
this.benefits = const [],
this.errorMessage,
});
const BenefitsOverviewState.initial()
: this(status: BenefitsOverviewStatus.initial);
BenefitsOverviewState copyWith({
BenefitsOverviewStatus? status,
List<Benefit>? benefits,
String? errorMessage,
}) {
return BenefitsOverviewState(
status: status ?? this.status,
benefits: benefits ?? this.benefits,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, benefits, errorMessage];
}

View File

@@ -1,12 +1,10 @@
import 'dart:math' as math;
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:staff_home/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; import 'package:staff_home/src/presentation/widgets/benefits_overview/benefits_overview_body.dart';
/// Page displaying a detailed overview of the worker's benefits. /// Page displaying a detailed overview of the worker's benefits.
class BenefitsOverviewPage extends StatelessWidget { class BenefitsOverviewPage extends StatelessWidget {
@@ -15,24 +13,29 @@ class BenefitsOverviewPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<HomeCubit>.value( return Scaffold(
value: Modular.get<HomeCubit>(), appBar: UiAppBar(
child: Scaffold( title: t.staff.home.benefits.overview.title,
backgroundColor: const Color(0xFFF8FAFC), subtitle: t.staff.home.benefits.overview.subtitle,
appBar: _buildAppBar(context), showBackButton: true,
body: BlocBuilder<HomeCubit, HomeState>( ),
body: BlocProvider<BenefitsOverviewCubit>(
create: (context) =>
Modular.get<BenefitsOverviewCubit>()..loadBenefits(),
child: BlocBuilder<BenefitsOverviewCubit, BenefitsOverviewState>(
builder: (context, state) { builder: (context, state) {
if (state.status == HomeStatus.loading || if (state.status == BenefitsOverviewStatus.loading ||
state.status == HomeStatus.initial) { state.status == BenefitsOverviewStatus.initial) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (state.status == HomeStatus.error) { if (state.status == BenefitsOverviewStatus.error) {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(UiConstants.space6), padding: const EdgeInsets.all(UiConstants.space6),
child: Text( child: Text(
state.errorMessage ?? t.staff.home.benefits.overview.subtitle, state.errorMessage ??
t.staff.home.benefits.overview.subtitle,
style: UiTypography.body1r.textSecondary, style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -40,8 +43,7 @@ class BenefitsOverviewPage extends StatelessWidget {
); );
} }
final benefits = state.benefits; if (state.benefits.isEmpty) {
if (benefits.isEmpty) {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(UiConstants.space6), padding: const EdgeInsets.all(UiConstants.space6),
@@ -54,400 +56,10 @@ class BenefitsOverviewPage extends StatelessWidget {
); );
} }
return ListView.builder( return BenefitsOverviewBody(benefits: state.benefits);
padding: const EdgeInsets.only(
left: UiConstants.space4,
right: UiConstants.space4,
top: UiConstants.space6,
bottom: 120,
),
itemCount: benefits.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: _BenefitCard(benefit: benefits[index]),
);
},
);
}, },
), ),
), ),
); );
} }
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconPrimary),
onPressed: () => Navigator.of(context).pop(),
),
centerTitle: true,
title: Column(
children: [
Text(
t.staff.home.benefits.overview.title,
style: UiTypography.title2b.textPrimary,
),
const SizedBox(height: 2),
Text(
t.staff.home.benefits.overview.subtitle,
style: UiTypography.footnote2r.textSecondary,
),
],
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border.withOpacity(0.5), height: 1),
),
);
}
}
class _BenefitCard extends StatelessWidget {
final Benefit benefit;
const _BenefitCard({required this.benefit});
@override
Widget build(BuildContext context) {
final bool isSickLeave = benefit.title.toLowerCase().contains('sick');
final bool isVacation = benefit.title.toLowerCase().contains('vacation');
final bool isHolidays = benefit.title.toLowerCase().contains('holiday');
final i18n = t.staff.home.benefits.overview;
return Container(
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border.withOpacity(0.5)),
boxShadow: [
BoxShadow(
color: UiColors.black.withOpacity(0.02),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_buildProgressCircle(),
const SizedBox(width: UiConstants.space4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
benefit.title,
style: UiTypography.body1b.textPrimary,
),
const SizedBox(height: 4),
Text(
_getSubtitle(benefit.title),
style: UiTypography.footnote2r.textSecondary,
),
const SizedBox(height: UiConstants.space4),
_buildStatsRow(),
],
),
),
],
),
const SizedBox(height: UiConstants.space6),
if (isSickLeave) ...[
_AccordionHistory(label: i18n.sick_leave_history),
const SizedBox(height: UiConstants.space6),
],
if (isVacation || isHolidays) ...[
_buildComplianceBanner(i18n.compliance_banner),
const SizedBox(height: UiConstants.space6),
],
SizedBox(
width: double.infinity,
child: UiButton.primary(
text: i18n.request_payment(benefit: benefit.title),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0038A8),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
// TODO: Implement payment request
UiSnackbar.show(context, message: i18n.request_submitted(benefit: benefit.title), type: UiSnackbarType.success);
},
),
),
],
),
);
}
Widget _buildProgressCircle() {
final double progress = benefit.entitlementHours > 0
? (benefit.remainingHours / benefit.entitlementHours)
: 0.0;
final bool isSickLeave = benefit.title.toLowerCase().contains('sick');
final Color circleColor = isSickLeave ? const Color(0xFF2563EB) : const Color(0xFF10B981);
return SizedBox(
width: 72,
height: 72,
child: CustomPaint(
painter: _CircularProgressPainter(
progress: progress,
color: circleColor,
backgroundColor: const Color(0xFFE2E8F0),
strokeWidth: 6,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${benefit.remainingHours.toInt()}/${benefit.entitlementHours.toInt()}',
style: UiTypography.body2b.textPrimary.copyWith(fontSize: 14),
),
Text(
t.client_billing.hours_suffix,
style: UiTypography.footnote2r.textTertiary.copyWith(fontSize: 9),
),
],
),
),
),
);
}
Widget _buildStatsRow() {
final i18n = t.staff.home.benefits.overview;
return Row(
children: [
_buildStatChip(
i18n.entitlement,
'${benefit.entitlementHours.toInt()}',
),
const SizedBox(width: 8),
_buildStatChip(
i18n.used,
'${benefit.usedHours.toInt()}',
),
const SizedBox(width: 8),
_buildStatChip(
i18n.remaining,
'${benefit.remainingHours.toInt()}',
),
],
);
}
Widget _buildStatChip(String label, String value) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: UiTypography.footnote2r.textTertiary.copyWith(
fontSize: 10,
),
),
Text(
'$value ${t.staff.home.benefits.overview.hours}',
style: UiTypography.footnote2b.textPrimary.copyWith(
fontSize: 12,
),
),
],
),
);
}
String _getSubtitle(String title) {
final i18n = t.staff.home.benefits.overview;
if (title.toLowerCase().contains('sick')) {
return i18n.sick_leave_subtitle;
} else if (title.toLowerCase().contains('vacation')) {
return i18n.vacation_subtitle;
} else if (title.toLowerCase().contains('holiday')) {
return i18n.holidays_subtitle;
}
return '';
}
Widget _buildComplianceBanner(String text) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFECFDF5),
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(UiIcons.checkCircle, size: 16, color: Color(0xFF10B981)),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: UiTypography.footnote1r.copyWith(
color: const Color(0xFF065F46),
fontSize: 11,
),
),
),
],
),
);
}
}
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;
}
class _AccordionHistory extends StatefulWidget {
final String label;
const _AccordionHistory({required this.label});
@override
State<_AccordionHistory> createState() => _AccordionHistoryState();
}
class _AccordionHistoryState extends State<_AccordionHistory> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 1, color: Color(0xFFE2E8F0)),
InkWell(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.label,
style: UiTypography.footnote2b.textSecondary.copyWith(
letterSpacing: 0.5,
fontSize: 11,
),
),
Icon(
_isExpanded ? UiIcons.chevronUp : UiIcons.chevronDown,
size: 16,
color: UiColors.iconSecondary,
),
],
),
),
),
if (_isExpanded) ...[
_buildHistoryItem('1 Jan, 2024', 'Pending', const Color(0xFFF1F5F9), const Color(0xFF64748B)),
const SizedBox(height: 14),
_buildHistoryItem('28 Jan, 2024', 'Submitted', const Color(0xFFECFDF5), const Color(0xFF10B981)),
const SizedBox(height: 14),
_buildHistoryItem('5 Feb, 2024', 'Submitted', const Color(0xFFECFDF5), const Color(0xFF10B981)),
const SizedBox(height: 14),
_buildHistoryItem('28 Jan, 2024', 'Submitted', const Color(0xFFECFDF5), const Color(0xFF10B981)),
const SizedBox(height: 14),
_buildHistoryItem('5 Feb, 2024', 'Submitted', const Color(0xFFECFDF5), const Color(0xFF10B981)),
const SizedBox(height: 4),
]
],
);
}
Widget _buildHistoryItem(String date, String status, Color bgColor, Color textColor) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
date,
style: UiTypography.footnote1r.textSecondary.copyWith(
fontSize: 12,
color: const Color(0xFF64748B),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: status == 'Pending' ? Border.all(color: const Color(0xFFE2E8F0)) : null,
),
child: Text(
status,
style: UiTypography.footnote2m.copyWith(
color: textColor,
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
} }

View File

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
import 'package:staff_home/src/presentation/widgets/home_page/benefits_section.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/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/home_header.dart';

View File

@@ -0,0 +1,140 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Widget displaying collapsible benefit history.
class AccordionHistory extends StatefulWidget {
/// The label for the accordion header.
final String label;
/// Creates an [AccordionHistory].
const AccordionHistory({required this.label, super.key});
@override
State<AccordionHistory> createState() => _AccordionHistoryState();
}
class _AccordionHistoryState extends State<AccordionHistory> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 1, color: Color(0xFFE2E8F0)),
InkWell(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.label,
style: UiTypography.footnote2b.textSecondary.copyWith(
letterSpacing: 0.5,
fontSize: 11,
),
),
Icon(
_isExpanded ? UiIcons.chevronUp : UiIcons.chevronDown,
size: 16,
color: UiColors.iconSecondary,
),
],
),
),
),
if (_isExpanded) ...[
_HistoryItem(
date: '1 Jan, 2024',
status: 'Pending',
bgColor: const Color(0xFFF1F5F9),
textColor: const Color(0xFF64748B),
),
const SizedBox(height: 14),
_HistoryItem(
date: '28 Jan, 2024',
status: 'Submitted',
bgColor: const Color(0xFFECFDF5),
textColor: const Color(0xFF10B981),
),
const SizedBox(height: 14),
_HistoryItem(
date: '5 Feb, 2024',
status: 'Submitted',
bgColor: const Color(0xFFECFDF5),
textColor: const Color(0xFF10B981),
),
const SizedBox(height: 14),
_HistoryItem(
date: '28 Jan, 2024',
status: 'Submitted',
bgColor: const Color(0xFFECFDF5),
textColor: const Color(0xFF10B981),
),
const SizedBox(height: 14),
_HistoryItem(
date: '5 Feb, 2024',
status: 'Submitted',
bgColor: const Color(0xFFECFDF5),
textColor: const Color(0xFF10B981),
),
const SizedBox(height: 4),
],
],
);
}
}
class _HistoryItem extends StatelessWidget {
final String date;
final String status;
final Color bgColor;
final Color textColor;
const _HistoryItem({
required this.date,
required this.status,
required this.bgColor,
required this.textColor,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
date,
style: UiTypography.footnote1r.textSecondary.copyWith(
fontSize: 12,
color: const Color(0xFF64748B),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: status == 'Pending'
? Border.all(color: const Color(0xFFE2E8F0))
: null,
),
child: Text(
status,
style: UiTypography.footnote2m.copyWith(
color: textColor,
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,77 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_home/src/presentation/widgets/benefits_overview/accordion_history.dart';
import 'package:staff_home/src/presentation/widgets/benefits_overview/benefit_card_header.dart';
import 'package:staff_home/src/presentation/widgets/benefits_overview/compliance_banner.dart';
/// Card widget displaying detailed benefit information.
class BenefitCard extends StatelessWidget {
/// The benefit to display.
final Benefit benefit;
/// Creates a [BenefitCard].
const BenefitCard({required this.benefit, super.key});
@override
Widget build(BuildContext context) {
final bool isSickLeave = benefit.title.toLowerCase().contains('sick');
final bool isVacation = benefit.title.toLowerCase().contains('vacation');
final bool isHolidays = benefit.title.toLowerCase().contains('holiday');
final i18n = t.staff.home.benefits.overview;
return Container(
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border.withOpacity(0.5)),
boxShadow: [
BoxShadow(
color: UiColors.black.withOpacity(0.02),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BenefitCardHeader(benefit: benefit),
const SizedBox(height: UiConstants.space6),
if (isSickLeave) ...[
AccordionHistory(label: i18n.sick_leave_history),
const SizedBox(height: UiConstants.space6),
],
if (isVacation || isHolidays) ...[
ComplianceBanner(text: i18n.compliance_banner),
const SizedBox(height: UiConstants.space6),
],
SizedBox(
width: double.infinity,
child: UiButton.primary(
text: i18n.request_payment(benefit: benefit.title),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0038A8),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
// TODO: Implement payment request
UiSnackbar.show(
context,
message: i18n.request_submitted(benefit: benefit.title),
type: UiSnackbarType.success,
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,118 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_home/src/presentation/widgets/benefits_overview/circular_progress_painter.dart';
import 'package:staff_home/src/presentation/widgets/benefits_overview/stat_chip.dart';
/// Header section of a benefit card showing progress circle, title, and stats.
class BenefitCardHeader extends StatelessWidget {
/// The benefit to display.
final Benefit benefit;
/// Creates a [BenefitCardHeader].
const BenefitCardHeader({required this.benefit, super.key});
@override
Widget build(BuildContext context) {
final i18n = t.staff.home.benefits.overview;
return Row(
children: [
_buildProgressCircle(),
const SizedBox(width: UiConstants.space4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
benefit.title,
style: UiTypography.body1b.textPrimary,
),
const SizedBox(height: 4),
Text(
_getSubtitle(benefit.title),
style: UiTypography.footnote2r.textSecondary,
),
const SizedBox(height: UiConstants.space4),
_buildStatsRow(i18n),
],
),
),
],
);
}
Widget _buildProgressCircle() {
final double progress = benefit.entitlementHours > 0
? (benefit.remainingHours / benefit.entitlementHours)
: 0.0;
final bool isSickLeave = benefit.title.toLowerCase().contains('sick');
final Color circleColor =
isSickLeave ? const Color(0xFF2563EB) : const Color(0xFF10B981);
return SizedBox(
width: 72,
height: 72,
child: CustomPaint(
painter: CircularProgressPainter(
progress: progress,
color: circleColor,
backgroundColor: const Color(0xFFE2E8F0),
strokeWidth: 6,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${benefit.remainingHours.toInt()}/${benefit.entitlementHours.toInt()}',
style: UiTypography.body2b.textPrimary.copyWith(fontSize: 14),
),
Text(
t.client_billing.hours_suffix,
style: UiTypography.footnote2r.textTertiary.copyWith(
fontSize: 9,
),
),
],
),
),
),
);
}
Widget _buildStatsRow(dynamic i18n) {
return Row(
children: [
StatChip(
label: i18n.entitlement,
value: '${benefit.entitlementHours.toInt()}',
),
const SizedBox(width: 8),
StatChip(
label: i18n.used,
value: '${benefit.usedHours.toInt()}',
),
const SizedBox(width: 8),
StatChip(
label: i18n.remaining,
value: '${benefit.remainingHours.toInt()}',
),
],
);
}
String _getSubtitle(String title) {
final i18n = t.staff.home.benefits.overview;
if (title.toLowerCase().contains('sick')) {
return i18n.sick_leave_subtitle;
} else if (title.toLowerCase().contains('vacation')) {
return i18n.vacation_subtitle;
} else if (title.toLowerCase().contains('holiday')) {
return i18n.holidays_subtitle;
}
return '';
}
}

View File

@@ -0,0 +1,32 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_home/src/presentation/widgets/benefits_overview/benefit_card.dart';
/// Body widget displaying a list of benefit cards.
class BenefitsOverviewBody extends StatelessWidget {
/// The list of benefits to display.
final List<Benefit> benefits;
/// Creates a [BenefitsOverviewBody].
const BenefitsOverviewBody({required this.benefits, super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.only(
left: UiConstants.space4,
right: UiConstants.space4,
top: UiConstants.space6,
bottom: 120,
),
itemCount: benefits.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: BenefitCard(benefit: benefits[index]),
);
},
);
}
}

View File

@@ -0,0 +1,47 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
/// Custom painter for circular progress indicators.
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;
}

View File

@@ -0,0 +1,45 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Widget displaying a compliance information banner.
class ComplianceBanner extends StatelessWidget {
/// The text to display in the banner.
final String text;
/// Creates a [ComplianceBanner].
const ComplianceBanner({
required this.text,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFECFDF5),
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
UiIcons.checkCircle,
size: 16,
color: Color(0xFF10B981),
),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: UiTypography.footnote1r.copyWith(
color: const Color(0xFF065F46),
fontSize: 11,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Widget displaying a single statistic chip.
class StatChip extends StatelessWidget {
/// The label for the stat (e.g., "Entitlement", "Used", "Remaining").
final String label;
/// The numeric value to display.
final String value;
/// Creates a [StatChip].
const StatChip({
required this.label,
required this.value,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: UiTypography.footnote2r.textTertiary.copyWith(fontSize: 10),
),
Text(
'$value ${t.staff.home.benefits.overview.hours}',
style: UiTypography.footnote2b.textPrimary.copyWith(fontSize: 12),
),
],
),
);
}
}

View File

@@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
import 'package:staff_home/src/presentation/widgets/home_page/section_layout.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'; import 'package:staff_home/src/presentation/widgets/worker/worker_benefits/benefits_widget.dart';

View File

@@ -80,7 +80,7 @@ class RecommendedShiftCard extends StatelessWidget {
), ),
Text( Text(
'\$${shift.hourlyRate.toStringAsFixed(0)}/hr', '\$${shift.hourlyRate.toStringAsFixed(0)}/hr',
style: UiTypography.body3r, style: UiTypography.body3r.textSecondary,
), ),
], ],
), ),

View File

@@ -3,7 +3,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; import 'package:staff_home/src/presentation/blocs/home/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/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/recommended_shift_card.dart';
import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart'; import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart';

View File

@@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; import 'package:staff_home/src/presentation/blocs/home/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/empty_state_widget.dart';
import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart'; import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart';
import 'package:staff_home/src/presentation/widgets/shift_card.dart'; import 'package:staff_home/src/presentation/widgets/shift_card.dart';

View File

@@ -2,7 +2,7 @@ import 'package:core_localization/core_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; import 'package:staff_home/src/presentation/blocs/home/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/empty_state_widget.dart';
import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart'; import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart';
import 'package:staff_home/src/presentation/widgets/shift_card.dart'; import 'package:staff_home/src/presentation/widgets/shift_card.dart';

View File

@@ -4,7 +4,8 @@ import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:staff_home/src/data/repositories/home_repository_impl.dart'; import 'package:staff_home/src/data/repositories/home_repository_impl.dart';
import 'package:staff_home/src/domain/repositories/home_repository.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; import 'package:staff_home/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart';
import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
import 'package:staff_home/src/presentation/pages/benefits_overview_page.dart'; import 'package:staff_home/src/presentation/pages/benefits_overview_page.dart';
import 'package:staff_home/src/presentation/pages/worker_home_page.dart'; import 'package:staff_home/src/presentation/pages/worker_home_page.dart';
@@ -31,13 +32,18 @@ class StaffHomeModule extends Module {
), ),
); );
// Presentation layer - Cubit // Presentation layer - Cubits
i.addSingleton( i.addSingleton(
() => HomeCubit( () => HomeCubit(
repository: i.get<HomeRepository>(), repository: i.get<HomeRepository>(),
getProfileCompletion: i.get<GetProfileCompletionUseCase>(), getProfileCompletion: i.get<GetProfileCompletionUseCase>(),
), ),
); );
// Cubit for benefits overview page
i.addLazySingleton(
() => BenefitsOverviewCubit(repository: i.get<HomeRepository>()),
);
} }
@override @override