fix: Update staff application to connect with data connect

This commit is contained in:
dhinesh-m24
2026-02-24 17:29:20 +05:30
40 changed files with 3035 additions and 199 deletions

View File

@@ -113,6 +113,25 @@ class HomeRepositoryImpl
});
}
@override
Future<List<Benefit>> getBenefits() async {
return _service.run(() async {
final staffId = await _service.getStaffId();
final response = await _service.connector
.listBenefitsDataByStaffId(staffId: staffId)
.execute();
return response.data.benefitsDatas.map((data) {
final plan = data.vendorBenefitPlan;
return Benefit(
title: plan.title,
entitlementHours: plan.total?.toDouble() ?? 0.0,
usedHours: data.current.toDouble(),
);
}).toList();
});
}
// Mappers specific to Home's Domain Entity 'Shift'
// Note: Home's 'Shift' entity might differ slightly from 'StaffPayment' Shift.

View File

@@ -17,4 +17,7 @@ abstract class HomeRepository {
/// Retrieves the current staff member's name.
Future<String?> getStaffName();
/// Retrieves the list of benefits for the staff member.
Future<List<Benefit>> getBenefits();
}

View File

@@ -34,15 +34,18 @@ class HomeCubit extends Cubit<HomeState> with BlocErrorHandler<HomeState> {
await handleError(
emit: emit,
action: () async {
// Fetch shifts, name, and profile completion status concurrently
final shiftsAndProfile = await Future.wait([
// Fetch shifts, name, benefits and profile completion status concurrently
final results = await Future.wait([
_getHomeShifts.call(),
_getPersonalInfoCompletion.call(),
_repository.getBenefits(),
_repository.getStaffName(),
]);
final homeResult = shiftsAndProfile[0] as HomeShifts;
final isProfileComplete = shiftsAndProfile[1] as bool;
final name = await _repository.getStaffName();
final homeResult = results[0] as HomeShifts;
final isProfileComplete = results[1] as bool;
final benefits = results[2] as List<Benefit>;
final name = results[3] as String?;
if (isClosed) return;
emit(
@@ -53,6 +56,7 @@ class HomeCubit extends Cubit<HomeState> with BlocErrorHandler<HomeState> {
recommendedShifts: homeResult.recommended,
staffName: name,
isProfileComplete: isProfileComplete,
benefits: benefits,
),
);
},

View File

@@ -11,6 +11,7 @@ class HomeState extends Equatable {
final bool isProfileComplete;
final String? staffName;
final String? errorMessage;
final List<Benefit> benefits;
const HomeState({
required this.status,
@@ -21,6 +22,7 @@ class HomeState extends Equatable {
this.isProfileComplete = false,
this.staffName,
this.errorMessage,
this.benefits = const [],
});
const HomeState.initial() : this(status: HomeStatus.initial);
@@ -34,6 +36,7 @@ class HomeState extends Equatable {
bool? isProfileComplete,
String? staffName,
String? errorMessage,
List<Benefit>? benefits,
}) {
return HomeState(
status: status ?? this.status,
@@ -44,6 +47,7 @@ class HomeState extends Equatable {
isProfileComplete: isProfileComplete ?? this.isProfileComplete,
staffName: staffName ?? this.staffName,
errorMessage: errorMessage ?? this.errorMessage,
benefits: benefits ?? this.benefits,
);
}
@@ -57,5 +61,6 @@ class HomeState extends Equatable {
isProfileComplete,
staffName,
errorMessage,
benefits,
];
}

View File

@@ -0,0 +1,407 @@
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:krow_domain/krow_domain.dart';
import 'package:core_localization/core_localization.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
import 'dart:math' as math;
/// Page displaying a detailed overview of the worker's benefits.
class BenefitsOverviewPage extends StatelessWidget {
/// Creates a [BenefitsOverviewPage].
const BenefitsOverviewPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<HomeCubit>.value(
value: Modular.get<HomeCubit>(),
child: Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: _buildAppBar(context),
body: BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
if (state.status == HomeStatus.loading ||
state.status == HomeStatus.initial) {
return const Center(child: CircularProgressIndicator());
}
if (state.status == HomeStatus.error) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space6),
child: Text(
state.errorMessage ?? t.staff.home.benefits.overview.subtitle,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
),
);
}
final benefits = state.benefits;
if (benefits.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space6),
child: Text(
t.staff.home.benefits.overview.subtitle,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
),
);
}
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]),
);
},
);
},
),
),
);
}
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: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
benefit.title,
style: UiTypography.body1b.textPrimary,
),
const Icon(UiIcons.info, size: 18, color: Color(0xFFE2E8F0)),
],
),
const SizedBox(height: 4),
Text(
_getSubtitle(benefit.title),
style: UiTypography.footnote2r.textSecondary,
),
],
),
),
],
),
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),
),
],
),
),
),
);
}
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

@@ -13,6 +13,7 @@ import 'package:staff_home/src/presentation/widgets/home_page/quick_action_item.
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';
/// The home page for the staff worker application.
///
@@ -212,6 +213,16 @@ class WorkerHomePage extends StatelessWidget {
},
),
const SizedBox(height: UiConstants.space6),
// Benefits
BlocBuilder<HomeCubit, HomeState>(
buildWhen: (previous, current) =>
previous.benefits != current.benefits,
builder: (context, state) {
return BenefitsWidget(benefits: state.benefits);
},
),
const SizedBox(height: UiConstants.space6),
],
),
),

View File

@@ -1,84 +1,90 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
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({super.key});
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.space4),
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: UiColors.border),
boxShadow: [
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border.withOpacity(0.5)),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.05),
blurRadius: 2,
offset: const Offset(0, 1),
color: UiColors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
children: <Widget>[
Text(
i18n.title,
style: UiTypography.title1m.textPrimary,
style: UiTypography.body1b.textPrimary,
),
GestureDetector(
onTap: () => Modular.to.pushNamed('/benefits'),
onTap: () => Modular.to.toBenefits(),
child: Row(
children: [
children: <Widget>[
Text(
i18n.view_all,
style: UiTypography.buttonL.textPrimary,
style: UiTypography.footnote2r.copyWith(
color: const Color(0xFF2563EB),
fontWeight: FontWeight.w500,
),
),
Icon(
const SizedBox(width: 4),
const Icon(
UiIcons.chevronRight,
size: UiConstants.space4,
color: UiColors.primary,
size: 14,
color: Color(0xFF2563EB),
),
],
),
),
],
),
const SizedBox(height: UiConstants.space4),
const SizedBox(height: UiConstants.space6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_BenefitItem(
label: i18n.items.sick_days,
current: 10,
total: 40,
color: UiColors.primary,
),
_BenefitItem(
label: i18n.items.vacation,
current: 40,
total: 40,
color: UiColors.primary,
),
_BenefitItem(
label: i18n.items.holidays,
current: 24,
total: 24,
color: UiColors.primary,
),
],
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(),
),
],
),
@@ -88,53 +94,64 @@ class BenefitsWidget extends StatelessWidget {
class _BenefitItem extends StatelessWidget {
final String label;
final double current;
final double remaining;
final double total;
final double used;
final Color color;
const _BenefitItem({
required this.label,
required this.current,
required this.remaining,
required this.total,
required this.used,
required this.color,
});
@override
Widget build(BuildContext context) {
final i18n = t.staff.home.benefits;
final double progress = total > 0 ? (remaining / total) : 0.0;
return Column(
children: [
children: <Widget>[
SizedBox(
width: UiConstants.space14,
height: UiConstants.space14,
width: 64,
height: 64,
child: CustomPaint(
painter: _CircularProgressPainter(
progress: current / total,
progress: progress,
color: color,
backgroundColor: UiColors.border,
strokeWidth: 4,
backgroundColor: const Color(0xFFE2E8F0),
strokeWidth: 5,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
Text(
'${current.toInt()}/${total.toInt()}',
style: UiTypography.body3m.textPrimary,
'${remaining.toInt()}/${total.toInt()}',
style: UiTypography.body2b.textPrimary.copyWith(
fontSize: 12,
letterSpacing: -0.5,
),
),
Text(
i18n.hours_label,
style: UiTypography.footnote1r.textTertiary,
'hours',
style: UiTypography.footnote2r.textTertiary.copyWith(
fontSize: 8,
),
),
],
),
),
),
),
const SizedBox(height: UiConstants.space2),
const SizedBox(height: UiConstants.space3),
Text(
label,
style: UiTypography.body3m.textSecondary,
style: UiTypography.footnote2r.textSecondary.copyWith(
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
);

View File

@@ -5,6 +5,7 @@ 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/domain/repositories/home_repository.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
import 'package:staff_home/src/presentation/pages/benefits_overview_page.dart';
import 'package:staff_home/src/presentation/pages/worker_home_page.dart';
/// The module for the staff home feature.
@@ -45,5 +46,9 @@ class StaffHomeModule extends Module {
StaffPaths.childRoute(StaffPaths.home, StaffPaths.home),
child: (BuildContext context) => const WorkerHomePage(),
);
r.child(
StaffPaths.childRoute(StaffPaths.home, StaffPaths.benefits),
child: (BuildContext context) => const BenefitsOverviewPage(),
);
}
}