feat: architecture overhaul, launchpad-style reports, and uber-style locations

- Strengthened Buffer Layer architecture to decouple Data Connect from Domain
- Rewired Coverage, Performance, and Forecast reports to match Launchpad logic
- Implemented Uber-style Preferred Locations search using Google Places API
- Added session recovery logic to prevent crashes on app restart
- Synchronized backend schemas & SDK for ShiftStatus enums
- Fixed various build/compilation errors and localization duplicates
This commit is contained in:
2026-02-20 17:20:06 +05:30
parent e6c4b51e84
commit 8849bf2273
60 changed files with 3804 additions and 2397 deletions

View File

@@ -1,261 +1,58 @@
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_data_connect/krow_data_connect.dart' as dc;
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.
/// Implementation of [BillingRepository] that delegates to [dc.BillingConnectorRepository].
///
/// This class is responsible for retrieving billing data from the
/// Data Connect layer and mapping it to Domain entities.
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
/// connector repository from the data_connect package.
class BillingRepositoryImpl implements BillingRepository {
/// Creates a [BillingRepositoryImpl].
final dc.BillingConnectorRepository _connectorRepository;
final dc.DataConnectService _service;
BillingRepositoryImpl({
data_connect.DataConnectService? service,
}) : _service = service ?? data_connect.DataConnectService.instance;
dc.BillingConnectorRepository? connectorRepository,
dc.DataConnectService? service,
}) : _connectorRepository = connectorRepository ??
dc.DataConnectService.instance.getBillingRepository(),
_service = service ?? dc.DataConnectService.instance;
final data_connect.DataConnectService _service;
/// Fetches bank accounts associated with the business.
@override
Future<List<BusinessBankAccount>> getBankAccounts() async {
return _service.run(() async {
final String businessId = await _service.getBusinessId();
final fdc.QueryResult<
data_connect.GetAccountsByOwnerIdData,
data_connect.GetAccountsByOwnerIdVariables> result =
await _service.connector
.getAccountsByOwnerId(ownerId: businessId)
.execute();
return result.data.accounts.map(_mapBankAccount).toList();
});
final businessId = await _service.getBusinessId();
return _connectorRepository.getBankAccounts(businessId: businessId);
}
/// Fetches the current bill amount by aggregating open invoices.
@override
Future<double> getCurrentBillAmount() async {
return _service.run(() async {
final String businessId = await _service.getBusinessId();
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
data_connect.ListInvoicesByBusinessIdVariables> result =
await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.execute();
return result.data.invoices
.map(_mapInvoice)
.where((Invoice i) => i.status == InvoiceStatus.open)
.fold<double>(
0.0,
(double sum, Invoice item) => sum + item.totalAmount,
);
});
final businessId = await _service.getBusinessId();
return _connectorRepository.getCurrentBillAmount(businessId: businessId);
}
/// Fetches the history of paid invoices.
@override
Future<List<Invoice>> getInvoiceHistory() async {
return _service.run(() async {
final String businessId = await _service.getBusinessId();
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
data_connect.ListInvoicesByBusinessIdVariables> result =
await _service.connector
.listInvoicesByBusinessId(
businessId: businessId,
)
.limit(10)
.execute();
return result.data.invoices.map(_mapInvoice).toList();
});
final businessId = await _service.getBusinessId();
return _connectorRepository.getInvoiceHistory(businessId: businessId);
}
/// Fetches pending invoices (Open or Disputed).
@override
Future<List<Invoice>> getPendingInvoices() async {
return _service.run(() async {
final String businessId = await _service.getBusinessId();
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
data_connect.ListInvoicesByBusinessIdVariables> result =
await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.execute();
return result.data.invoices
.map(_mapInvoice)
.where(
(Invoice i) =>
i.status == InvoiceStatus.open ||
i.status == InvoiceStatus.disputed,
)
.toList();
});
final businessId = await _service.getBusinessId();
return _connectorRepository.getPendingInvoices(businessId: businessId);
}
/// Fetches the estimated savings amount.
@override
Future<double> getSavingsAmount() async {
// Simulating savings calculation (e.g., comparing to market rates).
await Future<void>.delayed(const Duration(milliseconds: 0));
// Simulating savings calculation
return 0.0;
}
/// Fetches the breakdown of spending.
@override
Future<List<InvoiceItem>> getSpendingBreakdown(BillingPeriod period) async {
return _service.run(() async {
final String businessId = await _service.getBusinessId();
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 _service.connector
.listShiftRolesByBusinessAndDatesSummary(
businessId: businessId,
start: _service.toTimestamp(start),
end: _service.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();
});
}
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: _service.toDateTime(invoice.issueDate)!,
);
}
BusinessBankAccount _mapBankAccount(
data_connect.GetAccountsByOwnerIdAccounts account,
) {
return BusinessBankAccountAdapter.fromPrimitives(
id: account.id,
bank: account.bank,
last4: account.last4,
isPrimary: account.isPrimary ?? false,
expiryTime: _service.toDateTime(account.expiryTime),
);
}
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,
final businessId = await _service.getBusinessId();
return _connectorRepository.getSpendingBreakdown(
businessId: businessId,
period: period,
);
}
}

View File

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

View File

@@ -1,5 +1,4 @@
import 'package:krow_domain/krow_domain.dart';
import '../models/billing_period.dart';
/// Repository interface for billing related operations.
///

View File

@@ -1,6 +1,5 @@
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.

View File

@@ -1,5 +1,5 @@
import 'package:equatable/equatable.dart';
import '../../domain/models/billing_period.dart';
import 'package:krow_domain/krow_domain.dart';
/// Base class for all billing events.
abstract class BillingEvent extends Equatable {

View File

@@ -1,6 +1,5 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/models/billing_period.dart';
import '../models/billing_invoice_model.dart';
import '../models/spending_breakdown_model.dart';

View File

@@ -2,7 +2,7 @@ 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 'package:krow_domain/krow_domain.dart';
import '../blocs/billing_bloc.dart';
import '../blocs/billing_state.dart';
import '../blocs/billing_event.dart';