Refactor billing data parsing and filtering, update invoice queries, and remove the dedicated timesheets page.
This commit is contained in:
@@ -62,13 +62,13 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
action: () async {
|
||||
final List<dynamic> results =
|
||||
await Future.wait<dynamic>(<Future<dynamic>>[
|
||||
_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<Invoice> pendingInvoices = results[2] as List<Invoice>;
|
||||
@@ -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,
|
||||
);
|
||||
@@ -196,33 +193,35 @@ 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',
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user