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();
});
}
@@ -79,16 +103,25 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
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,11 +129,13 @@ 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;
@@ -123,14 +158,16 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
}
return summary.values
.map((_RoleSummary item) => InvoiceItem(
.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,23 +199,39 @@ 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) {
List<InvoiceWorker> workers = <InvoiceWorker>[];
// 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() ??
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;
(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'];
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,
@@ -186,9 +242,57 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
checkIn: _service.toDateTime(checkInVal),
checkOut: _service.toDateTime(checkOutVal),
breakMinutes: role['breakMinutes'] ?? role['break_minutes'] ?? 0,
avatarUrl: role['avatarUrl'] ?? role['photoUrl'] ?? role['staffPhoto'],
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 {
);
}
}

View File

@@ -78,10 +78,12 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
results[5] as List<BusinessBankAccount>;
// Map Domain Entities to Presentation Models
final List<BillingInvoice> uiPendingInvoices =
pendingInvoices.map(_mapInvoiceToUiModel).toList();
final List<BillingInvoice> uiInvoiceHistory =
invoiceHistory.map(_mapInvoiceToUiModel).toList();
final List<BillingInvoice> uiPendingInvoices = pendingInvoices
.map(_mapInvoiceToUiModel)
.toList();
final List<BillingInvoice> uiInvoiceHistory = invoiceHistory
.map(_mapInvoiceToUiModel)
.toList();
final List<SpendingBreakdownItem> uiSpendingBreakdown =
_mapSpendingItemsToUiModel(spendingItems);
final double periodTotal = uiSpendingBreakdown.fold(
@@ -101,10 +103,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
),
);
},
onError: (String errorKey) => state.copyWith(
status: BillingStatus.failure,
errorMessage: errorKey,
),
onError: (String errorKey) =>
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
);
}
@@ -115,8 +115,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
await handleError(
emit: emit.call,
action: () async {
final List<InvoiceItem> spendingItems =
await _getSpendingBreakdown.call(event.period);
final List<InvoiceItem> spendingItems = await _getSpendingBreakdown
.call(event.period);
final List<SpendingBreakdownItem> uiSpendingBreakdown =
_mapSpendingItemsToUiModel(spendingItems);
final double periodTotal = uiSpendingBreakdown.fold(
@@ -131,10 +131,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
),
);
},
onError: (String errorKey) => state.copyWith(
status: BillingStatus.failure,
errorMessage: errorKey,
),
onError: (String errorKey) =>
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
);
}
@@ -148,10 +146,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
await _approveInvoice.call(event.invoiceId);
add(const BillingLoadStarted());
},
onError: (String errorKey) => state.copyWith(
status: BillingStatus.failure,
errorMessage: errorKey,
),
onError: (String errorKey) =>
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
);
}
@@ -167,10 +163,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
);
add(const BillingLoadStarted());
},
onError: (String errorKey) => state.copyWith(
status: BillingStatus.failure,
errorMessage: errorKey,
),
onError: (String errorKey) =>
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
);
}
@@ -180,15 +174,18 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
? 'N/A'
: formatter.format(invoice.issueDate!);
final List<BillingWorkerRecord> workers = invoice.workers.map((InvoiceWorker w) {
final List<BillingWorkerRecord> workers = invoice.workers.map((
InvoiceWorker w,
) {
final DateFormat timeFormat = DateFormat('h:mm a');
return BillingWorkerRecord(
workerName: w.name,
roleName: w.role,
totalAmount: w.amount,
hours: w.hours,
rate: w.rate,
startTime: w.checkIn != null ? '${w.checkIn!.hour.toString().padLeft(2, '0')}:${w.checkIn!.minute.toString().padLeft(2, '0')}' : '--:--',
endTime: w.checkOut != null ? '${w.checkOut!.hour.toString().padLeft(2, '0')}:${w.checkOut!.minute.toString().padLeft(2, '0')}' : '--:--',
startTime: w.checkIn != null ? timeFormat.format(w.checkIn!) : '--:--',
endTime: w.checkOut != null ? timeFormat.format(w.checkOut!) : '--:--',
breakMinutes: w.breakMinutes,
workerAvatarUrl: w.avatarUrl,
);
@@ -197,32 +194,34 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
String? overallStart;
String? overallEnd;
// Find valid times from workers instead of just taking the first one
final validStartTimes = workers
.where((w) => w.startTime != '--:--')
.map((w) => w.startTime)
// Find valid times from actual DateTime checks to ensure chronological sorting
final List<DateTime> validCheckIns = invoice.workers
.where((InvoiceWorker w) => w.checkIn != null)
.map((InvoiceWorker w) => w.checkIn!)
.toList();
final validEndTimes = workers
.where((w) => w.endTime != '--:--')
.map((w) => w.endTime)
final List<DateTime> validCheckOuts = invoice.workers
.where((InvoiceWorker w) => w.checkOut != null)
.map((InvoiceWorker w) => w.checkOut!)
.toList();
if (validStartTimes.isNotEmpty) {
validStartTimes.sort();
overallStart = validStartTimes.first;
final DateFormat timeFormat = DateFormat('h:mm a');
if (validCheckIns.isNotEmpty) {
validCheckIns.sort();
overallStart = timeFormat.format(validCheckIns.first);
} else if (workers.isNotEmpty) {
overallStart = workers.first.startTime;
}
if (validEndTimes.isNotEmpty) {
validEndTimes.sort();
overallEnd = validEndTimes.last;
if (validCheckOuts.isNotEmpty) {
validCheckOuts.sort();
overallEnd = timeFormat.format(validCheckOuts.last);
} else if (workers.isNotEmpty) {
overallEnd = workers.first.endTime;
}
return BillingInvoice(
id: invoice.invoiceNumber ?? invoice.id,
id: invoice.id,
title: invoice.title ?? 'N/A',
locationAddress: invoice.locationAddress ?? 'Remote',
clientName: invoice.clientName ?? 'N/A',

View File

@@ -96,7 +96,7 @@ class _BillingViewState extends State<BillingView> {
leading: Center(
child: UiIconButton(
icon: UiIcons.arrowLeft,
backgroundColor: UiColors.white.withOpacity(0.15),
backgroundColor: UiColors.white.withValues(alpha: 0.15),
iconColor: UiColors.white,
useBlur: true,
size: 40,
@@ -119,7 +119,7 @@ class _BillingViewState extends State<BillingView> {
Text(
t.client_billing.current_period,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withOpacity(0.7),
color: UiColors.white.withValues(alpha: 0.7),
),
),
const SizedBox(height: UiConstants.space1),
@@ -232,77 +232,4 @@ class _BillingViewState extends State<BillingView> {
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(height: UiConstants.space12),
Container(
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: UiColors.bgPopup,
shape: BoxShape.circle,
border: Border.all(color: UiColors.border),
),
child: const Icon(
UiIcons.file,
size: 48,
color: UiColors.textSecondary,
),
),
const SizedBox(height: UiConstants.space4),
Text(
t.client_billing.no_invoices_period,
style: UiTypography.body1m.textSecondary,
textAlign: TextAlign.center,
),
],
),
);
}
}
class _InvoicesReadyBanner extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => Modular.to.toInvoiceReady(),
child: Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.success.withValues(alpha: 0.1),
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.success.withValues(alpha: 0.3)),
),
child: Row(
children: [
const Icon(UiIcons.file, color: UiColors.success),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.client_billing.invoices_ready_title,
style: UiTypography.body1b.copyWith(
color: UiColors.success,
),
),
Text(
t.client_billing.invoices_ready_subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.success,
),
),
],
),
),
const Icon(UiIcons.chevronRight, color: UiColors.success),
],
),
),
);
}
}

View File

@@ -1,85 +0,0 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:core_localization/core_localization.dart';
class ClientTimesheetsPage extends StatelessWidget {
const ClientTimesheetsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.t.client_billing.timesheets.title),
elevation: 0,
backgroundColor: UiColors.white,
foregroundColor: UiColors.primary,
),
body: ListView.separated(
padding: const EdgeInsets.all(UiConstants.space5),
itemCount: 3,
separatorBuilder: (context, index) => const SizedBox(height: 16),
itemBuilder: (context, index) {
final workers = ['Sarah Miller', 'David Chen', 'Mike Ross'];
final roles = ['Cashier', 'Stocker', 'Event Support'];
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.separatorPrimary),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(workers[index], style: UiTypography.body2b.textPrimary),
Text('\$84.00', style: UiTypography.body2b.primary),
],
),
Text(roles[index], style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: 12),
Row(
children: [
const Icon(UiIcons.clock, size: 14, color: UiColors.iconSecondary),
const SizedBox(width: 6),
Text('09:00 AM - 05:00 PM (8h)', style: UiTypography.footnote2r.textSecondary),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: UiButton.secondary(
text: context.t.client_billing.timesheets.decline_button,
style: OutlinedButton.styleFrom(
foregroundColor: UiColors.destructive,
side: const BorderSide(color: UiColors.destructive),
),
onPressed: () {},
),
),
const SizedBox(width: 12),
Expanded(
child: UiButton.primary(
text: context.t.client_billing.timesheets.approve_button,
onPressed: () {
UiSnackbar.show(
context,
message: context.t.client_billing.timesheets.approved_message,
type: UiSnackbarType.success,
);
},
),
),
],
),
],
),
);
},
),
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import '../../blocs/billing_bloc.dart';
import '../../blocs/billing_event.dart';
@@ -72,8 +73,7 @@ class CompletionReviewActions extends StatelessWidget {
Modular.get<BillingBloc>().add(
BillingInvoiceDisputed(invoiceId, controller.text),
);
Navigator.pop(dialogContext);
Modular.to.pop();
Modular.to.toClientBilling();
UiSnackbar.show(
context,
message: t.client_billing.flagged_success,

View File

@@ -25,7 +25,7 @@ class InvoiceHistorySection extends StatelessWidget {
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border.withOpacity(0.5)),
border: Border.all(color: UiColors.border.withValues(alpha: 0.5)),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.04),
@@ -77,7 +77,7 @@ class _InvoiceItem extends StatelessWidget {
),
child: Icon(
UiIcons.file,
color: UiColors.iconSecondary.withOpacity(0.6),
color: UiColors.iconSecondary.withValues(alpha: 0.6),
size: 20,
),
),
@@ -86,10 +86,7 @@ class _InvoiceItem extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
invoice.id,
style: UiTypography.body1b.textPrimary.copyWith(fontSize: 15),
),
Text(invoice.title, style: UiTypography.body1r.textPrimary),
Text(
invoice.date,
style: UiTypography.footnote2r.textSecondary,

View File

@@ -25,7 +25,7 @@ class PendingInvoicesSection extends StatelessWidget {
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border.withOpacity(0.5)),
border: Border.all(color: UiColors.border.withValues(alpha: 0.5)),
),
child: Row(
children: <Widget>[
@@ -41,9 +41,9 @@ class PendingInvoicesSection extends StatelessWidget {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Row(
children: [
children: <Widget>[
Text(
t.client_billing.awaiting_approval,
style: UiTypography.body1b.textPrimary,
@@ -79,7 +79,7 @@ class PendingInvoicesSection extends StatelessWidget {
Icon(
UiIcons.chevronRight,
size: 20,
color: UiColors.iconSecondary.withOpacity(0.5),
color: UiColors.iconSecondary.withValues(alpha: 0.5),
),
],
),
@@ -180,7 +180,7 @@ class PendingInvoiceCard extends StatelessWidget {
Container(
width: 1,
height: 32,
color: UiColors.border.withOpacity(0.3),
color: UiColors.border.withValues(alpha: 0.3),
),
Expanded(
child: _buildStatItem(
@@ -192,7 +192,7 @@ class PendingInvoiceCard extends StatelessWidget {
Container(
width: 1,
height: 32,
color: UiColors.border.withOpacity(0.3),
color: UiColors.border.withValues(alpha: 0.3),
),
Expanded(
child: _buildStatItem(
@@ -225,7 +225,11 @@ class PendingInvoiceCard extends StatelessWidget {
Widget _buildStatItem(IconData icon, String value, String label) {
return Column(
children: <Widget>[
Icon(icon, size: 20, color: UiColors.iconSecondary.withOpacity(0.8)),
Icon(
icon,
size: 20,
color: UiColors.iconSecondary.withValues(alpha: 0.8),
),
const SizedBox(height: 6),
Text(
value,

View File

@@ -61,6 +61,29 @@ query listInvoices(
}
}
shift {
id
title
applications_on_shift {
id
status
checkInTime
checkOutTime
staff {
fullName
photoUrl
}
shiftRole {
role {
name
}
totalValue
hours
breakType
}
}
}
}
}
@@ -123,6 +146,29 @@ query getInvoiceById($id: UUID!) @auth(level: USER) {
}
}
shift {
id
title
applications_on_shift {
id
status
checkInTime
checkOutTime
staff {
fullName
photoUrl
}
shiftRole {
role {
name
}
totalValue
hours
breakType
}
}
}
}
}
@@ -194,6 +240,29 @@ query listInvoicesByVendorId(
}
}
shift {
id
title
applications_on_shift {
id
status
checkInTime
checkOutTime
staff {
fullName
photoUrl
}
shiftRole {
role {
name
}
totalValue
hours
breakType
}
}
}
}
}
@@ -265,6 +334,29 @@ query listInvoicesByBusinessId(
}
}
shift {
id
title
applications_on_shift {
id
status
checkInTime
checkOutTime
staff {
fullName
photoUrl
}
shiftRole {
role {
name
}
totalValue
hours
breakType
}
}
}
}
}
@@ -336,6 +428,29 @@ query listInvoicesByOrderId(
}
}
shift {
id
title
applications_on_shift {
id
status
checkInTime
checkOutTime
staff {
fullName
photoUrl
}
shiftRole {
role {
name
}
totalValue
hours
breakType
}
}
}
}
}
@@ -407,6 +522,29 @@ query listInvoicesByStatus(
}
}
shift {
id
title
applications_on_shift {
id
status
checkInTime
checkOutTime
staff {
fullName
photoUrl
}
shiftRole {
role {
name
}
totalValue
hours
breakType
}
}
}
}
}
@@ -501,6 +639,29 @@ query filterInvoices(
}
}
shift {
id
title
applications_on_shift {
id
status
checkInTime
checkOutTime
staff {
fullName
photoUrl
}
shiftRole {
role {
name
}
totalValue
hours
breakType
}
}
}
}
}
@@ -576,5 +737,28 @@ query listOverdueInvoices(
}
}
shift {
id
title
applications_on_shift {
id
status
checkInTime
checkOutTime
staff {
fullName
photoUrl
}
shiftRole {
role {
name
}
totalValue
hours
breakType
}
}
}
}
}