brakedown by week and month

This commit is contained in:
José Salazar
2026-01-26 17:51:30 -05:00
parent f65121a26f
commit fd06391e63
19 changed files with 18566 additions and 17804 deletions

View File

@@ -23,6 +23,7 @@ class BillingModule extends Module {
i.addSingleton<BillingRepository>(
() => BillingRepositoryImpl(
financialRepository: i.get<FinancialRepositoryMock>(),
dataConnect: ExampleConnector.instance,
),
);

View File

@@ -1,5 +1,7 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:krow_data_connect/krow_data_connect.dart' as data_connect;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/models/billing_period.dart';
import '../../domain/repositories/billing_repository.dart';
/// Implementation of [BillingRepository] in the Data layer.
@@ -16,9 +18,12 @@ class BillingRepositoryImpl implements BillingRepository {
/// Requires the [financialRepository] to fetch financial data.
BillingRepositoryImpl({
required data_connect.FinancialRepositoryMock financialRepository,
}) : _financialRepository = financialRepository;
required data_connect.ExampleConnector dataConnect,
}) : _financialRepository = financialRepository,
_dataConnect = dataConnect;
final data_connect.FinancialRepositoryMock _financialRepository;
final data_connect.ExampleConnector _dataConnect;
/// Fetches the current bill amount by aggregating open invoices.
@override
@@ -72,9 +77,114 @@ class BillingRepositoryImpl implements BillingRepository {
/// Fetches the breakdown of spending.
@override
Future<List<InvoiceItem>> getSpendingBreakdown() async {
// Assuming breakdown is based on the current period's invoice items.
// We fetch items for a dummy invoice ID representing the current period.
return _financialRepository.getInvoiceItems('current_period_invoice');
Future<List<InvoiceItem>> getSpendingBreakdown(BillingPeriod period) async {
final String? businessId =
data_connect.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
return <InvoiceItem>[];
}
final DateTime now = DateTime.now();
final DateTime start;
final DateTime end;
if (period == BillingPeriod.week) {
final int daysFromMonday = now.weekday - DateTime.monday;
final DateTime monday = DateTime(
now.year,
now.month,
now.day,
).subtract(Duration(days: daysFromMonday));
start = DateTime(monday.year, monday.month, monday.day);
end = DateTime(monday.year, monday.month, monday.day + 6, 23, 59, 59, 999);
} else {
start = DateTime(now.year, now.month, 1);
end = DateTime(now.year, now.month + 1, 0, 23, 59, 59, 999);
}
final fdc.QueryResult<
data_connect.ListShiftRolesByBusinessAndDatesSummaryData,
data_connect.ListShiftRolesByBusinessAndDatesSummaryVariables> result =
await _dataConnect
.listShiftRolesByBusinessAndDatesSummary(
businessId: businessId,
start: _toTimestamp(start),
end: _toTimestamp(end),
)
.execute();
final List<data_connect.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
shiftRoles = result.data.shiftRoles;
if (shiftRoles.isEmpty) {
return <InvoiceItem>[];
}
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{};
for (final data_connect.ListShiftRolesByBusinessAndDatesSummaryShiftRoles role
in shiftRoles) {
final String roleId = role.roleId;
final String roleName = role.role.name;
final double hours = role.hours ?? 0.0;
final double totalValue = role.totalValue ?? 0.0;
final _RoleSummary? existing = summary[roleId];
if (existing == null) {
summary[roleId] = _RoleSummary(
roleId: roleId,
roleName: roleName,
totalHours: hours,
totalValue: totalValue,
);
} else {
summary[roleId] = existing.copyWith(
totalHours: existing.totalHours + hours,
totalValue: existing.totalValue + totalValue,
);
}
}
return summary.values
.map(
(_RoleSummary item) => InvoiceItem(
id: item.roleId,
invoiceId: item.roleId,
staffId: item.roleName,
workHours: item.totalHours,
rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0,
amount: item.totalValue,
),
)
.toList();
}
fdc.Timestamp _toTimestamp(DateTime dateTime) {
final int seconds = dateTime.millisecondsSinceEpoch ~/ 1000;
final int nanoseconds =
(dateTime.millisecondsSinceEpoch % 1000) * 1000000;
return fdc.Timestamp(nanoseconds, seconds);
}
}
class _RoleSummary {
const _RoleSummary({
required this.roleId,
required this.roleName,
required this.totalHours,
required this.totalValue,
});
final String roleId;
final String roleName;
final double totalHours;
final double totalValue;
_RoleSummary copyWith({
double? totalHours,
double? totalValue,
}) {
return _RoleSummary(
roleId: roleId,
roleName: roleName,
totalHours: totalHours ?? this.totalHours,
totalValue: totalValue ?? this.totalValue,
);
}
}

View File

@@ -0,0 +1,4 @@
enum BillingPeriod {
week,
month,
}

View File

@@ -1,4 +1,5 @@
import 'package:krow_domain/krow_domain.dart';
import '../models/billing_period.dart';
/// Repository interface for billing related operations.
///
@@ -19,5 +20,5 @@ abstract class BillingRepository {
Future<double> getSavingsAmount();
/// Fetches invoice items for spending breakdown analysis.
Future<List<InvoiceItem>> getSpendingBreakdown();
Future<List<InvoiceItem>> getSpendingBreakdown(BillingPeriod period);
}

View File

@@ -1,17 +1,20 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../models/billing_period.dart';
import '../repositories/billing_repository.dart';
/// Use case for fetching the spending breakdown items.
///
/// This use case encapsulates the logic for retrieving the spending breakdown by category or item.
/// It delegates the data retrieval to the [BillingRepository].
class GetSpendingBreakdownUseCase extends NoInputUseCase<List<InvoiceItem>> {
class GetSpendingBreakdownUseCase
extends UseCase<BillingPeriod, List<InvoiceItem>> {
/// Creates a [GetSpendingBreakdownUseCase].
GetSpendingBreakdownUseCase(this._repository);
final BillingRepository _repository;
@override
Future<List<InvoiceItem>> call() => _repository.getSpendingBreakdown();
Future<List<InvoiceItem>> call(BillingPeriod period) =>
_repository.getSpendingBreakdown(period);
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/models/billing_period.dart';
import '../../domain/usecases/get_current_bill_amount.dart';
import '../../domain/usecases/get_invoice_history.dart';
import '../../domain/usecases/get_pending_invoices.dart';
@@ -26,6 +27,7 @@ class BillingBloc extends Bloc<BillingEvent, BillingState> {
_getSpendingBreakdown = getSpendingBreakdown,
super(const BillingState()) {
on<BillingLoadStarted>(_onLoadStarted);
on<BillingPeriodChanged>(_onPeriodChanged);
}
final GetCurrentBillAmountUseCase _getCurrentBillAmount;
@@ -45,7 +47,7 @@ class BillingBloc extends Bloc<BillingEvent, BillingState> {
_getSavingsAmount.call(),
_getPendingInvoices.call(),
_getInvoiceHistory.call(),
_getSpendingBreakdown.call(),
_getSpendingBreakdown.call(state.period),
]);
final double currentBill = results[0] as double;
@@ -83,6 +85,31 @@ class BillingBloc extends Bloc<BillingEvent, BillingState> {
}
}
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);
emit(
state.copyWith(
period: event.period,
spendingBreakdown: uiSpendingBreakdown,
),
);
} catch (e) {
emit(
state.copyWith(
status: BillingStatus.failure,
errorMessage: e.toString(),
),
);
}
}
BillingInvoice _mapInvoiceToUiModel(Invoice invoice) {
// In a real app, fetches related Event/Business names via ID.
// For now, mapping available fields and hardcoding missing UI placeholders.
@@ -104,16 +131,11 @@ class BillingBloc extends Bloc<BillingEvent, BillingState> {
List<SpendingBreakdownItem> _mapSpendingItemsToUiModel(
List<InvoiceItem> items,
) {
// Aggregating items by some logic.
// Since InvoiceItem doesn't have category, we mock it based on staffId or similar.
final Map<String, SpendingBreakdownItem> aggregation = <String, SpendingBreakdownItem>{};
final Map<String, SpendingBreakdownItem> aggregation =
<String, SpendingBreakdownItem>{};
for (final InvoiceItem item in items) {
// Mocking category derivation
final String category = item.staffId.hashCode % 2 == 0
? 'Server Staff'
: 'Bar Staff';
final String category = item.staffId;
final SpendingBreakdownItem? existing = aggregation[category];
if (existing != null) {
aggregation[category] = SpendingBreakdownItem(

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart';
import '../../domain/models/billing_period.dart';
/// Base class for all billing events.
abstract class BillingEvent extends Equatable {
@@ -14,3 +15,12 @@ class BillingLoadStarted extends BillingEvent {
/// Creates a [BillingLoadStarted] event.
const BillingLoadStarted();
}
class BillingPeriodChanged extends BillingEvent {
const BillingPeriodChanged(this.period);
final BillingPeriod period;
@override
List<Object?> get props => <Object?>[period];
}

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart';
import '../../domain/models/billing_period.dart';
import '../models/billing_invoice_model.dart';
import '../models/spending_breakdown_model.dart';
@@ -27,6 +28,7 @@ class BillingState extends Equatable {
this.pendingInvoices = const <BillingInvoice>[],
this.invoiceHistory = const <BillingInvoice>[],
this.spendingBreakdown = const <SpendingBreakdownItem>[],
this.period = BillingPeriod.week,
this.errorMessage,
});
@@ -48,6 +50,9 @@ class BillingState extends Equatable {
/// Breakdown of spending by category.
final List<SpendingBreakdownItem> spendingBreakdown;
/// Selected period for the breakdown.
final BillingPeriod period;
/// Error message if loading failed.
final String? errorMessage;
@@ -59,6 +64,7 @@ class BillingState extends Equatable {
List<BillingInvoice>? pendingInvoices,
List<BillingInvoice>? invoiceHistory,
List<SpendingBreakdownItem>? spendingBreakdown,
BillingPeriod? period,
String? errorMessage,
}) {
return BillingState(
@@ -68,6 +74,7 @@ class BillingState extends Equatable {
pendingInvoices: pendingInvoices ?? this.pendingInvoices,
invoiceHistory: invoiceHistory ?? this.invoiceHistory,
spendingBreakdown: spendingBreakdown ?? this.spendingBreakdown,
period: period ?? this.period,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@@ -80,6 +87,7 @@ class BillingState extends Equatable {
pendingInvoices,
invoiceHistory,
spendingBreakdown,
period,
errorMessage,
];
}

View File

@@ -2,8 +2,10 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/models/billing_period.dart';
import '../blocs/billing_bloc.dart';
import '../blocs/billing_state.dart';
import '../blocs/billing_event.dart';
import '../models/spending_breakdown_model.dart';
/// Card showing the spending breakdown for the current period.
@@ -92,6 +94,13 @@ class _SpendingBreakdownCardState extends State<SpendingBreakdownCard>
indicatorSize: TabBarIndicatorSize.tab,
labelPadding: const EdgeInsets.symmetric(horizontal: 12),
dividerColor: Colors.transparent,
onTap: (int index) {
final BillingPeriod period =
index == 0 ? BillingPeriod.week : BillingPeriod.month;
context.read<BillingBloc>().add(
BillingPeriodChanged(period),
);
},
tabs: <Widget>[
Tab(text: t.client_billing.week),
Tab(text: t.client_billing.month),