Refactor billing data parsing and filtering, update invoice queries, and remove the dedicated timesheets page.

This commit is contained in:
Achintha Isuru
2026-02-28 15:26:05 -05:00
parent 119b6cc000
commit 8c0708d2d3
8 changed files with 414 additions and 286 deletions

View File

@@ -6,16 +6,21 @@ import '../../domain/repositories/billing_connector_repository.dart';
/// Implementation of [BillingConnectorRepository].
class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
BillingConnectorRepositoryImpl({
dc.DataConnectService? service,
}) : _service = service ?? dc.DataConnectService.instance;
BillingConnectorRepositoryImpl({dc.DataConnectService? service})
: _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service;
@override
Future<List<BusinessBankAccount>> getBankAccounts({required String businessId}) async {
Future<List<BusinessBankAccount>> getBankAccounts({
required String businessId,
}) async {
return _service.run(() async {
final QueryResult<dc.GetAccountsByOwnerIdData, dc.GetAccountsByOwnerIdVariables> result = await _service.connector
final QueryResult<
dc.GetAccountsByOwnerIdData,
dc.GetAccountsByOwnerIdVariables
>
result = await _service.connector
.getAccountsByOwnerId(ownerId: businessId)
.execute();
@@ -26,21 +31,32 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
@override
Future<double> getCurrentBillAmount({required String businessId}) async {
return _service.run(() async {
final QueryResult<dc.ListInvoicesByBusinessIdData, dc.ListInvoicesByBusinessIdVariables> result = await _service.connector
final QueryResult<
dc.ListInvoicesByBusinessIdData,
dc.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);
.fold<double>(
0.0,
(double sum, Invoice item) => sum + item.totalAmount,
);
});
}
@override
Future<List<Invoice>> getInvoiceHistory({required String businessId}) async {
return _service.run(() async {
final QueryResult<dc.ListInvoicesByBusinessIdData, dc.ListInvoicesByBusinessIdVariables> result = await _service.connector
final QueryResult<
dc.ListInvoicesByBusinessIdData,
dc.ListInvoicesByBusinessIdVariables
>
result = await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.limit(20)
.execute();
@@ -55,14 +71,22 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
@override
Future<List<Invoice>> getPendingInvoices({required String businessId}) async {
return _service.run(() async {
final QueryResult<dc.ListInvoicesByBusinessIdData, dc.ListInvoicesByBusinessIdVariables> result = await _service.connector
final QueryResult<
dc.ListInvoicesByBusinessIdData,
dc.ListInvoicesByBusinessIdVariables
>
result = await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.execute();
return result.data.invoices
.map(_mapInvoice)
.where((Invoice i) =>
i.status != InvoiceStatus.paid)
.where(
(Invoice i) =>
i.status != InvoiceStatus.paid &&
i.status != InvoiceStatus.disputed &&
i.status != InvoiceStatus.open,
)
.toList();
});
}
@@ -76,19 +100,28 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
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));
final DateTime monday = DateTime(
now.year,
now.month,
now.day,
).subtract(Duration(days: daysFromMonday));
start = monday;
end = monday.add(const Duration(days: 6, hours: 23, minutes: 59, seconds: 59));
end = monday.add(
const Duration(days: 6, hours: 23, minutes: 59, seconds: 59),
);
} else {
start = DateTime(now.year, now.month, 1);
end = DateTime(now.year, now.month + 1, 0, 23, 59, 59);
}
final QueryResult<dc.ListShiftRolesByBusinessAndDatesSummaryData, dc.ListShiftRolesByBusinessAndDatesSummaryVariables> result = await _service.connector
final QueryResult<
dc.ListShiftRolesByBusinessAndDatesSummaryData,
dc.ListShiftRolesByBusinessAndDatesSummaryVariables
>
result = await _service.connector
.listShiftRolesByBusinessAndDatesSummary(
businessId: businessId,
start: _service.toTimestamp(start),
@@ -96,16 +129,18 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
)
.execute();
final List<dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles> shiftRoles = result.data.shiftRoles;
final List<dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
shiftRoles = result.data.shiftRoles;
if (shiftRoles.isEmpty) return <InvoiceItem>[];
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{};
for (final dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles role in shiftRoles) {
for (final dc.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(
@@ -123,14 +158,16 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
}
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,
))
.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();
});
}
@@ -146,7 +183,10 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
}
@override
Future<void> disputeInvoice({required String id, required String reason}) async {
Future<void> disputeInvoice({
required String id,
required String reason,
}) async {
return _service.run(() async {
await _service.connector
.updateInvoice(id: id)
@@ -159,36 +199,100 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
// --- MAPPERS ---
Invoice _mapInvoice(dynamic invoice) {
final List<dynamic> rolesData = invoice.roles is List ? invoice.roles : [];
final List<InvoiceWorker> workers = rolesData.map((dynamic r) {
final Map<String, dynamic> role = r as Map<String, dynamic>;
// Handle various possible key naming conventions in the JSON data
final String name = role['name'] ?? role['staffName'] ?? role['fullName'] ?? 'Unknown';
final String roleTitle = role['role'] ?? role['roleName'] ?? role['title'] ?? 'Staff';
final double amount = (role['amount'] as num?)?.toDouble() ??
(role['totalValue'] as num?)?.toDouble() ?? 0.0;
final double hours = (role['hours'] as num?)?.toDouble() ??
(role['workHours'] as num?)?.toDouble() ??
(role['totalHours'] as num?)?.toDouble() ?? 0.0;
final double rate = (role['rate'] as num?)?.toDouble() ??
(role['hourlyRate'] as num?)?.toDouble() ?? 0.0;
final dynamic checkInVal = role['checkInTime'] ?? role['startTime'] ?? role['check_in_time'];
final dynamic checkOutVal = role['checkOutTime'] ?? role['endTime'] ?? role['check_out_time'];
List<InvoiceWorker> workers = <InvoiceWorker>[];
return InvoiceWorker(
name: name,
role: roleTitle,
amount: amount,
hours: hours,
rate: rate,
checkIn: _service.toDateTime(checkInVal),
checkOut: _service.toDateTime(checkOutVal),
breakMinutes: role['breakMinutes'] ?? role['break_minutes'] ?? 0,
avatarUrl: role['avatarUrl'] ?? role['photoUrl'] ?? role['staffPhoto'],
);
}).toList();
// Try to get workers from denormalized 'roles' field first
final List<dynamic> rolesData = invoice.roles is List
? invoice.roles
: <dynamic>[];
if (rolesData.isNotEmpty) {
workers = rolesData.map((dynamic r) {
final Map<String, dynamic> role = r as Map<String, dynamic>;
// Handle various possible key naming conventions in the JSON data
final String name =
role['name'] ?? role['staffName'] ?? role['fullName'] ?? 'Unknown';
final String roleTitle =
role['role'] ?? role['roleName'] ?? role['title'] ?? 'Staff';
final double amount =
(role['amount'] as num?)?.toDouble() ??
(role['totalValue'] as num?)?.toDouble() ??
0.0;
final double hours =
(role['hours'] as num?)?.toDouble() ??
(role['workHours'] as num?)?.toDouble() ??
(role['totalHours'] as num?)?.toDouble() ??
0.0;
final double rate =
(role['rate'] as num?)?.toDouble() ??
(role['hourlyRate'] as num?)?.toDouble() ??
0.0;
final dynamic checkInVal =
role['checkInTime'] ?? role['startTime'] ?? role['check_in_time'];
final dynamic checkOutVal =
role['checkOutTime'] ?? role['endTime'] ?? role['check_out_time'];
return InvoiceWorker(
name: name,
role: roleTitle,
amount: amount,
hours: hours,
rate: rate,
checkIn: _service.toDateTime(checkInVal),
checkOut: _service.toDateTime(checkOutVal),
breakMinutes: role['breakMinutes'] ?? role['break_minutes'] ?? 0,
avatarUrl:
role['avatarUrl'] ?? role['photoUrl'] ?? role['staffPhoto'],
);
}).toList();
}
// Fallback: If roles is empty, try to get workers from shift applications
else if (invoice.shift != null &&
invoice.shift.applications_on_shift != null) {
final List<dynamic> apps = invoice.shift.applications_on_shift;
workers = apps.map((dynamic app) {
final String name = app.staff?.fullName ?? 'Unknown';
final String roleTitle = app.shiftRole?.role?.name ?? 'Staff';
final double amount =
(app.shiftRole?.totalValue as num?)?.toDouble() ?? 0.0;
final double hours = (app.shiftRole?.hours as num?)?.toDouble() ?? 0.0;
// Calculate rate if not explicitly provided
double rate = 0.0;
if (hours > 0) {
rate = amount / hours;
}
// Map break type to minutes
int breakMin = 0;
final String? breakType = app.shiftRole?.breakType?.toString();
if (breakType != null) {
if (breakType.contains('10'))
breakMin = 10;
else if (breakType.contains('15'))
breakMin = 15;
else if (breakType.contains('30'))
breakMin = 30;
else if (breakType.contains('45'))
breakMin = 45;
else if (breakType.contains('60'))
breakMin = 60;
}
return InvoiceWorker(
name: name,
role: roleTitle,
amount: amount,
hours: hours,
rate: rate,
checkIn: _service.toDateTime(app.checkInTime),
checkOut: _service.toDateTime(app.checkOutTime),
breakMinutes: breakMin,
avatarUrl: app.staff?.photoUrl,
);
}).toList();
}
return Invoice(
id: invoice.id,
@@ -202,8 +306,10 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
issueDate: _service.toDateTime(invoice.issueDate)!,
title: invoice.order?.eventName,
clientName: invoice.business?.businessName,
locationAddress: invoice.order?.teamHub?.hubName ?? invoice.order?.teamHub?.address,
staffCount: invoice.staffCount ?? (workers.isNotEmpty ? workers.length : 0),
locationAddress:
invoice.order?.teamHub?.hubName ?? invoice.order?.teamHub?.address,
staffCount:
invoice.staffCount ?? (workers.isNotEmpty ? workers.length : 0),
totalHours: _calculateTotalHours(rolesData),
workers: workers,
);
@@ -256,10 +362,7 @@ class _RoleSummary {
final double totalHours;
final double totalValue;
_RoleSummary copyWith({
double? totalHours,
double? totalValue,
}) {
_RoleSummary copyWith({double? totalHours, double? totalValue}) {
return _RoleSummary(
roleId: roleId,
roleName: roleName,
@@ -268,4 +371,3 @@ class _RoleSummary {
);
}
}