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:
Achintha Isuru
2026-03-19 01:10:27 -04:00
parent a45a3f6af1
commit 843eec5692
123 changed files with 2102 additions and 1087 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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();

View File

@@ -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);

View File

@@ -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) =>

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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) =>

View File

@@ -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,
),
);
}

View File

@@ -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)),
),

View File

@@ -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(

View File

@@ -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,
),
),
),

View File

@@ -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(