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,201 @@
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart';
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/pagination_status.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
import 'package:krow/features/earning/domain/entities/earnings_summary_entity.dart';
import 'package:krow/features/earning/domain/staff_earning_repository.dart';
part 'earnings_data.dart';
part 'earnings_event.dart';
part 'earnings_state.dart';
class EarningsBloc extends Bloc<EarningsEvent, EarningsState> {
EarningsBloc()
: super(const EarningsState.initial()
..copyWith(
tabs: defaultEarningTabs,
)) {
on<EarningsInitEvent>(_onInit);
on<EarningsSwitchBalancePeriodEvent>(_onSwitchBalancePeriod);
on<EarningsTabChangedEvent>(_onTabChanged);
on<LoadAdditionalEarnings>(_onLoadAdditionalEarnings);
on<ReloadCurrentEarnings>(_onReloadCurrentEarnings);
on<ConfirmStaffEarning>(_onConfirmStaffEarning);
on<DisputeStaffEarning>(_onDisputeStaffEarning);
}
final _earningRepository = getIt<StaffEarningRepository>();
Future<void> _onInit(
EarningsInitEvent event,
Emitter<EarningsState> emit,
) async {
add(const LoadAdditionalEarnings());
emit(state.copyWith(status: StateStatus.loading));
EarningsSummaryEntity? earningsData;
try {
earningsData = await _earningRepository.getStaffEarningsData();
} catch (except) {
log('Error in EarningsBloc, on EarningsInitEvent', error: except);
}
emit(state.copyWith(earnings: earningsData, status: StateStatus.idle));
}
void _onSwitchBalancePeriod(
EarningsSwitchBalancePeriodEvent event,
Emitter<EarningsState> emit,
) {
emit(state.copyWith(balancePeriod: event.period));
}
void _onTabChanged(
EarningsTabChangedEvent event,
Emitter<EarningsState> emit,
) {
emit(state.copyWith(tabIndex: event.tabIndex));
if (state.currentTab.status == PaginationStatus.initial) {
add(const LoadAdditionalEarnings());
}
}
Future<void> _onLoadAdditionalEarnings(
LoadAdditionalEarnings event,
Emitter<EarningsState> emit,
) async {
if (!state.currentTab.status.allowLoad) return;
emit(state.updateCurrentTab(status: PaginationStatus.loading));
final tabIndex = state.tabIndex;
try {
final earningsBatch = await _earningRepository.getEarningsBatch(
status: state.currentTab.loadingKey,
lastEntryCursor: state.currentTab.paginationCursor,
);
emit(
state.copyWithTab(
tabIndex: tabIndex,
tab: state.tabs[tabIndex].copyWith(
status: earningsBatch.hasNextBatch
? PaginationStatus.idle
: PaginationStatus.end,
items: [...state.tabs[tabIndex].items, ...earningsBatch.earnings],
hasMoreItems: earningsBatch.hasNextBatch,
),
),
);
} catch (except) {
log(
'Error in EarningsBloc, on LoadAdditionalEarnings',
error: except,
);
emit(
state.copyWithTab(
tabIndex: tabIndex,
tab: state.tabs[tabIndex].copyWith(
status: PaginationStatus.error,
),
),
);
}
if (state.tabs[tabIndex].status == PaginationStatus.loading) {
emit(
state.copyWithTab(
tabIndex: tabIndex,
tab: state.tabs[tabIndex].copyWith(
status: PaginationStatus.idle,
),
),
);
}
}
void _onReloadCurrentEarnings(
ReloadCurrentEarnings event,
Emitter<EarningsState> emit,
) {
emit(
state.copyWithTab(
tab: EarningsTabState.initial(
label: state.currentTab.label,
loadingKey: state.currentTab.loadingKey,
),
),
);
add(const LoadAdditionalEarnings());
}
Future<void> _onEarningAction(
Future<EarningShiftEntity?> earningFuture,
String eventName,
Emitter<EarningsState> emit,
) async {
emit(state.copyWith(status: StateStatus.loading));
EarningShiftEntity? earning;
try {
earning = await earningFuture;
} catch (except) {
log(
'Error in EarningsBloc, on $eventName',
error: except,
);
}
var tabData = state.currentTab;
if (earning != null) {
final earningIndex =
state.currentTab.items.indexWhere((item) => item.id == earning?.id);
if (earningIndex >= 0) {
tabData = state.currentTab.copyWith(
items: List.from(state.currentTab.items)..[earningIndex] = earning,
);
}
}
emit(
state.copyWithTab(
status: StateStatus.idle,
tab: tabData,
),
);
}
Future<void> _onConfirmStaffEarning(
ConfirmStaffEarning event,
Emitter<EarningsState> emit,
) {
return _onEarningAction(
_earningRepository.confirmStaffEarning(
earningId: event.earningId,
),
'ConfirmStaffEarning',
emit,
);
}
Future<void> _onDisputeStaffEarning(
DisputeStaffEarning event,
Emitter<EarningsState> emit,
) async {
return _onEarningAction(
_earningRepository.disputeStaffEarning(
id: event.earningId,
reason: event.reason,
details: event.details,
),
'DeclineStaffEarning',
emit,
);
}
}

View File

@@ -0,0 +1,85 @@
part of 'earnings_bloc.dart';
enum BalancePeriod {
week,
month,
}
class EarningsTabState {
const EarningsTabState({
required this.label,
required this.loadingKey,
required this.items,
this.status = PaginationStatus.idle,
this.hasMoreItems = true,
this.paginationCursor,
});
const EarningsTabState.initial({
required this.label,
required this.loadingKey,
this.items = const [],
this.status = PaginationStatus.initial,
this.hasMoreItems = true,
this.paginationCursor,
});
final String label;
final String loadingKey;
final List<EarningShiftEntity> items;
final PaginationStatus status;
final bool hasMoreItems;
final String? paginationCursor;
EarningsTabState copyWith({
List<EarningShiftEntity>? items,
PaginationStatus? status,
bool? hasMoreItems,
String? label,
String? loadingKey,
String? paginationCursor,
}) {
return EarningsTabState(
loadingKey: loadingKey ?? this.loadingKey,
items: items ?? this.items,
status: status ?? this.status,
hasMoreItems: hasMoreItems ?? this.hasMoreItems,
label: label ?? this.label,
paginationCursor: paginationCursor ?? this.paginationCursor,
);
}
}
const defaultEarningTabs = [
EarningsTabState.initial(
label: 'new_earning',
loadingKey: 'new' // Confirmed by admin
),
EarningsTabState.initial(
label: 'confirmed',
loadingKey: 'confirmed', // Confirmed by staff
),
EarningsTabState.initial(
label: 'disputed',
loadingKey: 'disputed', // Declined by staff
),
EarningsTabState.initial(
label: 'sent',
loadingKey: 'sent', // Should remain sent
),
EarningsTabState.initial(
label: 'received',
loadingKey: 'paid', // Should remain paid
),
];
// $of = match ($status) {
// 'new' => fn(Builder $q) => $q->whereIn('status', [StaffPaymentStatus::new]),
// 'confirmed' => fn(Builder $q) => $q->whereIn('status', [StaffPaymentStatus::confirmed_by_admin]),
// 'pending' => fn(Builder $q) => $q->whereIn('status', [
// StaffPaymentStatus::decline_by_staff,
// StaffPaymentStatus::confirmed_by_staff,
// ]),
// 'sent' => fn(Builder $q) => $q->whereIn('status', [StaffPaymentStatus::sent]),
// 'paid' => fn(Builder $q) => $q->whereIn('status', [StaffPaymentStatus::paid]),
// };

View File

@@ -0,0 +1,48 @@
part of 'earnings_bloc.dart';
@immutable
sealed class EarningsEvent {
const EarningsEvent();
}
class EarningsInitEvent extends EarningsEvent {
const EarningsInitEvent();
}
class EarningsSwitchBalancePeriodEvent extends EarningsEvent {
final BalancePeriod period;
const EarningsSwitchBalancePeriodEvent({required this.period});
}
class EarningsTabChangedEvent extends EarningsEvent {
final int tabIndex;
const EarningsTabChangedEvent({required this.tabIndex});
}
class LoadAdditionalEarnings extends EarningsEvent {
const LoadAdditionalEarnings();
}
class ReloadCurrentEarnings extends EarningsEvent {
const ReloadCurrentEarnings();
}
class ConfirmStaffEarning extends EarningsEvent {
const ConfirmStaffEarning(this.earningId);
final String earningId;
}
class DisputeStaffEarning extends EarningsEvent {
const DisputeStaffEarning({
required this.earningId,
required this.reason,
required this.details,
});
final String earningId;
final String reason;
final String details;
}

View File

@@ -0,0 +1,92 @@
part of 'earnings_bloc.dart';
@immutable
class EarningsState {
const EarningsState({
required this.status,
required this.tabIndex,
required this.balancePeriod,
required this.earnings,
required this.tabs,
});
const EarningsState.initial({
this.status = StateStatus.idle,
this.tabIndex = 0,
this.balancePeriod = BalancePeriod.week,
this.earnings = const EarningsSummaryEntity.empty(),
this.tabs = defaultEarningTabs,
});
final StateStatus status;
final int tabIndex;
final BalancePeriod balancePeriod;
final EarningsSummaryEntity earnings;
final List<EarningsTabState> tabs;
EarningsTabState get currentTab => tabs[tabIndex];
double get totalEarnings {
return balancePeriod == BalancePeriod.week
? earnings.totalEarningsByWeek
: earnings.totalEarningsByMonth;
}
double get totalHours {
return balancePeriod == BalancePeriod.week
? earnings.totalWorkedHoursByWeek
: earnings.totalWorkedHoursByMonth;
}
double get totalPayout {
return balancePeriod == BalancePeriod.week
? earnings.payoutByWeek
: earnings.payoutByMonth;
}
EarningsState copyWith({
StateStatus? status,
int? tabIndex,
BalancePeriod? balancePeriod,
EarningsSummaryEntity? earnings,
List<EarningsTabState>? tabs,
}) {
return EarningsState(
status: status ?? this.status,
tabIndex: tabIndex ?? this.tabIndex,
balancePeriod: balancePeriod ?? this.balancePeriod,
earnings: earnings ?? this.earnings,
tabs: tabs ?? this.tabs,
);
}
EarningsState copyWithTab({
required EarningsTabState tab,
int? tabIndex,
StateStatus? status,
}) {
return copyWith(
tabs: List.from(tabs)..[tabIndex ?? this.tabIndex] = tab,
status: status,
);
}
EarningsState updateCurrentTab({
List<EarningShiftEntity>? items,
PaginationStatus? status,
bool? hasMoreItems,
String? label,
String? paginationCursor,
}) {
return copyWith(
tabs: List.from(tabs)
..[tabIndex] = currentTab.copyWith(
items: items,
status: status,
hasMoreItems: hasMoreItems,
label: label,
paginationCursor: paginationCursor,
),
);
}
}

View File

@@ -0,0 +1,4 @@
class EarningDisputeInfo {
String reason = '';
String details = '';
}

View File

@@ -0,0 +1,64 @@
class EarningShiftEntity {
final String id;
final EarningStatus status;
final String businessImageUrl;
final String skillName;
final String businessName;
final int? totalBreakTime;
final int? paymentStatus;
final double? earned;
final DateTime? clockIn;
final DateTime? clockOut;
final String eventName;
final DateTime eventDate;
EarningShiftEntity({
required this.id,
required this.status,
required this.businessImageUrl,
required this.skillName,
required this.businessName,
required this.totalBreakTime,
required this.paymentStatus,
required this.earned,
required this.clockIn,
required this.clockOut,
required this.eventName,
required this.eventDate,
});
int get paymentProgressStep {
return switch (status) {
EarningStatus.pending => 0, // corresponds to Pending Sent
EarningStatus.processing => 1, // corresponds to Pending Payment
EarningStatus.paid => 2, // corresponds to Payment Received
_ => -1, // don't show active step
};
}
}
enum EarningStatus {
new_,
pending,
confirmedByAdmin,
confirmedByStaff,
declineByStaff,
processing,
paid,
failed,
canceled;
static EarningStatus fromString(value) {
return switch (value) {
'new' => new_,
'confirmed_by_admin' => confirmedByAdmin,
'confirmed_by_staff' => confirmedByStaff,
'decline_by_staff' => declineByStaff,
'pending' => pending,
'paid' => paid,
'failed' => failed,
'canceled' => canceled,
_ => canceled,
};
}
}

View File

@@ -0,0 +1,15 @@
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
class EarningsBatchEntity {
EarningsBatchEntity({
required this.batchStatus,
required this.hasNextBatch,
required this.earnings,
this.cursor,
});
final String batchStatus;
final bool hasNextBatch;
final List<EarningShiftEntity> earnings;
final String? cursor;
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/foundation.dart';
@immutable
class EarningsSummaryEntity {
const EarningsSummaryEntity({
required this.totalEarningsByWeek,
required this.totalEarningsByMonth,
required this.totalWorkedHoursByWeek,
required this.totalWorkedHoursByMonth,
required this.payoutByWeek,
required this.payoutByMonth,
required this.startDatePeriod,
required this.endDatePeriod,
required this.maxEarningInPeriod,
required this.minEarningInPeriod,
});
const EarningsSummaryEntity.empty({
this.totalEarningsByWeek = 0,
this.totalEarningsByMonth = 0,
this.totalWorkedHoursByWeek = 0,
this.totalWorkedHoursByMonth = 0,
this.payoutByWeek = 0,
this.payoutByMonth = 0,
this.startDatePeriod,
this.endDatePeriod,
this.maxEarningInPeriod = 0,
this.minEarningInPeriod = 0,
});
final double totalEarningsByWeek;
final double totalEarningsByMonth;
final double totalWorkedHoursByWeek;
final double totalWorkedHoursByMonth;
final double payoutByWeek;
final double payoutByMonth;
final DateTime? startDatePeriod;
final DateTime? endDatePeriod;
final int maxEarningInPeriod;
final int minEarningInPeriod;
EarningsSummaryEntity copyWith({
double? totalEarningsByWeek,
double? totalEarningsByMonth,
double? totalWorkedHoursByWeek,
double? totalWorkedHoursByMonth,
double? payoutByWeek,
double? payoutByMonth,
DateTime? startDatePeriod,
DateTime? endDatePeriod,
int? maxEarningInPeriod,
int? minEarningInPeriod,
}) {
return EarningsSummaryEntity(
totalEarningsByWeek: totalEarningsByWeek ?? this.totalEarningsByWeek,
totalEarningsByMonth: totalEarningsByMonth ?? this.totalEarningsByMonth,
totalWorkedHoursByWeek:
totalWorkedHoursByWeek ?? this.totalWorkedHoursByWeek,
totalWorkedHoursByMonth:
totalWorkedHoursByMonth ?? this.totalWorkedHoursByMonth,
payoutByWeek: payoutByWeek ?? this.payoutByWeek,
payoutByMonth: payoutByMonth ?? this.payoutByMonth,
startDatePeriod: startDatePeriod,
endDatePeriod: endDatePeriod,
maxEarningInPeriod: maxEarningInPeriod ?? this.maxEarningInPeriod,
minEarningInPeriod: minEarningInPeriod ?? this.minEarningInPeriod,
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
import 'package:krow/features/earning/domain/entities/earnings_batch_entity.dart';
import 'package:krow/features/earning/domain/entities/earnings_summary_entity.dart';
abstract interface class StaffEarningRepository {
Future<EarningsSummaryEntity> getStaffEarningsData();
Future<EarningsBatchEntity> getEarningsBatch({
required String status,
int limit = 10,
String? lastEntryCursor,
});
Future<EarningShiftEntity?> confirmStaffEarning({required String earningId});
Future<EarningShiftEntity?> disputeStaffEarning({
required String id,
required String reason,
String? details,
});
}