feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
import 'package:injectable/injectable.dart';
import 'package:krow/features/profile/benefits/domain/benefits_repository.dart';
import 'package:krow/features/profile/benefits/domain/entities/benefit_entity.dart';
import 'package:krow/features/profile/benefits/domain/entities/benefit_record_entity.dart';
@Injectable(as: BenefitsRepository)
class BenefitsRepositoryImpl implements BenefitsRepository {
static final _benefitsMock = [
BenefitEntity(
name: 'Sick Leave',
requirement: 'You need at least 8 hours to request sick leave',
requiredHours: 40,
currentHours: 10,
isClaimed: false,
info: 'Listed certificates are mandatory for employees. If the employee '
'does not have the complete certificates, they cant proceed with '
'their registration.',
history: [
BenefitRecordEntity(
createdAt: DateTime(2024, 6, 14),
status: RecordStatus.submitted,
),
BenefitRecordEntity(
createdAt: DateTime(2023, 6, 5),
status: RecordStatus.submitted,
),
BenefitRecordEntity(
createdAt: DateTime(2019, 6, 4),
status: RecordStatus.submitted,
),
BenefitRecordEntity(
createdAt: DateTime(2018, 6, 1),
status: RecordStatus.submitted,
),
BenefitRecordEntity(
createdAt: DateTime(2017, 6, 24),
status: RecordStatus.submitted,
),
BenefitRecordEntity(
createdAt: DateTime(2016, 6, 15),
status: RecordStatus.submitted,
),
BenefitRecordEntity(
createdAt: DateTime(2015, 6, 6),
status: RecordStatus.submitted,
),
],
),
const BenefitEntity(
name: 'Vacation',
requirement: 'You need 40 hours to claim vacation pay',
requiredHours: 40,
currentHours: 40,
isClaimed: false,
info: 'Listed certificates are mandatory for employees. If the employee '
'does not have the complete certificates, they cant proceed with '
'their registration.',
history: [],
),
const BenefitEntity(
name: 'Holidays',
requirement: 'Pay holidays: Thanksgiving, Christmas, New Year',
requiredHours: 24,
currentHours: 1,
isClaimed: false,
info: 'Listed certificates are mandatory for employees. If the employee '
'does not have the complete certificates, they cant proceed with '
'their registration.',
history: [],
),
];
@override
Future<List<BenefitEntity>> getStaffBenefits() async {
await Future.delayed(const Duration(milliseconds: 500));
return _benefitsMock;
}
@override
Future<BenefitEntity?> requestBenefit({
required BenefitEntity benefit,
}) async {
if (benefit.currentHours != benefit.requiredHours || benefit.isClaimed) {
return null;
}
await Future.delayed(const Duration(seconds: 1));
return benefit.copyWith(isClaimed: true);
}
}

View File

@@ -0,0 +1,7 @@
import 'package:krow/features/profile/benefits/domain/entities/benefit_entity.dart';
abstract interface class BenefitsRepository {
Future<List<BenefitEntity>> getStaffBenefits();
Future<BenefitEntity?> requestBenefit({required BenefitEntity benefit});
}

View File

@@ -0,0 +1,51 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/features/profile/benefits/domain/benefits_repository.dart';
import 'package:krow/features/profile/benefits/domain/entities/benefit_entity.dart';
part 'benefits_event.dart';
part 'benefits_state.dart';
class BenefitsBloc extends Bloc<BenefitsEvent, BenefitsState> {
BenefitsBloc() : super(const BenefitsState()) {
on<InitializeBenefits>((event, emit) async {
emit(state.copyWith(status: StateStatus.loading));
final benefits = await _repository.getStaffBenefits();
emit(state.copyWith(status: StateStatus.idle, benefits: benefits));
});
on<SendBenefitRequest>((event, emit) async {
emit(state.copyWith(status: StateStatus.loading));
final result = await _repository.requestBenefit(benefit: event.benefit);
int index = -1;
List<BenefitEntity>? updatedBenefits;
if (result != null) {
index = state.benefits.indexWhere(
(benefit) => benefit.name == result.name,
);
if (index >= 0) {
updatedBenefits = List.from(state.benefits)..[index] = result;
}
}
emit(
state.copyWith(
benefits: updatedBenefits,
status: StateStatus.idle,
),
);
event.requestCompleter.complete(result ?? event.benefit);
});
}
final BenefitsRepository _repository = getIt<BenefitsRepository>();
}

View File

@@ -0,0 +1,20 @@
part of 'benefits_bloc.dart';
@immutable
sealed class BenefitsEvent {
const BenefitsEvent();
}
class InitializeBenefits extends BenefitsEvent {
const InitializeBenefits();
}
class SendBenefitRequest extends BenefitsEvent {
const SendBenefitRequest({
required this.benefit,
required this.requestCompleter,
});
final BenefitEntity benefit;
final Completer<BenefitEntity> requestCompleter;
}

View File

@@ -0,0 +1,26 @@
part of 'benefits_bloc.dart';
@immutable
class BenefitsState {
const BenefitsState({
this.status = StateStatus.idle,
this.benefits = const [],
this.exception,
});
final StateStatus status;
final List<BenefitEntity> benefits;
final Exception? exception;
BenefitsState copyWith({
StateStatus? status,
List<BenefitEntity>? benefits,
Exception? exception,
}) {
return BenefitsState(
status: status ?? this.status,
benefits: benefits ?? this.benefits,
exception: exception,
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/foundation.dart';
import 'package:krow/features/profile/benefits/domain/entities/benefit_record_entity.dart';
@immutable
class BenefitEntity {
const BenefitEntity({
required this.name,
required this.requirement,
required this.requiredHours,
required this.currentHours,
required this.isClaimed,
required this.info,
required this.history,
});
final String name;
final String requirement;
final int requiredHours;
final int currentHours;
final bool isClaimed;
final String info;
final List<BenefitRecordEntity> history;
double get progress {
final progress = currentHours / requiredHours;
return progress > 1 ? 1 : progress;
}
BenefitEntity copyWith({
String? name,
String? requirement,
int? requiredHours,
int? currentHours,
bool? isClaimed,
String? info,
List<BenefitRecordEntity>? history,
}) {
return BenefitEntity(
name: name ?? this.name,
requirement: requirement ?? this.requirement,
requiredHours: requiredHours ?? this.requiredHours,
currentHours: currentHours ?? this.currentHours,
isClaimed: isClaimed ?? this.isClaimed,
info: info ?? this.info,
history: history ?? this.history,
);
}
}

View File

@@ -0,0 +1,11 @@
import 'package:flutter/foundation.dart';
@immutable
class BenefitRecordEntity {
const BenefitRecordEntity({required this.createdAt, required this.status});
final DateTime createdAt;
final RecordStatus status;
}
enum RecordStatus { pending, submitted }

View File

@@ -0,0 +1,95 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_loading_overlay.dart';
import 'package:krow/features/profile/benefits/domain/bloc/benefits_bloc.dart';
import 'package:krow/features/profile/benefits/presentation/widgets/benefit_card_widget.dart';
@RoutePage()
class BenefitsScreen extends StatefulWidget implements AutoRouteWrapper {
const BenefitsScreen({super.key});
@override
State<BenefitsScreen> createState() => _BenefitsScreenState();
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider<BenefitsBloc>(
create: (context) => BenefitsBloc()..add(const InitializeBenefits()),
child: this,
);
}
}
class _BenefitsScreenState extends State<BenefitsScreen> {
final OverlayPortalController _controller = OverlayPortalController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: KwLoadingOverlay(
controller: _controller,
child: BlocListener<BenefitsBloc, BenefitsState>(
listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) {
if (state.status == StateStatus.loading) {
_controller.show();
} else {
_controller.hide();
}
},
child: CustomScrollView(
primary: false,
slivers: [
SliverList.list(
children: [
KwAppBar(
titleText: 'your_benefits_overview'.tr(),
showNotification: true,
),
const Gap(16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'manage_and_track_benefits'.tr(),
style: AppTextStyles.bodySmallReg.copyWith(
color: AppColors.blackGray,
),
textAlign: TextAlign.start,
),
),
],
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 120),
sliver: BlocBuilder<BenefitsBloc, BenefitsState>(
buildWhen: (current, previous) =>
current.benefits != previous.benefits,
builder: (context, state) {
return SliverList.separated(
itemCount: state.benefits.length,
separatorBuilder: (context, index) {
return const SizedBox(height: 12);
},
itemBuilder: (context, index) {
return BenefitCardWidget(
benefit: state.benefits[index],
);
},
);
},
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,253 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/features/profile/benefits/domain/bloc/benefits_bloc.dart';
import 'package:krow/features/profile/benefits/domain/entities/benefit_entity.dart';
import 'package:krow/features/profile/benefits/presentation/widgets/benefit_history_widget.dart';
class BenefitCardWidget extends StatefulWidget {
const BenefitCardWidget({super.key, required this.benefit});
final BenefitEntity benefit;
@override
State<BenefitCardWidget> createState() => _BenefitCardWidgetState();
}
class _BenefitCardWidgetState extends State<BenefitCardWidget>
with TickerProviderStateMixin {
late final AnimationController _animationController;
late BenefitEntity _benefit = widget.benefit;
Completer<BenefitEntity>? _requestCompleter;
double _progress = 0;
bool _isReady = false;
@override
void initState() {
_progress = widget.benefit.progress;
_isReady = _progress == 1;
super.initState();
_animationController = AnimationController(vsync: this);
_animationController.animateTo(
_progress,
duration: const Duration(seconds: 1),
curve: Curves.easeOut,
);
}
Future<void> _handleRequestPress() async {
_requestCompleter?.completeError(Exception('previous_aborted'.tr()));
final completer = _requestCompleter = Completer<BenefitEntity>();
this.context.read<BenefitsBloc>().add(
SendBenefitRequest(
benefit: _benefit,
requestCompleter: completer,
),
);
final benefit = await completer.future;
if (!benefit.isClaimed) return;
setState(() {
_progress = 0;
_isReady = false;
_benefit = _benefit.copyWith(currentHours: 0);
});
await _animationController.animateTo(
_progress,
duration: const Duration(seconds: 1),
curve: Curves.easeOut,
);
final context = this.context;
if (!context.mounted) return;
await KwDialog.show(
context: context,
icon: Assets.images.icons.like,
state: KwDialogState.positive,
title: 'request_submitted'.tr(),
message: 'request_submitted_message'.tr(args: [_benefit.name.toLowerCase()]),
primaryButtonLabel: 'back_to_profile'.tr(),
onPrimaryButtonPressed: (dialogContext) {
Navigator.maybePop(dialogContext);
context.maybePop();
},
);
}
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
color: AppColors.grayPrimaryFrame,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Stack(
alignment: AlignmentDirectional.center,
children: [
AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
return CircularProgressIndicator(
constraints:
BoxConstraints.tight(const Size.square(90)),
strokeWidth: 8,
strokeCap: StrokeCap.round,
backgroundColor: AppColors.bgColorLight,
color: _isReady
? AppColors.statusSuccess
: AppColors.primaryBlue,
value: _animationController.value,
);
},
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
RichText(
text: TextSpan(
text: '${_benefit.currentHours}/',
style: AppTextStyles.headingH3,
children: [
TextSpan(
text: '${_benefit.requiredHours}',
style: AppTextStyles.headingH3.copyWith(
color: _isReady
? AppColors.blackBlack
: AppColors.blackCaptionText,
),
),
],
),
),
const SizedBox(height: 4),
Text(
'hours'.tr().toLowerCase(),
style: AppTextStyles.captionReg
.copyWith(color: AppColors.blackCaptionText),
),
],
)
],
),
const SizedBox(width: 24),
Expanded(
child: Stack(
alignment: AlignmentDirectional.centerStart,
clipBehavior: Clip.none,
children: [
const SizedBox(height: 90),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_benefit.name, style: AppTextStyles.headingH3),
const SizedBox(height: 6),
Text(
_benefit.requirement,
style: AppTextStyles.bodySmallReg.copyWith(
color: AppColors.blackGray,
),
),
],
),
Positioned(
top: -4,
right: 0,
child: Assets.images.icons.alertCircle.svg(
height: 16,
width: 16,
colorFilter: const ColorFilter.mode(
AppColors.grayStroke,
BlendMode.srcIn,
),
),
)
],
),
),
],
),
const SizedBox(height: 20),
if (_isReady)
Container(
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DecoratedBox(
decoration: const BoxDecoration(
color: AppColors.tintGreen,
shape: BoxShape.circle,
),
child: SizedBox.square(
dimension: 28,
child: Assets.images.icons.checkCircle.svg(
height: 10,
width: 10,
fit: BoxFit.scaleDown,
colorFilter: const ColorFilter.mode(
AppColors.statusSuccess,
BlendMode.srcIn,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_benefit.info,
style: AppTextStyles.bodyTinyMed.copyWith(
color: AppColors.statusSuccess,
),
),
),
],
),
),
BenefitHistoryWidget(benefit: _benefit),
if (_isReady)
Padding(
padding: const EdgeInsets.only(top: 20),
child: KwButton.primary(
label: '${'request_payment_for'.tr()} ${_benefit.name}',
onPressed: _handleRequestPress,
),
),
],
),
),
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
}
}

View File

@@ -0,0 +1,123 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/profile/benefits/domain/entities/benefit_entity.dart';
import 'package:krow/features/profile/benefits/domain/entities/benefit_record_entity.dart';
class BenefitHistoryWidget extends StatelessWidget {
const BenefitHistoryWidget({super.key, required this.benefit});
final BenefitEntity benefit;
@override
Widget build(BuildContext context) {
return ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 12),
childrenPadding: const EdgeInsets.symmetric(horizontal: 12),
dense: true,
visualDensity: VisualDensity.compact,
collapsedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
backgroundColor: AppColors.graySecondaryFrame,
collapsedBackgroundColor: AppColors.graySecondaryFrame,
iconColor: AppColors.blackBlack,
collapsedIconColor: AppColors.blackBlack,
title: Text(
'${benefit.name} ${'history'.tr()}'.toUpperCase(),
style: AppTextStyles.captionBold,
),
children: [
const Divider(
thickness: 1,
color: AppColors.tintGray,
),
const SizedBox(height: 12),
if (benefit.history.isEmpty)
SizedBox(
height: 80,
child: Center(
child: Text('no_history_yet'.tr()),
),
)
else
SizedBox(
height: 168,
child: RawScrollbar(
padding: EdgeInsets.zero,
thumbVisibility: true,
trackVisibility: true,
thumbColor: AppColors.grayStroke,
trackColor: AppColors.grayTintStroke,
trackRadius: const Radius.circular(8),
radius: const Radius.circular(8),
trackBorderColor: Colors.transparent,
thickness: 5,
minOverscrollLength: 0,
child: ListView.separated(
padding: const EdgeInsetsDirectional.only(end: 10),
itemCount: benefit.history.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 12),
itemBuilder: (context, index) {
return _HistoryRecordWidget(record: benefit.history[index]);
},
),
),
),
const SizedBox(height: 12),
],
);
}
}
class _HistoryRecordWidget extends StatelessWidget {
const _HistoryRecordWidget({required this.record});
static final _dateFormat = DateFormat('d MMM, yyyy');
final BenefitRecordEntity record;
@override
Widget build(BuildContext context) {
final Color color = switch (record.status) {
RecordStatus.pending => AppColors.primaryBlue,
RecordStatus.submitted => AppColors.statusSuccess,
};
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_dateFormat.format(record.createdAt),
style: AppTextStyles.bodySmallReg.copyWith(
color: AppColors.blackGray,
),
),
DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(24)),
border: Border.all(color: color),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Text(
switch (record.status) {
RecordStatus.pending => 'pending'.tr(),
RecordStatus.submitted => 'submitted'.tr(),
},
style: AppTextStyles.bodySmallReg.copyWith(
color: color,
),
),
),
),
],
);
}
}