Merge pull request #388 from Oloodi/feature/centralized-data-error-handling

Fix: Resolve critical bugs and linting issues (concurrency, syntax, dead code)
This commit is contained in:
Achintha Isuru
2026-02-10 11:10:03 -05:00
committed by GitHub
46 changed files with 4057 additions and 1299 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_current_bill_amount.dart';
import '../../domain/usecases/get_invoice_history.dart';
@@ -11,7 +12,8 @@ import 'billing_event.dart';
import 'billing_state.dart';
/// BLoC for managing billing state and data loading.
class BillingBloc extends Bloc<BillingEvent, BillingState> {
class BillingBloc extends Bloc<BillingEvent, BillingState>
with BlocErrorHandler<BillingState> {
/// Creates a [BillingBloc] with the given use cases.
BillingBloc({
required GetCurrentBillAmountUseCase getCurrentBillAmount,
@@ -40,82 +42,82 @@ class BillingBloc extends Bloc<BillingEvent, BillingState> {
Emitter<BillingState> emit,
) async {
emit(state.copyWith(status: BillingStatus.loading));
try {
final List<dynamic> results = await Future.wait<dynamic>(<Future<dynamic>>[
_getCurrentBillAmount.call(),
_getSavingsAmount.call(),
_getPendingInvoices.call(),
_getInvoiceHistory.call(),
_getSpendingBreakdown.call(state.period),
]);
await handleError(
emit: emit,
action: () async {
final List<dynamic> results =
await Future.wait<dynamic>(<Future<dynamic>>[
_getCurrentBillAmount.call(),
_getSavingsAmount.call(),
_getPendingInvoices.call(),
_getInvoiceHistory.call(),
_getSpendingBreakdown.call(state.period),
]);
final double currentBill = results[0] as double;
final double savings = results[1] as double;
final List<Invoice> pendingInvoices = results[2] as List<Invoice>;
final List<Invoice> invoiceHistory = results[3] as List<Invoice>;
final List<InvoiceItem> spendingItems = results[4] as List<InvoiceItem>;
final double currentBill = results[0] as double;
final double savings = results[1] as double;
final List<Invoice> pendingInvoices = results[2] as List<Invoice>;
final List<Invoice> invoiceHistory = results[3] as List<Invoice>;
final List<InvoiceItem> spendingItems = results[4] as List<InvoiceItem>;
// Map Domain Entities to Presentation Models
final List<BillingInvoice> uiPendingInvoices = pendingInvoices
.map(_mapInvoiceToUiModel)
.toList();
final List<BillingInvoice> uiInvoiceHistory = invoiceHistory
.map(_mapInvoiceToUiModel)
.toList();
final List<SpendingBreakdownItem> uiSpendingBreakdown = _mapSpendingItemsToUiModel(spendingItems);
final double periodTotal = uiSpendingBreakdown.fold(
0.0,
(double sum, SpendingBreakdownItem item) => sum + item.amount,
);
// Map Domain Entities to Presentation Models
final List<BillingInvoice> uiPendingInvoices =
pendingInvoices.map(_mapInvoiceToUiModel).toList();
final List<BillingInvoice> uiInvoiceHistory =
invoiceHistory.map(_mapInvoiceToUiModel).toList();
final List<SpendingBreakdownItem> uiSpendingBreakdown =
_mapSpendingItemsToUiModel(spendingItems);
final double periodTotal = uiSpendingBreakdown.fold(
0.0,
(double sum, SpendingBreakdownItem item) => sum + item.amount,
);
emit(
state.copyWith(
status: BillingStatus.success,
currentBill: periodTotal,
savings: savings,
pendingInvoices: uiPendingInvoices,
invoiceHistory: uiInvoiceHistory,
spendingBreakdown: uiSpendingBreakdown,
),
);
} catch (e) {
emit(
state.copyWith(
status: BillingStatus.failure,
errorMessage: e.toString(),
),
);
}
emit(
state.copyWith(
status: BillingStatus.success,
currentBill: periodTotal,
savings: savings,
pendingInvoices: uiPendingInvoices,
invoiceHistory: uiInvoiceHistory,
spendingBreakdown: uiSpendingBreakdown,
),
);
},
onError: (String errorKey) => state.copyWith(
status: BillingStatus.failure,
errorMessage: errorKey,
),
);
}
Future<void> _onPeriodChanged(
BillingPeriodChanged event,
Emitter<BillingState> emit,
) async {
try {
final List<InvoiceItem> spendingItems =
await _getSpendingBreakdown.call(event.period);
final List<SpendingBreakdownItem> uiSpendingBreakdown =
_mapSpendingItemsToUiModel(spendingItems);
final double periodTotal = uiSpendingBreakdown.fold(
0.0,
(double sum, SpendingBreakdownItem item) => sum + item.amount,
);
emit(
state.copyWith(
period: event.period,
spendingBreakdown: uiSpendingBreakdown,
currentBill: periodTotal,
),
);
} catch (e) {
emit(
state.copyWith(
status: BillingStatus.failure,
errorMessage: e.toString(),
),
);
}
await handleError(
emit: emit,
action: () async {
final List<InvoiceItem> spendingItems =
await _getSpendingBreakdown.call(event.period);
final List<SpendingBreakdownItem> uiSpendingBreakdown =
_mapSpendingItemsToUiModel(spendingItems);
final double periodTotal = uiSpendingBreakdown.fold(
0.0,
(double sum, SpendingBreakdownItem item) => sum + item.amount,
);
emit(
state.copyWith(
period: event.period,
spendingBreakdown: uiSpendingBreakdown,
currentBill: periodTotal,
),
);
},
onError: (String errorKey) => state.copyWith(
status: BillingStatus.failure,
errorMessage: errorKey,
),
);
}
BillingInvoice _mapInvoiceToUiModel(Invoice invoice) {

View File

@@ -1,6 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/arguments/get_coverage_stats_arguments.dart';
import '../../domain/arguments/get_shifts_for_date_arguments.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_coverage_stats_usecase.dart';
import '../../domain/usecases/get_shifts_for_date_usecase.dart';
@@ -13,14 +14,15 @@ import 'coverage_state.dart';
/// - Loading shifts for a specific date
/// - Loading coverage statistics
/// - Refreshing coverage data
class CoverageBloc extends Bloc<CoverageEvent, CoverageState> {
class CoverageBloc extends Bloc<CoverageEvent, CoverageState>
with BlocErrorHandler<CoverageState> {
/// Creates a [CoverageBloc].
CoverageBloc({
required GetShiftsForDateUseCase getShiftsForDate,
required GetCoverageStatsUseCase getCoverageStats,
}) : _getShiftsForDate = getShiftsForDate,
_getCoverageStats = getCoverageStats,
super(const CoverageState()) {
}) : _getShiftsForDate = getShiftsForDate,
_getCoverageStats = getCoverageStats,
super(const CoverageState()) {
on<CoverageLoadRequested>(_onLoadRequested);
on<CoverageRefreshRequested>(_onRefreshRequested);
}
@@ -40,31 +42,31 @@ class CoverageBloc extends Bloc<CoverageEvent, CoverageState> {
),
);
try {
// Fetch shifts and stats concurrently
final List<Object> results = await Future.wait<Object>(<Future<Object>>[
_getShiftsForDate(GetShiftsForDateArguments(date: event.date)),
_getCoverageStats(GetCoverageStatsArguments(date: event.date)),
]);
await handleError(
emit: emit,
action: () async {
// Fetch shifts and stats concurrently
final List<Object> results = await Future.wait<Object>(<Future<Object>>[
_getShiftsForDate(GetShiftsForDateArguments(date: event.date)),
_getCoverageStats(GetCoverageStatsArguments(date: event.date)),
]);
final List<CoverageShift> shifts = results[0] as List<CoverageShift>;
final CoverageStats stats = results[1] as CoverageStats;
final List<CoverageShift> shifts = results[0] as List<CoverageShift>;
final CoverageStats stats = results[1] as CoverageStats;
emit(
state.copyWith(
status: CoverageStatus.success,
shifts: shifts,
stats: stats,
),
);
} catch (error) {
emit(
state.copyWith(
status: CoverageStatus.failure,
errorMessage: error.toString(),
),
);
}
emit(
state.copyWith(
status: CoverageStatus.success,
shifts: shifts,
stats: stats,
),
);
},
onError: (String errorKey) => state.copyWith(
status: CoverageStatus.failure,
errorMessage: errorKey,
),
);
}
/// Handles the refresh requested event.
@@ -78,3 +80,4 @@ class CoverageBloc extends Bloc<CoverageEvent, CoverageState> {
add(CoverageLoadRequested(date: state.selectedDate!));
}
}

View File

@@ -128,9 +128,7 @@ class ClientCreateOrderRepositoryImpl
final double rate = order.roleRates[position.role] ?? 0;
final double totalValue = rate * hours * position.count;
print(
'CreateOneTimeOrder shiftRole: start=${start.toIso8601String()} end=${normalizedEnd.toIso8601String()}',
);
await executeProtected(() => _dataConnect
.createShiftRole(

View File

@@ -1,4 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_order_types_usecase.dart';
import 'client_create_order_event.dart';
@@ -6,7 +7,8 @@ import 'client_create_order_state.dart';
/// BLoC for managing the list of available order types.
class ClientCreateOrderBloc
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState>
with BlocErrorHandler<ClientCreateOrderState> {
ClientCreateOrderBloc(this._getOrderTypesUseCase)
: super(const ClientCreateOrderInitial()) {
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
@@ -17,7 +19,14 @@ class ClientCreateOrderBloc
ClientCreateOrderTypesRequested event,
Emitter<ClientCreateOrderState> emit,
) async {
final List<OrderType> types = await _getOrderTypesUseCase();
emit(ClientCreateOrderLoadSuccess(types));
await handleError(
emit: emit,
action: () async {
final List<OrderType> types = await _getOrderTypesUseCase();
emit(ClientCreateOrderLoadSuccess(types));
},
onError: (String errorKey) => ClientCreateOrderLoadFailure(errorKey),
);
}
}

View File

@@ -24,3 +24,13 @@ class ClientCreateOrderLoadSuccess extends ClientCreateOrderState {
@override
List<Object?> get props => <Object?>[orderTypes];
}
/// State representing a failure to load order types.
class ClientCreateOrderLoadFailure extends ClientCreateOrderState {
const ClientCreateOrderLoadFailure(this.error);
final String error;
@override
List<Object?> get props => <Object?>[error];
}

View File

@@ -1,5 +1,6 @@
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/one_time_order_arguments.dart';
@@ -8,7 +9,8 @@ import 'one_time_order_event.dart';
import 'one_time_order_state.dart';
/// BLoC for managing the multi-step one-time order creation form.
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
with BlocErrorHandler<OneTimeOrderState> {
OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._dataConnect)
: super(OneTimeOrderState.initial()) {
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
@@ -29,79 +31,93 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
final dc.ExampleConnector _dataConnect;
Future<void> _loadVendors() async {
try {
final QueryResult<dc.ListVendorsData, void> result = await _dataConnect.listVendors().execute();
final List<Vendor> vendors = result.data.vendors
.map(
(dc.ListVendorsVendors vendor) => Vendor(
id: vendor.id,
name: vendor.companyName,
rates: const <String, double>{},
),
)
.toList();
final List<Vendor>? vendors = await handleErrorWithResult(
action: () async {
final QueryResult<dc.ListVendorsData, void> result =
await _dataConnect.listVendors().execute();
return result.data.vendors
.map(
(dc.ListVendorsVendors vendor) => Vendor(
id: vendor.id,
name: vendor.companyName,
rates: const <String, double>{},
),
)
.toList();
},
onError: (_) => add(const OneTimeOrderVendorsLoaded(<Vendor>[])),
);
if (vendors != null) {
add(OneTimeOrderVendorsLoaded(vendors));
} catch (_) {
add(const OneTimeOrderVendorsLoaded(<Vendor>[]));
}
}
Future<void> _loadRolesForVendor(String vendorId) async {
try {
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables> result = await _dataConnect.listRolesByVendorId(
vendorId: vendorId,
).execute();
final List<OneTimeOrderRoleOption> roles = result.data.roles
.map(
(dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption(
id: role.id,
name: role.name,
costPerHour: role.costPerHour,
),
)
.toList();
Future<void> _loadRolesForVendor(String vendorId, Emitter<OneTimeOrderState> emit) async {
final List<OneTimeOrderRoleOption>? roles = await handleErrorWithResult(
action: () async {
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
result = await _dataConnect.listRolesByVendorId(vendorId: vendorId).execute();
return result.data.roles
.map(
(dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption(
id: role.id,
name: role.name,
costPerHour: role.costPerHour,
),
)
.toList();
},
onError: (_) => emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[])),
);
if (roles != null) {
emit(state.copyWith(roles: roles));
} catch (_) {
emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[]));
}
}
Future<void> _loadHubs() async {
try {
final String? businessId = dc.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[]));
return;
}
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
result = await _dataConnect.listTeamHubsByOwnerId(ownerId: businessId).execute();
final List<OneTimeOrderHubOption> hubs = result.data.teamHubs
.map(
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => OneTimeOrderHubOption(
id: hub.id,
name: hub.hubName,
address: hub.address,
placeId: hub.placeId,
latitude: hub.latitude,
longitude: hub.longitude,
city: hub.city,
state: hub.state,
street: hub.street,
country: hub.country,
zipCode: hub.zipCode,
),
)
.toList();
final List<OneTimeOrderHubOption>? hubs = await handleErrorWithResult(
action: () async {
final String? businessId =
dc.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
return <OneTimeOrderHubOption>[];
}
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
result = await _dataConnect
.listTeamHubsByOwnerId(ownerId: businessId)
.execute();
return result.data.teamHubs
.map(
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => OneTimeOrderHubOption(
id: hub.id,
name: hub.hubName,
address: hub.address,
placeId: hub.placeId,
latitude: hub.latitude,
longitude: hub.longitude,
city: hub.city,
state: hub.state,
street: hub.street,
country: hub.country,
zipCode: hub.zipCode,
),
)
.toList();
},
onError: (_) => add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[])),
);
if (hubs != null) {
add(OneTimeOrderHubsLoaded(hubs));
} catch (_) {
add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[]));
}
}
void _onVendorsLoaded(
Future<void> _onVendorsLoaded(
OneTimeOrderVendorsLoaded event,
Emitter<OneTimeOrderState> emit,
) {
) async {
final Vendor? selectedVendor =
event.vendors.isNotEmpty ? event.vendors.first : null;
emit(
@@ -111,16 +127,16 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
),
);
if (selectedVendor != null) {
_loadRolesForVendor(selectedVendor.id);
await _loadRolesForVendor(selectedVendor.id, emit);
}
}
void _onVendorChanged(
Future<void> _onVendorChanged(
OneTimeOrderVendorChanged event,
Emitter<OneTimeOrderState> emit,
) {
) async {
emit(state.copyWith(selectedVendor: event.vendor));
_loadRolesForVendor(event.vendor.id);
await _loadRolesForVendor(event.vendor.id, emit);
}
void _onHubsLoaded(
@@ -207,44 +223,45 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
Emitter<OneTimeOrderState> emit,
) async {
emit(state.copyWith(status: OneTimeOrderStatus.loading));
try {
final Map<String, double> roleRates = <String, double>{
for (final OneTimeOrderRoleOption role in state.roles) role.id: role.costPerHour,
};
final OneTimeOrderHubOption? selectedHub = state.selectedHub;
if (selectedHub == null) {
throw Exception('Hub is missing.');
}
final OneTimeOrder order = OneTimeOrder(
date: state.date,
location: selectedHub.name,
positions: state.positions,
hub: OneTimeOrderHubDetails(
id: selectedHub.id,
name: selectedHub.name,
address: selectedHub.address,
placeId: selectedHub.placeId,
latitude: selectedHub.latitude,
longitude: selectedHub.longitude,
city: selectedHub.city,
state: selectedHub.state,
street: selectedHub.street,
country: selectedHub.country,
zipCode: selectedHub.zipCode,
),
eventName: state.eventName,
vendorId: state.selectedVendor?.id,
roleRates: roleRates,
);
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
emit(state.copyWith(status: OneTimeOrderStatus.success));
} catch (e) {
emit(
state.copyWith(
status: OneTimeOrderStatus.failure,
errorMessage: e.toString(),
),
);
}
await handleError(
emit: emit,
action: () async {
final Map<String, double> roleRates = <String, double>{
for (final OneTimeOrderRoleOption role in state.roles)
role.id: role.costPerHour,
};
final OneTimeOrderHubOption? selectedHub = state.selectedHub;
if (selectedHub == null) {
throw const OrderMissingHubException();
}
final OneTimeOrder order = OneTimeOrder(
date: state.date,
location: selectedHub.name,
positions: state.positions,
hub: OneTimeOrderHubDetails(
id: selectedHub.id,
name: selectedHub.name,
address: selectedHub.address,
placeId: selectedHub.placeId,
latitude: selectedHub.latitude,
longitude: selectedHub.longitude,
city: selectedHub.city,
state: selectedHub.state,
street: selectedHub.street,
country: selectedHub.country,
zipCode: selectedHub.zipCode,
),
eventName: state.eventName,
vendorId: state.selectedVendor?.id,
roleRates: roleRates,
);
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
emit(state.copyWith(status: OneTimeOrderStatus.success));
},
onError: (String errorKey) => state.copyWith(
status: OneTimeOrderStatus.failure,
errorMessage: errorKey,
),
);
}
}

View File

@@ -1,11 +1,13 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import '../../domain/arguments/rapid_order_arguments.dart';
import '../../domain/usecases/create_rapid_order_usecase.dart';
import 'rapid_order_event.dart';
import 'rapid_order_state.dart';
/// BLoC for managing the rapid (urgent) order creation flow.
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
with BlocErrorHandler<RapidOrderState> {
RapidOrderBloc(this._createRapidOrderUseCase)
: super(
const RapidOrderInitial(
@@ -64,19 +66,18 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
final RapidOrderState currentState = state;
if (currentState is RapidOrderInitial) {
final String message = currentState.message;
print('RapidOrder submit: message="$message"');
emit(const RapidOrderSubmitting());
try {
await _createRapidOrderUseCase(
RapidOrderArguments(description: message),
);
print('RapidOrder submit: success');
emit(const RapidOrderSuccess());
} catch (e) {
print('RapidOrder submit: error=$e');
emit(RapidOrderFailure(e.toString()));
}
await handleError(
emit: emit,
action: () async {
await _createRapidOrderUseCase(
RapidOrderArguments(description: message),
);
emit(const RapidOrderSuccess());
},
onError: (String errorKey) => RapidOrderFailure(errorKey),
);
}
}
@@ -90,3 +91,4 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:client_home/src/domain/repositories/home_repository_interface.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_dashboard_data_usecase.dart';
import '../../domain/usecases/get_recent_reorders_usecase.dart';
@@ -8,8 +9,8 @@ import 'client_home_event.dart';
import 'client_home_state.dart';
/// BLoC responsible for managing the state and business logic of the client home dashboard.
class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState> {
class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState>
with BlocErrorHandler<ClientHomeState> {
ClientHomeBloc({
required GetDashboardDataUseCase getDashboardDataUseCase,
required GetRecentReordersUseCase getRecentReordersUseCase,
@@ -35,31 +36,31 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState> {
Emitter<ClientHomeState> emit,
) async {
emit(state.copyWith(status: ClientHomeStatus.loading));
try {
// Get session data
final UserSessionData sessionData = _getUserSessionDataUseCase();
await handleError(
emit: emit,
action: () async {
// Get session data
final UserSessionData sessionData = _getUserSessionDataUseCase();
// Get dashboard data
final HomeDashboardData data = await _getDashboardDataUseCase();
final List<ReorderItem> reorderItems = await _getRecentReordersUseCase();
// Get dashboard data
final HomeDashboardData data = await _getDashboardDataUseCase();
final List<ReorderItem> reorderItems = await _getRecentReordersUseCase();
emit(
state.copyWith(
status: ClientHomeStatus.success,
dashboardData: data,
reorderItems: reorderItems,
businessName: sessionData.businessName,
photoUrl: sessionData.photoUrl,
),
);
} catch (e) {
emit(
state.copyWith(
status: ClientHomeStatus.error,
errorMessage: e.toString(),
),
);
}
emit(
state.copyWith(
status: ClientHomeStatus.success,
dashboardData: data,
reorderItems: reorderItems,
businessName: sessionData.businessName,
photoUrl: sessionData.photoUrl,
),
);
},
onError: (String errorKey) => state.copyWith(
status: ClientHomeStatus.error,
errorMessage: errorKey,
),
);
}
void _onEditModeToggled(
@@ -73,7 +74,8 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState> {
ClientHomeWidgetVisibilityToggled event,
Emitter<ClientHomeState> emit,
) {
final Map<String, bool> newVisibility = Map<String, bool>.from(state.widgetVisibility);
final Map<String, bool> newVisibility =
Map<String, bool>.from(state.widgetVisibility);
newVisibility[event.widgetId] = !(newVisibility[event.widgetId] ?? true);
emit(state.copyWith(widgetVisibility: newVisibility));
}
@@ -119,3 +121,4 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState> {
);
}
}

View File

@@ -1,13 +1,14 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import '../../domain/usecases/sign_out_usecase.dart';
part 'client_settings_event.dart';
part 'client_settings_state.dart';
/// BLoC to manage client settings and profile state.
class ClientSettingsBloc
extends Bloc<ClientSettingsEvent, ClientSettingsState> {
class ClientSettingsBloc extends Bloc<ClientSettingsEvent, ClientSettingsState>
with BlocErrorHandler<ClientSettingsState> {
final SignOutUseCase _signOutUseCase;
ClientSettingsBloc({required SignOutUseCase signOutUseCase})
@@ -21,11 +22,14 @@ class ClientSettingsBloc
Emitter<ClientSettingsState> emit,
) async {
emit(const ClientSettingsLoading());
try {
await _signOutUseCase();
emit(const ClientSettingsSignOutSuccess());
} catch (e) {
emit(ClientSettingsError(e.toString()));
}
await handleError(
emit: emit,
action: () async {
await _signOutUseCase();
emit(const ClientSettingsSignOutSuccess());
},
onError: (String errorKey) => ClientSettingsError(errorKey),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:intl/intl.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/orders_day_arguments.dart';
import '../../domain/arguments/orders_range_arguments.dart';
@@ -10,14 +11,14 @@ import 'view_orders_state.dart';
/// Cubit for managing the state of the View Orders feature.
///
/// This Cubit handles loading orders, date selection, and tab filtering.
class ViewOrdersCubit extends Cubit<ViewOrdersState> {
class ViewOrdersCubit extends Cubit<ViewOrdersState>
with BlocErrorHandler<ViewOrdersState> {
ViewOrdersCubit({
required GetOrdersUseCase getOrdersUseCase,
required GetAcceptedApplicationsForDayUseCase getAcceptedAppsUseCase,
})
: _getOrdersUseCase = getOrdersUseCase,
_getAcceptedAppsUseCase = getAcceptedAppsUseCase,
super(ViewOrdersState(selectedDate: DateTime.now())) {
}) : _getOrdersUseCase = getOrdersUseCase,
_getAcceptedAppsUseCase = getAcceptedAppsUseCase,
super(ViewOrdersState(selectedDate: DateTime.now())) {
_init();
}
@@ -36,30 +37,33 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
}) async {
final int requestId = ++_requestId;
emit(state.copyWith(status: ViewOrdersStatus.loading));
try {
final List<OrderItem> orders = await _getOrdersUseCase(
OrdersRangeArguments(start: rangeStart, end: rangeEnd),
);
final Map<String, List<Map<String, dynamic>>> apps = await _getAcceptedAppsUseCase(
OrdersDayArguments(day: dayForApps),
);
final List<OrderItem> updatedOrders = _applyApplications(orders, apps);
if (requestId != _requestId) {
return;
}
emit(
state.copyWith(
status: ViewOrdersStatus.success,
orders: updatedOrders,
),
);
_updateDerivedState();
} catch (_) {
if (requestId != _requestId) {
return;
}
emit(state.copyWith(status: ViewOrdersStatus.failure));
}
await handleError(
emit: (ViewOrdersState s) {
if (requestId == _requestId) emit(s);
},
action: () async {
final List<OrderItem> orders = await _getOrdersUseCase(
OrdersRangeArguments(start: rangeStart, end: rangeEnd),
);
final Map<String, List<Map<String, dynamic>>> apps =
await _getAcceptedAppsUseCase(OrdersDayArguments(day: dayForApps));
if (requestId != _requestId) {
return;
}
final List<OrderItem> updatedOrders = _applyApplications(orders, apps);
emit(
state.copyWith(
status: ViewOrdersStatus.success,
orders: updatedOrders,
),
);
_updateDerivedState();
},
onError: (String _) => state.copyWith(status: ViewOrdersStatus.failure),
);
}
void selectDate(DateTime date) {
@@ -78,7 +82,9 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
final DateTime? selectedDate = state.selectedDate;
final DateTime updatedSelectedDate =
selectedDate != null &&
calendarDays.any((DateTime day) => _isSameDay(day, selectedDate))
calendarDays.any(
(DateTime day) => _isSameDay(day, selectedDate),
)
? selectedDate
: calendarDays.first;
emit(
@@ -135,17 +141,21 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
}
Future<void> _refreshAcceptedApplications(DateTime day) async {
try {
final Map<String, List<Map<String, dynamic>>> apps = await _getAcceptedAppsUseCase(
OrdersDayArguments(day: day),
);
final List<OrderItem> updatedOrders =
_applyApplications(state.orders, apps);
emit(state.copyWith(orders: updatedOrders));
_updateDerivedState();
} catch (_) {
// Keep existing data on failure.
}
await handleErrorWithResult(
action: () async {
final Map<String, List<Map<String, dynamic>>> apps =
await _getAcceptedAppsUseCase(OrdersDayArguments(day: day));
final List<OrderItem> updatedOrders = _applyApplications(
state.orders,
apps,
);
emit(state.copyWith(orders: updatedOrders));
_updateDerivedState();
},
onError: (_) {
// Keep existing data on failure, just log error via handleErrorWithResult
},
);
}
List<OrderItem> _applyApplications(
@@ -153,7 +163,8 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
Map<String, List<Map<String, dynamic>>> apps,
) {
return orders.map((OrderItem order) {
final List<Map<String, dynamic>> confirmed = apps[order.id] ?? const <Map<String, dynamic>>[];
final List<Map<String, dynamic>> confirmed =
apps[order.id] ?? const <Map<String, dynamic>>[];
if (confirmed.isEmpty) {
return order;
}
@@ -209,9 +220,10 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
).format(state.selectedDate!);
// Filter by date
final List<OrderItem> ordersOnDate = state.orders
.where((OrderItem s) => s.date == selectedDateStr)
.toList();
final List<OrderItem> ordersOnDate =
state.orders
.where((OrderItem s) => s.date == selectedDateStr)
.toList();
// Sort by start time
ordersOnDate.sort(
@@ -219,30 +231,38 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
);
if (state.filterTab == 'all') {
final List<OrderItem> filtered = ordersOnDate
.where(
(OrderItem s) =>
// TODO(orders): move PENDING to its own tab once available.
<String>['OPEN', 'FILLED', 'CONFIRMED', 'PENDING', 'ASSIGNED']
.contains(s.status),
)
.toList();
final List<OrderItem> filtered =
ordersOnDate
.where(
(OrderItem s) =>
// TODO(orders): move PENDING to its own tab once available.
<String>[
'OPEN',
'FILLED',
'CONFIRMED',
'PENDING',
'ASSIGNED',
].contains(s.status),
)
.toList();
print(
'ViewOrders tab=all statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}',
);
return filtered;
} else if (state.filterTab == 'active') {
final List<OrderItem> filtered = ordersOnDate
.where((OrderItem s) => s.status == 'IN_PROGRESS')
.toList();
final List<OrderItem> filtered =
ordersOnDate
.where((OrderItem s) => s.status == 'IN_PROGRESS')
.toList();
print(
'ViewOrders tab=active statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}',
);
return filtered;
} else if (state.filterTab == 'completed') {
final List<OrderItem> filtered = ordersOnDate
.where((OrderItem s) => s.status == 'COMPLETED')
.toList();
final List<OrderItem> filtered =
ordersOnDate
.where((OrderItem s) => s.status == 'COMPLETED')
.toList();
print(
'ViewOrders tab=completed statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}',
);
@@ -260,11 +280,17 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
if (category == 'active') {
return state.orders
.where((OrderItem s) => s.date == selectedDateStr && s.status == 'IN_PROGRESS')
.where(
(OrderItem s) =>
s.date == selectedDateStr && s.status == 'IN_PROGRESS',
)
.length;
} else if (category == 'completed') {
return state.orders
.where((OrderItem s) => s.date == selectedDateStr && s.status == 'COMPLETED')
.where(
(OrderItem s) =>
s.date == selectedDateStr && s.status == 'COMPLETED',
)
.length;
}
return 0;
@@ -281,9 +307,15 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
.where(
(OrderItem s) =>
s.date == selectedDateStr &&
<String>['OPEN', 'FILLED', 'CONFIRMED', 'PENDING', 'ASSIGNED']
.contains(s.status),
<String>[
'OPEN',
'FILLED',
'CONFIRMED',
'PENDING',
'ASSIGNED',
].contains(s.status),
)
.length;
}
}