@@ -23,6 +23,7 @@ class BillingModule extends Module {
|
||||
i.addSingleton<BillingRepository>(
|
||||
() => BillingRepositoryImpl(
|
||||
financialRepository: i.get<FinancialRepositoryMock>(),
|
||||
dataConnect: ExampleConnector.instance,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -39,12 +44,22 @@ class BillingRepositoryImpl implements BillingRepository {
|
||||
/// Fetches the history of paid invoices.
|
||||
@override
|
||||
Future<List<Invoice>> getInvoiceHistory() async {
|
||||
final List<Invoice> invoices = await _financialRepository.getInvoices(
|
||||
'current_business',
|
||||
);
|
||||
return invoices
|
||||
.where((Invoice i) => i.status == InvoiceStatus.paid)
|
||||
.toList();
|
||||
final String? businessId =
|
||||
data_connect.ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return <Invoice>[];
|
||||
}
|
||||
|
||||
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
|
||||
data_connect.ListInvoicesByBusinessIdVariables> result =
|
||||
await _dataConnect
|
||||
.listInvoicesByBusinessId(
|
||||
businessId: businessId,
|
||||
)
|
||||
.limit(10)
|
||||
.execute();
|
||||
|
||||
return result.data.invoices.map(_mapInvoice).toList();
|
||||
}
|
||||
|
||||
/// Fetches pending invoices (Open or Disputed).
|
||||
@@ -66,15 +81,156 @@ class BillingRepositoryImpl implements BillingRepository {
|
||||
@override
|
||||
Future<double> getSavingsAmount() async {
|
||||
// Simulating savings calculation (e.g., comparing to market rates).
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
return 320.00;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 0));
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
Invoice _mapInvoice(data_connect.ListInvoicesByBusinessIdInvoices invoice) {
|
||||
return Invoice(
|
||||
id: invoice.id,
|
||||
eventId: invoice.orderId,
|
||||
businessId: invoice.businessId,
|
||||
status: _mapInvoiceStatus(invoice.status),
|
||||
totalAmount: invoice.amount,
|
||||
workAmount: invoice.amount,
|
||||
addonsAmount: invoice.otherCharges ?? 0,
|
||||
invoiceNumber: invoice.invoiceNumber,
|
||||
issueDate: invoice.issueDate.toDateTime(),
|
||||
);
|
||||
}
|
||||
|
||||
InvoiceStatus _mapInvoiceStatus(
|
||||
data_connect.EnumValue<data_connect.InvoiceStatus> status,
|
||||
) {
|
||||
if (status is data_connect.Known<data_connect.InvoiceStatus>) {
|
||||
switch (status.value) {
|
||||
case data_connect.InvoiceStatus.PAID:
|
||||
return InvoiceStatus.paid;
|
||||
case data_connect.InvoiceStatus.OVERDUE:
|
||||
return InvoiceStatus.overdue;
|
||||
case data_connect.InvoiceStatus.DISPUTED:
|
||||
return InvoiceStatus.disputed;
|
||||
case data_connect.InvoiceStatus.APPROVED:
|
||||
return InvoiceStatus.verified;
|
||||
case data_connect.InvoiceStatus.PENDING_REVIEW:
|
||||
case data_connect.InvoiceStatus.PENDING:
|
||||
case data_connect.InvoiceStatus.DRAFT:
|
||||
return InvoiceStatus.open;
|
||||
}
|
||||
}
|
||||
return InvoiceStatus.open;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
enum BillingPeriod {
|
||||
week,
|
||||
month,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -62,11 +64,15 @@ class BillingBloc extends Bloc<BillingEvent, BillingState> {
|
||||
.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: currentBill,
|
||||
currentBill: periodTotal,
|
||||
savings: savings,
|
||||
pendingInvoices: uiPendingInvoices,
|
||||
invoiceHistory: uiInvoiceHistory,
|
||||
@@ -83,37 +89,66 @@ 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);
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// Preserving "Existing Behavior" means we show something.
|
||||
final String dateLabel = invoice.issueDate == null
|
||||
? '2024-01-24'
|
||||
: invoice.issueDate!.toIso8601String().split('T').first;
|
||||
final String titleLabel = invoice.invoiceNumber ?? invoice.id;
|
||||
return BillingInvoice(
|
||||
id: invoice.id,
|
||||
id: titleLabel,
|
||||
title: 'Invoice #${invoice.id}', // Placeholder as Invoice lacks title
|
||||
locationAddress:
|
||||
'Location for ${invoice.eventId}', // Placeholder for address
|
||||
clientName: 'Client ${invoice.businessId}', // Placeholder for client name
|
||||
date: '2024-01-24', // Placeholder date
|
||||
date: dateLabel,
|
||||
totalAmount: invoice.totalAmount,
|
||||
workersCount: 5, // Placeholder count
|
||||
totalHours: invoice.workAmount / 25.0, // Estimating hours from amount
|
||||
status: invoice.status.name,
|
||||
status: invoice.status.name.toUpperCase(),
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class _InvoiceItem extends StatelessWidget {
|
||||
'\$${invoice.totalAmount.toStringAsFixed(2)}',
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
const _PaidBadge(),
|
||||
_StatusBadge(status: invoice.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
@@ -125,21 +125,24 @@ class _InvoiceItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PaidBadge extends StatelessWidget {
|
||||
const _PaidBadge();
|
||||
class _StatusBadge extends StatelessWidget {
|
||||
const _StatusBadge({required this.status});
|
||||
|
||||
final String status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isPaid = status.toUpperCase() == 'PAID';
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagSuccess,
|
||||
color: isPaid ? UiColors.tagSuccess : UiColors.tagPending,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
t.client_billing.paid_badge,
|
||||
isPaid ? t.client_billing.paid_badge : t.client_billing.pending_badge,
|
||||
style: UiTypography.titleUppercase4b.copyWith(
|
||||
color: UiColors.iconSuccess,
|
||||
color: isPaid ? UiColors.iconSuccess : UiColors.textWarning,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,111 +1,176 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
|
||||
/// Card showing the current payment method.
|
||||
class PaymentMethodCard extends StatelessWidget {
|
||||
class PaymentMethodCard extends StatefulWidget {
|
||||
/// Creates a [PaymentMethodCard].
|
||||
const PaymentMethodCard({super.key});
|
||||
|
||||
@override
|
||||
State<PaymentMethodCard> createState() => _PaymentMethodCardState();
|
||||
}
|
||||
|
||||
class _PaymentMethodCardState extends State<PaymentMethodCard> {
|
||||
late final Future<dc.GetAccountsByOwnerIdData?> _accountsFuture =
|
||||
_loadAccounts();
|
||||
|
||||
Future<dc.GetAccountsByOwnerIdData?> _loadAccounts() async {
|
||||
final String? businessId =
|
||||
dc.ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final fdc.QueryResult<dc.GetAccountsByOwnerIdData,
|
||||
dc.GetAccountsByOwnerIdVariables> result =
|
||||
await dc.ExampleConnector.instance
|
||||
.getAccountsByOwnerId(ownerId: businessId)
|
||||
.execute();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_billing.payment_method,
|
||||
style: UiTypography.title2b.textPrimary,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.add, size: 14, color: UiColors.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
t.client_billing.add_payment,
|
||||
style: UiTypography.footnote2b.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
return FutureBuilder<dc.GetAccountsByOwnerIdData?>(
|
||||
future: _accountsFuture,
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<dc.GetAccountsByOwnerIdData?> snapshot) {
|
||||
final List<dc.GetAccountsByOwnerIdAccounts> accounts =
|
||||
snapshot.data?.accounts ??
|
||||
<dc.GetAccountsByOwnerIdAccounts>[];
|
||||
final dc.GetAccountsByOwnerIdAccounts? account =
|
||||
accounts.isNotEmpty ? accounts.first : null;
|
||||
final String bankLabel =
|
||||
account?.bank.isNotEmpty == true ? account!.bank : '----';
|
||||
final String last4 =
|
||||
account?.last4.isNotEmpty == true ? account!.last4 : '----';
|
||||
final bool isPrimary = account?.isPrimary ?? false;
|
||||
final String expiryLabel = _formatExpiry(account?.expiryTime);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 40,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_billing.payment_method,
|
||||
style: UiTypography.title2b.textPrimary,
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'VISA',
|
||||
style: TextStyle(
|
||||
color: UiColors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.add,
|
||||
size: 14,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
t.client_billing.add_payment,
|
||||
style: UiTypography.footnote2b.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
],
|
||||
),
|
||||
if (account != null) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text('•••• 4242', style: UiTypography.body2b.textPrimary),
|
||||
Text(
|
||||
t.client_billing.expires(date: '12/25'),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
Container(
|
||||
width: 40,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
bankLabel,
|
||||
style: const TextStyle(
|
||||
color: UiColors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'•••• $last4',
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
Text(
|
||||
t.client_billing.expires(date: expiryLabel),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isPrimary)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.accent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
t.client_billing.default_badge,
|
||||
style: UiTypography.titleUppercase4b.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.accent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
t.client_billing.default_badge,
|
||||
style: UiTypography.titleUppercase4b.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatExpiry(fdc.Timestamp? expiryTime) {
|
||||
if (expiryTime == null) {
|
||||
return 'N/A';
|
||||
}
|
||||
final DateTime date = expiryTime.toDateTime();
|
||||
final String month = date.month.toString().padLeft(2, '0');
|
||||
final String year = (date.year % 100).toString().padLeft(2, '0');
|
||||
return '$month/$year';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user