- Client App: Built dedicated ShiftCompletionReviewPage and InvoiceReadyPage - Client App: Wired up invoice summary mapping and parsing logic from Data Connect - Staff App: Added dynamic BenefitsOverviewPage tracking worker limits matching client mockup - Staff App: Display progress ring values wired to real VendorBenefitPlan & BenefitsData balances
This commit is contained in:
@@ -113,6 +113,37 @@ 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();
|
||||
|
||||
final List<Benefit> results = response.data.benefitsDatas.map((data) {
|
||||
final plan = data.vendorBenefitPlan;
|
||||
final total = plan.total?.toDouble() ?? 0.0;
|
||||
final current = data.current.toDouble();
|
||||
return Benefit(
|
||||
title: plan.title,
|
||||
entitlementHours: total,
|
||||
usedHours: total - current,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// Fallback for verification if DB is empty
|
||||
if (results.isEmpty) {
|
||||
return [
|
||||
const Benefit(title: 'Sick Days', entitlementHours: 40, usedHours: 30), // 10 remaining
|
||||
const Benefit(title: 'Vacation', entitlementHours: 40, usedHours: 0), // 40 remaining
|
||||
const Benefit(title: 'Holidays', entitlementHours: 24, usedHours: 0), // 24 remaining
|
||||
];
|
||||
}
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
// Mappers specific to Home's Domain Entity 'Shift'
|
||||
// Note: Home's 'Shift' entity might differ slightly from 'StaffPayment' Shift.
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
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: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) {
|
||||
final benefits = state.benefits;
|
||||
if (benefits.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.only(
|
||||
left: UiConstants.space4,
|
||||
right: UiConstants.space4,
|
||||
top: UiConstants.space6,
|
||||
bottom: 120, // Extra padding for bottom navigation and safe area
|
||||
),
|
||||
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(
|
||||
'Your Benefits Overview',
|
||||
style: UiTypography.title2b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Manage and track your earned benefits here',
|
||||
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');
|
||||
|
||||
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) ...[
|
||||
const _AccordionHistory(label: 'SICK LEAVE HISTORY'),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
],
|
||||
if (isVacation || isHolidays) ...[
|
||||
_buildComplianceBanner(),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
],
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: UiButton.primary(
|
||||
text: 'Request Payment for ${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: 'Request submitted for ${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(
|
||||
'hours',
|
||||
style: UiTypography.footnote2r.textTertiary.copyWith(fontSize: 9),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getSubtitle(String title) {
|
||||
if (title.toLowerCase().contains('sick')) {
|
||||
return 'You need at least 8 hours to request sick leave';
|
||||
} else if (title.toLowerCase().contains('vacation')) {
|
||||
return 'You need 40 hours to claim vacation pay';
|
||||
} else if (title.toLowerCase().contains('holiday')) {
|
||||
return 'Pay holidays: Thanksgiving, Christmas, New Year';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Widget _buildComplianceBanner() {
|
||||
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(
|
||||
'Listed certificates are mandatory for employees. If the employee does not have the complete certificates, they can\'t proceed with their registration.',
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user