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]. /// Implementation of [BillingConnectorRepository].
class BillingConnectorRepositoryImpl implements BillingConnectorRepository { class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
BillingConnectorRepositoryImpl({ BillingConnectorRepositoryImpl({dc.DataConnectService? service})
dc.DataConnectService? service, : _service = service ?? dc.DataConnectService.instance;
}) : _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service; final dc.DataConnectService _service;
@override @override
Future<List<BusinessBankAccount>> getBankAccounts({required String businessId}) async { Future<List<BusinessBankAccount>> getBankAccounts({
required String businessId,
}) async {
return _service.run(() 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) .getAccountsByOwnerId(ownerId: businessId)
.execute(); .execute();
@@ -26,21 +31,32 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
@override @override
Future<double> getCurrentBillAmount({required String businessId}) async { Future<double> getCurrentBillAmount({required String businessId}) async {
return _service.run(() 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) .listInvoicesByBusinessId(businessId: businessId)
.execute(); .execute();
return result.data.invoices return result.data.invoices
.map(_mapInvoice) .map(_mapInvoice)
.where((Invoice i) => i.status == InvoiceStatus.open) .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 @override
Future<List<Invoice>> getInvoiceHistory({required String businessId}) async { Future<List<Invoice>> getInvoiceHistory({required String businessId}) async {
return _service.run(() 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) .listInvoicesByBusinessId(businessId: businessId)
.limit(20) .limit(20)
.execute(); .execute();
@@ -55,14 +71,22 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
@override @override
Future<List<Invoice>> getPendingInvoices({required String businessId}) async { Future<List<Invoice>> getPendingInvoices({required String businessId}) async {
return _service.run(() 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) .listInvoicesByBusinessId(businessId: businessId)
.execute(); .execute();
return result.data.invoices return result.data.invoices
.map(_mapInvoice) .map(_mapInvoice)
.where((Invoice i) => .where(
i.status != InvoiceStatus.paid) (Invoice i) =>
i.status != InvoiceStatus.paid &&
i.status != InvoiceStatus.disputed &&
i.status != InvoiceStatus.open,
)
.toList(); .toList();
}); });
} }
@@ -76,19 +100,28 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
final DateTime start; final DateTime start;
final DateTime end; final DateTime end;
if (period == BillingPeriod.week) { if (period == BillingPeriod.week) {
final int daysFromMonday = now.weekday - DateTime.monday; final int daysFromMonday = now.weekday - DateTime.monday;
final DateTime monday = DateTime(now.year, now.month, now.day) final DateTime monday = DateTime(
.subtract(Duration(days: daysFromMonday)); now.year,
now.month,
now.day,
).subtract(Duration(days: daysFromMonday));
start = monday; 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 { } else {
start = DateTime(now.year, now.month, 1); start = DateTime(now.year, now.month, 1);
end = DateTime(now.year, now.month + 1, 0, 23, 59, 59); 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( .listShiftRolesByBusinessAndDatesSummary(
businessId: businessId, businessId: businessId,
start: _service.toTimestamp(start), start: _service.toTimestamp(start),
@@ -96,16 +129,18 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
) )
.execute(); .execute();
final List<dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles> shiftRoles = result.data.shiftRoles; final List<dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
shiftRoles = result.data.shiftRoles;
if (shiftRoles.isEmpty) return <InvoiceItem>[]; if (shiftRoles.isEmpty) return <InvoiceItem>[];
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{}; 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 roleId = role.roleId;
final String roleName = role.role.name; final String roleName = role.role.name;
final double hours = role.hours ?? 0.0; final double hours = role.hours ?? 0.0;
final double totalValue = role.totalValue ?? 0.0; final double totalValue = role.totalValue ?? 0.0;
final _RoleSummary? existing = summary[roleId]; final _RoleSummary? existing = summary[roleId];
if (existing == null) { if (existing == null) {
summary[roleId] = _RoleSummary( summary[roleId] = _RoleSummary(
@@ -123,14 +158,16 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
} }
return summary.values return summary.values
.map((_RoleSummary item) => InvoiceItem( .map(
id: item.roleId, (_RoleSummary item) => InvoiceItem(
invoiceId: item.roleId, id: item.roleId,
staffId: item.roleName, invoiceId: item.roleId,
workHours: item.totalHours, staffId: item.roleName,
rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0, workHours: item.totalHours,
amount: item.totalValue, rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0,
)) amount: item.totalValue,
),
)
.toList(); .toList();
}); });
} }
@@ -146,7 +183,10 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
} }
@override @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 { return _service.run(() async {
await _service.connector await _service.connector
.updateInvoice(id: id) .updateInvoice(id: id)
@@ -159,36 +199,100 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
// --- MAPPERS --- // --- MAPPERS ---
Invoice _mapInvoice(dynamic invoice) { Invoice _mapInvoice(dynamic invoice) {
final List<dynamic> rolesData = invoice.roles is List ? invoice.roles : []; List<InvoiceWorker> workers = <InvoiceWorker>[];
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'];
return InvoiceWorker( // Try to get workers from denormalized 'roles' field first
name: name, final List<dynamic> rolesData = invoice.roles is List
role: roleTitle, ? invoice.roles
amount: amount, : <dynamic>[];
hours: hours, if (rolesData.isNotEmpty) {
rate: rate, workers = rolesData.map((dynamic r) {
checkIn: _service.toDateTime(checkInVal), final Map<String, dynamic> role = r as Map<String, dynamic>;
checkOut: _service.toDateTime(checkOutVal),
breakMinutes: role['breakMinutes'] ?? role['break_minutes'] ?? 0, // Handle various possible key naming conventions in the JSON data
avatarUrl: role['avatarUrl'] ?? role['photoUrl'] ?? role['staffPhoto'], final String name =
); role['name'] ?? role['staffName'] ?? role['fullName'] ?? 'Unknown';
}).toList(); 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( return Invoice(
id: invoice.id, id: invoice.id,
@@ -202,8 +306,10 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
issueDate: _service.toDateTime(invoice.issueDate)!, issueDate: _service.toDateTime(invoice.issueDate)!,
title: invoice.order?.eventName, title: invoice.order?.eventName,
clientName: invoice.business?.businessName, clientName: invoice.business?.businessName,
locationAddress: invoice.order?.teamHub?.hubName ?? invoice.order?.teamHub?.address, locationAddress:
staffCount: invoice.staffCount ?? (workers.isNotEmpty ? workers.length : 0), invoice.order?.teamHub?.hubName ?? invoice.order?.teamHub?.address,
staffCount:
invoice.staffCount ?? (workers.isNotEmpty ? workers.length : 0),
totalHours: _calculateTotalHours(rolesData), totalHours: _calculateTotalHours(rolesData),
workers: workers, workers: workers,
); );
@@ -256,10 +362,7 @@ class _RoleSummary {
final double totalHours; final double totalHours;
final double totalValue; final double totalValue;
_RoleSummary copyWith({ _RoleSummary copyWith({double? totalHours, double? totalValue}) {
double? totalHours,
double? totalValue,
}) {
return _RoleSummary( return _RoleSummary(
roleId: roleId, roleId: roleId,
roleName: roleName, roleName: roleName,
@@ -268,4 +371,3 @@ class _RoleSummary {
); );
} }
} }

View File

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

View File

@@ -96,7 +96,7 @@ class _BillingViewState extends State<BillingView> {
leading: Center( leading: Center(
child: UiIconButton( child: UiIconButton(
icon: UiIcons.arrowLeft, icon: UiIcons.arrowLeft,
backgroundColor: UiColors.white.withOpacity(0.15), backgroundColor: UiColors.white.withValues(alpha: 0.15),
iconColor: UiColors.white, iconColor: UiColors.white,
useBlur: true, useBlur: true,
size: 40, size: 40,
@@ -119,7 +119,7 @@ class _BillingViewState extends State<BillingView> {
Text( Text(
t.client_billing.current_period, t.client_billing.current_period,
style: UiTypography.footnote2r.copyWith( style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withOpacity(0.7), color: UiColors.white.withValues(alpha: 0.7),
), ),
), ),
const SizedBox(height: UiConstants.space1), 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:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import '../../blocs/billing_bloc.dart'; import '../../blocs/billing_bloc.dart';
import '../../blocs/billing_event.dart'; import '../../blocs/billing_event.dart';
@@ -72,8 +73,7 @@ class CompletionReviewActions extends StatelessWidget {
Modular.get<BillingBloc>().add( Modular.get<BillingBloc>().add(
BillingInvoiceDisputed(invoiceId, controller.text), BillingInvoiceDisputed(invoiceId, controller.text),
); );
Navigator.pop(dialogContext); Modular.to.toClientBilling();
Modular.to.pop();
UiSnackbar.show( UiSnackbar.show(
context, context,
message: t.client_billing.flagged_success, message: t.client_billing.flagged_success,

View File

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

View File

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