feat: Refactor context reading in emergency contact and FAQs widgets
- Updated the context reading method in `EmergencyContactAddButton` and `EmergencyContactFormItem` to use `ReadContext`. - Modified the `FaqsWidget` to utilize `ReadContext` for fetching FAQs. - Adjusted the `PrivacySectionWidget` to read from `PrivacySecurityBloc` using `ReadContext`. feat: Implement Firebase Auth isolation pattern - Introduced `FirebaseAuthService` and `FirebaseAuthServiceImpl` to abstract Firebase Auth operations. - Ensured features do not directly import `firebase_auth`, adhering to architecture rules. feat: Create repository interfaces for billing and coverage - Added `BillingRepositoryInterface` for billing-related operations. - Created `CoverageRepositoryInterface` for coverage data access. feat: Add use cases for order management - Implemented use cases for fetching hubs, managers, and roles related to orders. - Created `GetHubsUseCase`, `GetManagersByHubUseCase`, and `GetRolesByVendorUseCase`. feat: Develop report use cases for client reports - Added use cases for fetching various reports including coverage, daily operations, forecast, no-show, performance, and spend reports. - Implemented `GetCoverageReportUseCase`, `GetDailyOpsReportUseCase`, `GetForecastReportUseCase`, `GetNoShowReportUseCase`, `GetPerformanceReportUseCase`, and `GetSpendReportUseCase`. feat: Establish profile repository and use cases - Created `ProfileRepositoryInterface` for staff profile data access. - Implemented use cases for retrieving staff profile and section statuses: `GetStaffProfileUseCase` and `GetProfileSectionsUseCase`. - Added `SignOutUseCase` for signing out the current user.
This commit is contained in:
@@ -3,7 +3,7 @@ import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:billing/src/data/repositories_impl/billing_repository_impl.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
import 'package:billing/src/domain/usecases/approve_invoice.dart';
|
||||
import 'package:billing/src/domain/usecases/dispute_invoice.dart';
|
||||
import 'package:billing/src/domain/usecases/get_bank_accounts.dart';
|
||||
@@ -29,8 +29,8 @@ class BillingModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<BillingRepository>(
|
||||
() => BillingRepositoryImpl(apiService: i.get<BaseApiService>()),
|
||||
i.addLazySingleton<BillingRepositoryInterface>(
|
||||
() => BillingRepositoryInterfaceImpl(apiService: i.get<BaseApiService>()),
|
||||
);
|
||||
|
||||
// Use Cases
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
|
||||
/// Implementation of [BillingRepository] using the V2 REST API.
|
||||
/// Implementation of [BillingRepositoryInterface] using the V2 REST API.
|
||||
///
|
||||
/// All backend calls go through [BaseApiService] with [ClientEndpoints].
|
||||
class BillingRepositoryImpl implements BillingRepository {
|
||||
/// Creates a [BillingRepositoryImpl].
|
||||
BillingRepositoryImpl({required BaseApiService apiService})
|
||||
class BillingRepositoryInterfaceImpl implements BillingRepositoryInterface {
|
||||
/// Creates a [BillingRepositoryInterfaceImpl].
|
||||
BillingRepositoryInterfaceImpl({required BaseApiService apiService})
|
||||
: _apiService = apiService;
|
||||
|
||||
/// The API service used for all HTTP requests.
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
/// This interface defines the contract for accessing billing-related data,
|
||||
/// acting as a boundary between the Domain and Data layers.
|
||||
/// It allows the Domain layer to remain independent of specific data sources.
|
||||
abstract class BillingRepository {
|
||||
abstract class BillingRepositoryInterface {
|
||||
/// Fetches bank accounts associated with the business.
|
||||
Future<List<BillingAccount>> getBankAccounts();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
|
||||
/// Use case for approving an invoice.
|
||||
class ApproveInvoiceUseCase extends UseCase<String, void> {
|
||||
@@ -8,7 +8,7 @@ class ApproveInvoiceUseCase extends UseCase<String, void> {
|
||||
ApproveInvoiceUseCase(this._repository);
|
||||
|
||||
/// The billing repository.
|
||||
final BillingRepository _repository;
|
||||
final BillingRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(String input) => _repository.approveInvoice(input);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
|
||||
/// Params for [DisputeInvoiceUseCase].
|
||||
class DisputeInvoiceParams {
|
||||
@@ -20,7 +20,7 @@ class DisputeInvoiceUseCase extends UseCase<DisputeInvoiceParams, void> {
|
||||
DisputeInvoiceUseCase(this._repository);
|
||||
|
||||
/// The billing repository.
|
||||
final BillingRepository _repository;
|
||||
final BillingRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(DisputeInvoiceParams input) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
|
||||
/// Use case for fetching the bank accounts associated with the business.
|
||||
class GetBankAccountsUseCase extends NoInputUseCase<List<BillingAccount>> {
|
||||
@@ -9,7 +9,7 @@ class GetBankAccountsUseCase extends NoInputUseCase<List<BillingAccount>> {
|
||||
GetBankAccountsUseCase(this._repository);
|
||||
|
||||
/// The billing repository.
|
||||
final BillingRepository _repository;
|
||||
final BillingRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<BillingAccount>> call() => _repository.getBankAccounts();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
|
||||
/// Use case for fetching the current bill amount in cents.
|
||||
///
|
||||
/// Delegates data retrieval to the [BillingRepository].
|
||||
/// Delegates data retrieval to the [BillingRepositoryInterface].
|
||||
class GetCurrentBillAmountUseCase extends NoInputUseCase<int> {
|
||||
/// Creates a [GetCurrentBillAmountUseCase].
|
||||
GetCurrentBillAmountUseCase(this._repository);
|
||||
|
||||
/// The billing repository.
|
||||
final BillingRepository _repository;
|
||||
final BillingRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<int> call() => _repository.getCurrentBillCents();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
|
||||
/// Use case for fetching the invoice history.
|
||||
///
|
||||
@@ -11,7 +11,7 @@ class GetInvoiceHistoryUseCase extends NoInputUseCase<List<Invoice>> {
|
||||
GetInvoiceHistoryUseCase(this._repository);
|
||||
|
||||
/// The billing repository.
|
||||
final BillingRepository _repository;
|
||||
final BillingRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<Invoice>> call() => _repository.getInvoiceHistory();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
|
||||
/// Use case for fetching the pending invoices.
|
||||
///
|
||||
@@ -11,7 +11,7 @@ class GetPendingInvoicesUseCase extends NoInputUseCase<List<Invoice>> {
|
||||
GetPendingInvoicesUseCase(this._repository);
|
||||
|
||||
/// The billing repository.
|
||||
final BillingRepository _repository;
|
||||
final BillingRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<Invoice>> call() => _repository.getPendingInvoices();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
|
||||
/// Use case for fetching the savings amount in cents.
|
||||
///
|
||||
/// Delegates data retrieval to the [BillingRepository].
|
||||
/// Delegates data retrieval to the [BillingRepositoryInterface].
|
||||
class GetSavingsAmountUseCase extends NoInputUseCase<int> {
|
||||
/// Creates a [GetSavingsAmountUseCase].
|
||||
GetSavingsAmountUseCase(this._repository);
|
||||
|
||||
/// The billing repository.
|
||||
final BillingRepository _repository;
|
||||
final BillingRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<int> call() => _repository.getSavingsCents();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:billing/src/domain/repositories/billing_repository.dart';
|
||||
import 'package:billing/src/domain/repositories/billing_repository_interface.dart';
|
||||
|
||||
/// Parameters for [GetSpendBreakdownUseCase].
|
||||
class SpendBreakdownParams {
|
||||
@@ -20,14 +20,14 @@ class SpendBreakdownParams {
|
||||
|
||||
/// Use case for fetching the spending breakdown by category.
|
||||
///
|
||||
/// Delegates data retrieval to the [BillingRepository].
|
||||
/// Delegates data retrieval to the [BillingRepositoryInterface].
|
||||
class GetSpendBreakdownUseCase
|
||||
extends UseCase<SpendBreakdownParams, List<SpendItem>> {
|
||||
/// Creates a [GetSpendBreakdownUseCase].
|
||||
GetSpendBreakdownUseCase(this._repository);
|
||||
|
||||
/// The billing repository.
|
||||
final BillingRepository _repository;
|
||||
final BillingRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<SpendItem>> call(SpendBreakdownParams input) =>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
@@ -14,6 +12,9 @@ import 'package:billing/src/presentation/blocs/billing_event.dart';
|
||||
import 'package:billing/src/presentation/blocs/billing_state.dart';
|
||||
|
||||
/// BLoC for managing billing state and data loading.
|
||||
///
|
||||
/// Fetches billing summary data (current bill, savings, invoices,
|
||||
/// spend breakdown, bank accounts) and manages period tab selection.
|
||||
class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
with BlocErrorHandler<BillingState> {
|
||||
/// Creates a [BillingBloc] with the given use cases.
|
||||
@@ -35,64 +36,97 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
on<BillingPeriodChanged>(_onPeriodChanged);
|
||||
}
|
||||
|
||||
/// Use case for fetching bank accounts.
|
||||
final GetBankAccountsUseCase _getBankAccounts;
|
||||
|
||||
/// Use case for fetching the current bill amount.
|
||||
final GetCurrentBillAmountUseCase _getCurrentBillAmount;
|
||||
|
||||
/// Use case for fetching the savings amount.
|
||||
final GetSavingsAmountUseCase _getSavingsAmount;
|
||||
|
||||
/// Use case for fetching pending invoices.
|
||||
final GetPendingInvoicesUseCase _getPendingInvoices;
|
||||
|
||||
/// Use case for fetching invoice history.
|
||||
final GetInvoiceHistoryUseCase _getInvoiceHistory;
|
||||
|
||||
/// Use case for fetching spending breakdown.
|
||||
final GetSpendBreakdownUseCase _getSpendBreakdown;
|
||||
|
||||
/// Executes [loader] and returns null on failure, logging the error.
|
||||
Future<T?> _loadSafe<T>(Future<T> Function() loader) async {
|
||||
try {
|
||||
return await loader();
|
||||
} catch (e, stackTrace) {
|
||||
developer.log(
|
||||
'Partial billing load failed: $e',
|
||||
name: 'BillingBloc',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads all billing data concurrently.
|
||||
///
|
||||
/// Uses [handleError] to surface errors to the UI via state
|
||||
/// instead of silently swallowing them. Individual data fetches
|
||||
/// use [handleErrorWithResult] so partial failures populate
|
||||
/// with defaults rather than failing the entire load.
|
||||
Future<void> _onLoadStarted(
|
||||
BillingLoadStarted event,
|
||||
Emitter<BillingState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: BillingStatus.loading));
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
emit(state.copyWith(status: BillingStatus.loading));
|
||||
|
||||
final SpendBreakdownParams spendParams = _dateRangeFor(state.periodTab);
|
||||
final SpendBreakdownParams spendParams =
|
||||
_dateRangeFor(state.periodTab);
|
||||
|
||||
final List<Object?> results = await Future.wait<Object?>(
|
||||
<Future<Object?>>[
|
||||
_loadSafe<int>(() => _getCurrentBillAmount.call()),
|
||||
_loadSafe<int>(() => _getSavingsAmount.call()),
|
||||
_loadSafe<List<Invoice>>(() => _getPendingInvoices.call()),
|
||||
_loadSafe<List<Invoice>>(() => _getInvoiceHistory.call()),
|
||||
_loadSafe<List<SpendItem>>(() => _getSpendBreakdown.call(spendParams)),
|
||||
_loadSafe<List<BillingAccount>>(() => _getBankAccounts.call()),
|
||||
],
|
||||
);
|
||||
final List<Object?> results = await Future.wait<Object?>(
|
||||
<Future<Object?>>[
|
||||
handleErrorWithResult<int>(
|
||||
action: () => _getCurrentBillAmount.call(),
|
||||
onError: (_) {},
|
||||
),
|
||||
handleErrorWithResult<int>(
|
||||
action: () => _getSavingsAmount.call(),
|
||||
onError: (_) {},
|
||||
),
|
||||
handleErrorWithResult<List<Invoice>>(
|
||||
action: () => _getPendingInvoices.call(),
|
||||
onError: (_) {},
|
||||
),
|
||||
handleErrorWithResult<List<Invoice>>(
|
||||
action: () => _getInvoiceHistory.call(),
|
||||
onError: (_) {},
|
||||
),
|
||||
handleErrorWithResult<List<SpendItem>>(
|
||||
action: () => _getSpendBreakdown.call(spendParams),
|
||||
onError: (_) {},
|
||||
),
|
||||
handleErrorWithResult<List<BillingAccount>>(
|
||||
action: () => _getBankAccounts.call(),
|
||||
onError: (_) {},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final int? currentBillCents = results[0] as int?;
|
||||
final int? savingsCents = results[1] as int?;
|
||||
final List<Invoice>? pendingInvoices = results[2] as List<Invoice>?;
|
||||
final List<Invoice>? invoiceHistory = results[3] as List<Invoice>?;
|
||||
final List<SpendItem>? spendBreakdown = results[4] as List<SpendItem>?;
|
||||
final List<BillingAccount>? bankAccounts =
|
||||
results[5] as List<BillingAccount>?;
|
||||
final int? currentBillCents = results[0] as int?;
|
||||
final int? savingsCents = results[1] as int?;
|
||||
final List<Invoice>? pendingInvoices =
|
||||
results[2] as List<Invoice>?;
|
||||
final List<Invoice>? invoiceHistory =
|
||||
results[3] as List<Invoice>?;
|
||||
final List<SpendItem>? spendBreakdown =
|
||||
results[4] as List<SpendItem>?;
|
||||
final List<BillingAccount>? bankAccounts =
|
||||
results[5] as List<BillingAccount>?;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: BillingStatus.success,
|
||||
currentBillCents: currentBillCents ?? state.currentBillCents,
|
||||
savingsCents: savingsCents ?? state.savingsCents,
|
||||
pendingInvoices: pendingInvoices ?? state.pendingInvoices,
|
||||
invoiceHistory: invoiceHistory ?? state.invoiceHistory,
|
||||
spendBreakdown: spendBreakdown ?? state.spendBreakdown,
|
||||
bankAccounts: bankAccounts ?? state.bankAccounts,
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: BillingStatus.success,
|
||||
currentBillCents: currentBillCents ?? state.currentBillCents,
|
||||
savingsCents: savingsCents ?? state.savingsCents,
|
||||
pendingInvoices: pendingInvoices ?? state.pendingInvoices,
|
||||
invoiceHistory: invoiceHistory ?? state.invoiceHistory,
|
||||
spendBreakdown: spendBreakdown ?? state.spendBreakdown,
|
||||
bankAccounts: bankAccounts ?? state.bankAccounts,
|
||||
),
|
||||
);
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: BillingStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
|
||||
final DateFormat formatter = DateFormat('EEEE, MMMM d');
|
||||
final String dateLabel = resolvedInvoice.dueDate != null
|
||||
? formatter.format(resolvedInvoice.dueDate!)
|
||||
: 'N/A';
|
||||
: 'N/A'; // TODO: localize
|
||||
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(
|
||||
@@ -85,7 +85,7 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
|
||||
bottomNavigationBar: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: UiColors.primaryForeground,
|
||||
border: Border(
|
||||
top: BorderSide(color: UiColors.border.withValues(alpha: 0.5)),
|
||||
),
|
||||
|
||||
@@ -95,8 +95,8 @@ class CompletionReviewActions extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) => AlertDialog(
|
||||
title: Text(t.client_billing.flag_dialog.title),
|
||||
surfaceTintColor: Colors.white,
|
||||
backgroundColor: Colors.white,
|
||||
surfaceTintColor: UiColors.primaryForeground,
|
||||
backgroundColor: UiColors.primaryForeground,
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
|
||||
@@ -23,7 +23,7 @@ class CompletionReviewSearchAndTabs extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
color: UiColors.muted,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: TextField(
|
||||
@@ -69,17 +69,17 @@ class CompletionReviewSearchAndTabs extends StatelessWidget {
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF2563EB) : Colors.white,
|
||||
color: isSelected ? UiColors.primary : UiColors.white,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
border: Border.all(
|
||||
color: isSelected ? const Color(0xFF2563EB) : UiColors.border,
|
||||
color: isSelected ? UiColors.primary : UiColors.border,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: isSelected ? Colors.white : UiColors.textSecondary,
|
||||
color: isSelected ? UiColors.primaryForeground : UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -33,7 +33,7 @@ class PendingInvoicesSection extends StatelessWidget {
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
color: UiColors.textWarning,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
@@ -101,7 +101,7 @@ class PendingInvoiceCard extends StatelessWidget {
|
||||
final DateFormat formatter = DateFormat('EEEE, MMMM d');
|
||||
final String dateLabel = invoice.dueDate != null
|
||||
? formatter.format(invoice.dueDate!)
|
||||
: 'N/A';
|
||||
: 'N/A'; // TODO: localize
|
||||
final double amountDollars = invoice.amountCents / 100.0;
|
||||
|
||||
return Container(
|
||||
|
||||
Reference in New Issue
Block a user