From 8c0708d2d306667b64d8ed1f0041cc080b3da4da Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 28 Feb 2026 15:26:05 -0500 Subject: [PATCH] Refactor billing data parsing and filtering, update invoice queries, and remove the dedicated timesheets page. --- .../billing_connector_repository_impl.dart | 230 +++++++++++++----- .../src/presentation/blocs/billing_bloc.dart | 93 ++++--- .../src/presentation/pages/billing_page.dart | 77 +----- .../presentation/pages/timesheets_page.dart | 85 ------- .../completion_review_actions.dart | 4 +- .../widgets/invoice_history_section.dart | 9 +- .../widgets/pending_invoices_section.dart | 18 +- .../dataconnect/connector/invoice/queries.gql | 184 ++++++++++++++ 8 files changed, 414 insertions(+), 286 deletions(-) delete mode 100644 apps/mobile/packages/features/client/billing/lib/src/presentation/pages/timesheets_page.dart diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/billing/data/repositories/billing_connector_repository_impl.dart b/apps/mobile/packages/data_connect/lib/src/connectors/billing/data/repositories/billing_connector_repository_impl.dart index 7c955b71..63cda77d 100644 --- a/apps/mobile/packages/data_connect/lib/src/connectors/billing/data/repositories/billing_connector_repository_impl.dart +++ b/apps/mobile/packages/data_connect/lib/src/connectors/billing/data/repositories/billing_connector_repository_impl.dart @@ -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> getBankAccounts({required String businessId}) async { + Future> getBankAccounts({ + required String businessId, + }) async { return _service.run(() async { - final QueryResult 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 getCurrentBillAmount({required String businessId}) async { return _service.run(() async { - final QueryResult 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(0.0, (double sum, Invoice item) => sum + item.totalAmount); + .fold( + 0.0, + (double sum, Invoice item) => sum + item.totalAmount, + ); }); } @override Future> getInvoiceHistory({required String businessId}) async { return _service.run(() async { - final QueryResult 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> getPendingInvoices({required String businessId}) async { return _service.run(() async { - final QueryResult 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 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 shiftRoles = result.data.shiftRoles; + final List + shiftRoles = result.data.shiftRoles; if (shiftRoles.isEmpty) return []; final Map summary = {}; - 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 disputeInvoice({required String id, required String reason}) async { + Future 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 rolesData = invoice.roles is List ? invoice.roles : []; - final List workers = rolesData.map((dynamic r) { - final Map role = r as Map; - - // 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 workers = []; - 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 rolesData = invoice.roles is List + ? invoice.roles + : []; + if (rolesData.isNotEmpty) { + workers = rolesData.map((dynamic r) { + final Map role = r as Map; + + // 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 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 { ); } } - diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart index 0206a3b9..07c1b98d 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart @@ -62,13 +62,13 @@ class BillingBloc extends Bloc action: () async { final List results = await Future.wait(>[ - _getCurrentBillAmount.call(), - _getSavingsAmount.call(), - _getPendingInvoices.call(), - _getInvoiceHistory.call(), - _getSpendingBreakdown.call(state.period), - _getBankAccounts.call(), - ]); + _getCurrentBillAmount.call(), + _getSavingsAmount.call(), + _getPendingInvoices.call(), + _getInvoiceHistory.call(), + _getSpendingBreakdown.call(state.period), + _getBankAccounts.call(), + ]); final double savings = results[1] as double; final List pendingInvoices = results[2] as List; @@ -78,10 +78,12 @@ class BillingBloc extends Bloc results[5] as List; // Map Domain Entities to Presentation Models - final List uiPendingInvoices = - pendingInvoices.map(_mapInvoiceToUiModel).toList(); - final List uiInvoiceHistory = - invoiceHistory.map(_mapInvoiceToUiModel).toList(); + final List uiPendingInvoices = pendingInvoices + .map(_mapInvoiceToUiModel) + .toList(); + final List uiInvoiceHistory = invoiceHistory + .map(_mapInvoiceToUiModel) + .toList(); final List uiSpendingBreakdown = _mapSpendingItemsToUiModel(spendingItems); final double periodTotal = uiSpendingBreakdown.fold( @@ -101,10 +103,8 @@ class BillingBloc extends Bloc ), ); }, - 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 await handleError( emit: emit.call, action: () async { - final List spendingItems = - await _getSpendingBreakdown.call(event.period); + final List spendingItems = await _getSpendingBreakdown + .call(event.period); final List uiSpendingBreakdown = _mapSpendingItemsToUiModel(spendingItems); final double periodTotal = uiSpendingBreakdown.fold( @@ -131,10 +131,8 @@ class BillingBloc extends Bloc ), ); }, - 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 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 ); 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 ? 'N/A' : formatter.format(invoice.issueDate!); - final List workers = invoice.workers.map((InvoiceWorker w) { + final List 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, ); @@ -196,33 +193,35 @@ class BillingBloc extends Bloc 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 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 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', diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart index 20b2f0ef..f7a80aab 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart @@ -96,7 +96,7 @@ class _BillingViewState extends State { 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 { 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 { ), ); } - - Widget _buildEmptyState(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - 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), - ], - ), - ), - ); - } } diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/timesheets_page.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/timesheets_page.dart deleted file mode 100644 index 9a14faa2..00000000 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/timesheets_page.dart +++ /dev/null @@ -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, - ); - }, - ), - ), - ], - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_actions.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_actions.dart index c04ce60e..c8047b53 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_actions.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_actions.dart @@ -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().add( BillingInvoiceDisputed(invoiceId, controller.text), ); - Navigator.pop(dialogContext); - Modular.to.pop(); + Modular.to.toClientBilling(); UiSnackbar.show( context, message: t.client_billing.flagged_success, diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/invoice_history_section.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/invoice_history_section.dart index 55096618..fdbb5aa9 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/invoice_history_section.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/invoice_history_section.dart @@ -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( 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: [ - Text( - invoice.id, - style: UiTypography.body1b.textPrimary.copyWith(fontSize: 15), - ), + Text(invoice.title, style: UiTypography.body1r.textPrimary), Text( invoice.date, style: UiTypography.footnote2r.textSecondary, diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/pending_invoices_section.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/pending_invoices_section.dart index 9a36922f..8c0387d1 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/pending_invoices_section.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/pending_invoices_section.dart @@ -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: [ @@ -41,9 +41,9 @@ class PendingInvoicesSection extends StatelessWidget { Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Row( - children: [ + children: [ 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: [ - 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, diff --git a/backend/dataconnect/connector/invoice/queries.gql b/backend/dataconnect/connector/invoice/queries.gql index 9ee1df24..b2f39e22 100644 --- a/backend/dataconnect/connector/invoice/queries.gql +++ b/backend/dataconnect/connector/invoice/queries.gql @@ -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 + } + } + } } }