refactor: update billing module routes to use ClientPaths.childRoute and refactor PendingInvoicesPage to use UiAppBar and ListView.builder.

This commit is contained in:
Achintha Isuru
2026-02-26 14:37:20 -05:00
parent 94e15ae05d
commit f9c2d822e6
6 changed files with 110 additions and 128 deletions

View File

@@ -22,8 +22,6 @@ import 'presentation/pages/pending_invoices_page.dart';
class BillingModule extends Module { class BillingModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repositories // Repositories
i.addSingleton<BillingRepository>(BillingRepositoryImpl.new); i.addSingleton<BillingRepository>(BillingRepositoryImpl.new);
@@ -54,9 +52,22 @@ class BillingModule extends Module {
@override @override
void routes(RouteManager r) { void routes(RouteManager r) {
r.child(ClientPaths.childRoute(ClientPaths.billing, ClientPaths.billing), child: (_) => const BillingPage()); r.child(
r.child('/completion-review', child: (_) => ShiftCompletionReviewPage(invoice: r.args.data as BillingInvoice?)); ClientPaths.childRoute(ClientPaths.billing, ClientPaths.billing),
r.child('/invoice-ready', child: (_) => const InvoiceReadyPage()); child: (_) => const BillingPage(),
r.child('/awaiting-approval', child: (_) => const PendingInvoicesPage()); );
r.child(
ClientPaths.childRoute(ClientPaths.billing, ClientPaths.completionReview),
child: (_) =>
ShiftCompletionReviewPage(invoice: r.args.data as BillingInvoice?),
);
r.child(
ClientPaths.childRoute(ClientPaths.billing, ClientPaths.invoiceReady),
child: (_) => const InvoiceReadyPage(),
);
r.child(
ClientPaths.childRoute(ClientPaths.billing, ClientPaths.awaitingApproval),
child: (_) => const PendingInvoicesPage(),
);
} }
} }

View File

@@ -3,7 +3,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.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_state.dart'; import '../blocs/billing_state.dart';
import '../widgets/pending_invoices_section.dart'; import '../widgets/pending_invoices_section.dart';
@@ -13,112 +13,77 @@ class PendingInvoicesPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return BlocBuilder<BillingBloc, BillingState>(
backgroundColor: const Color(0xFFF8FAFC), bloc: Modular.get<BillingBloc>(),
body: BlocBuilder<BillingBloc, BillingState>( builder: (BuildContext context, BillingState state) {
bloc: Modular.get<BillingBloc>(), return Scaffold(
builder: (context, state) { appBar: UiAppBar(
return CustomScrollView( title: t.client_billing.awaiting_approval,
slivers: [ showBackButton: true,
_buildHeader(context, state.pendingInvoices.length), ),
if (state.status == BillingStatus.loading) body: _buildBody(context, state),
const SliverFillRemaining( );
child: Center(child: CircularProgressIndicator()), },
)
else if (state.pendingInvoices.isEmpty)
_buildEmptyState()
else
SliverPadding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space5,
UiConstants.space5,
100, // Bottom padding for scroll clearance
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: PendingInvoiceCard(invoice: state.pendingInvoices[index]),
);
},
childCount: state.pendingInvoices.length,
),
),
),
],
);
},
),
); );
} }
Widget _buildHeader(BuildContext context, int count) { Widget _buildBody(BuildContext context, BillingState state) {
return SliverAppBar( if (state.status == BillingStatus.loading) {
pinned: true, return const Center(child: CircularProgressIndicator());
expandedHeight: 140.0, }
backgroundColor: UiColors.primary,
elevation: 0, if (state.pendingInvoices.isEmpty) {
leadingWidth: 72, return _buildEmptyState();
leading: Center( }
child: UiIconButton(
icon: UiIcons.arrowLeft, return ListView.builder(
backgroundColor: UiColors.white.withOpacity(0.15), padding: const EdgeInsets.fromLTRB(
iconColor: UiColors.white, UiConstants.space5,
useBlur: true, UiConstants.space5,
size: 40, UiConstants.space5,
onTap: () => Navigator.of(context).pop(), 100, // Bottom padding for scroll clearance
),
),
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Text(
t.client_billing.awaiting_approval,
style: UiTypography.headline4b.copyWith(color: UiColors.white),
),
background: Center(
child: Padding(
padding: const EdgeInsets.only(top: 40),
child: Opacity(
opacity: 0.1,
child: Icon(UiIcons.clock, size: 100, color: UiColors.white),
),
),
),
), ),
itemCount: state.pendingInvoices.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: PendingInvoiceCard(invoice: state.pendingInvoices[index]),
);
},
); );
} }
Widget _buildEmptyState() { Widget _buildEmptyState() {
return SliverFillRemaining( return Center(
child: Center( child: Column(
child: Column( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
children: [ Container(
Container( padding: const EdgeInsets.all(UiConstants.space6),
padding: const EdgeInsets.all(UiConstants.space6), decoration: const BoxDecoration(
decoration: BoxDecoration( color: UiColors.bgPopup,
color: UiColors.bgPopup, shape: BoxShape.circle,
shape: BoxShape.circle,
),
child: const Icon(UiIcons.checkCircle, size: 48, color: UiColors.success),
), ),
const SizedBox(height: UiConstants.space4), child: const Icon(
Text( UiIcons.checkCircle,
t.client_billing.all_caught_up, size: 48,
style: UiTypography.body1m.textPrimary, color: UiColors.success,
), ),
Text( ),
t.client_billing.no_pending_invoices, const SizedBox(height: UiConstants.space4),
style: UiTypography.body2r.textSecondary, Text(
), t.client_billing.all_caught_up,
], style: UiTypography.body1m.textPrimary,
), ),
Text(
t.client_billing.no_pending_invoices,
style: UiTypography.body2r.textSecondary,
),
],
), ),
); );
} }
} }
// We need to export the card widget from the section file if we want to reuse it, // We need to export the card widget from the section file if we want to reuse it,
// or move it to its own file. I'll move it to a shared file or just make it public in the section file. // or move it to its own file. I'll move it to a shared file or just make it public in the section file.

View File

@@ -3,6 +3,7 @@ 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 'package:krow_core/core.dart';
import '../models/billing_invoice_model.dart'; import '../models/billing_invoice_model.dart';
/// Section showing a banner for invoices awaiting approval. /// Section showing a banner for invoices awaiting approval.
@@ -56,7 +57,10 @@ class PendingInvoicesSection extends StatelessWidget {
), ),
const SizedBox(width: UiConstants.space2), const SizedBox(width: UiConstants.space2),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: UiColors.accent, color: UiColors.accent,
shape: BoxShape.circle, shape: BoxShape.circle,
@@ -104,14 +108,7 @@ class PendingInvoiceCard 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, width: 0.5),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.04),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(UiConstants.space5), padding: const EdgeInsets.all(UiConstants.space5),
@@ -187,7 +184,11 @@ class PendingInvoiceCard extends StatelessWidget {
t.client_billing.stats.total, t.client_billing.stats.total,
), ),
), ),
Container(width: 1, height: 32, color: UiColors.border.withOpacity(0.3)), Container(
width: 1,
height: 32,
color: UiColors.border.withOpacity(0.3),
),
Expanded( Expanded(
child: _buildStatItem( child: _buildStatItem(
UiIcons.users, UiIcons.users,
@@ -195,11 +196,15 @@ class PendingInvoiceCard extends StatelessWidget {
t.client_billing.stats.workers, t.client_billing.stats.workers,
), ),
), ),
Container(width: 1, height: 32, color: UiColors.border.withOpacity(0.3)), Container(
width: 1,
height: 32,
color: UiColors.border.withOpacity(0.3),
),
Expanded( Expanded(
child: _buildStatItem( child: _buildStatItem(
UiIcons.clock, UiIcons.clock,
'${invoice.totalHours.toStringAsFixed(1)}', invoice.totalHours.toStringAsFixed(1),
t.client_billing.stats.hrs, t.client_billing.stats.hrs,
), ),
), ),
@@ -210,14 +215,12 @@ class PendingInvoiceCard extends StatelessWidget {
const SizedBox(height: UiConstants.space5), const SizedBox(height: UiConstants.space5),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: UiButton.primary( child: UiButton.secondary(
text: t.client_billing.review_and_approve, text: t.client_billing.review_and_approve,
leadingIcon: UiIcons.checkCircle, leadingIcon: UiIcons.checkCircle,
onPressed: () => Modular.to.toCompletionReview(arguments: invoice), onPressed: () =>
Modular.to.toCompletionReview(arguments: invoice),
size: UiButtonSize.large, size: UiButtonSize.large,
style: ElevatedButton.styleFrom(
textStyle: UiTypography.body1b.copyWith(fontSize: 16),
),
), ),
), ),
], ],

View File

@@ -86,7 +86,7 @@ class _ActionCard extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, color: color,
borderRadius: UiConstants.radiusLg, borderRadius: UiConstants.radiusLg,
border: Border.all(color: borderColor), border: Border.all(color: borderColor, width: 0.5),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@@ -3,13 +3,13 @@ import 'package:flutter/material.dart';
/// A dashboard widget that displays today's coverage status. /// A dashboard widget that displays today's coverage status.
class CoverageDashboard extends StatelessWidget { class CoverageDashboard extends StatelessWidget {
/// Creates a [CoverageDashboard]. /// Creates a [CoverageDashboard].
const CoverageDashboard({ const CoverageDashboard({
super.key, super.key,
required this.shifts, required this.shifts,
required this.applications, required this.applications,
}); });
/// The list of shifts for today. /// The list of shifts for today.
final List<dynamic> shifts; final List<dynamic> shifts;
@@ -23,7 +23,8 @@ class CoverageDashboard extends StatelessWidget {
double todayCost = 0; double todayCost = 0;
for (final dynamic s in shifts) { for (final dynamic s in shifts) {
final int needed = (s as Map<String, dynamic>)['workersNeeded'] as int? ?? 0; final int needed =
(s as Map<String, dynamic>)['workersNeeded'] as int? ?? 0;
final int confirmed = s['filled'] as int? ?? 0; final int confirmed = s['filled'] as int? ?? 0;
final double rate = s['hourlyRate'] as double? ?? 0.0; final double rate = s['hourlyRate'] as double? ?? 0.0;
final double hours = s['hours'] as double? ?? 0.0; final double hours = s['hours'] as double? ?? 0.0;
@@ -39,7 +40,9 @@ class CoverageDashboard extends StatelessWidget {
final int unfilledPositions = totalNeeded - totalConfirmed; final int unfilledPositions = totalNeeded - totalConfirmed;
final int checkedInCount = applications final int checkedInCount = applications
.where((dynamic a) => (a as Map<String, dynamic>)['checkInTime'] != null) .where(
(dynamic a) => (a as Map<String, dynamic>)['checkInTime'] != null,
)
.length; .length;
final int lateWorkersCount = applications final int lateWorkersCount = applications
.where((dynamic a) => (a as Map<String, dynamic>)['status'] == 'LATE') .where((dynamic a) => (a as Map<String, dynamic>)['status'] == 'LATE')
@@ -58,7 +61,7 @@ class CoverageDashboard extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.white, color: UiColors.white,
borderRadius: UiConstants.radiusLg, borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border), border: Border.all(color: UiColors.border, width: 0.5),
boxShadow: <BoxShadow>[ boxShadow: <BoxShadow>[
BoxShadow( BoxShadow(
color: UiColors.black.withValues(alpha: 0.02), color: UiColors.black.withValues(alpha: 0.02),
@@ -145,7 +148,6 @@ class CoverageDashboard extends StatelessWidget {
} }
class _StatusCard extends StatelessWidget { class _StatusCard extends StatelessWidget {
const _StatusCard({ const _StatusCard({
required this.label, required this.label,
required this.value, required this.value,

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
/// A widget that displays the daily coverage metrics. /// A widget that displays the daily coverage metrics.
class CoverageWidget extends StatelessWidget { class CoverageWidget extends StatelessWidget {
/// Creates a [CoverageWidget]. /// Creates a [CoverageWidget].
const CoverageWidget({ const CoverageWidget({
super.key, super.key,
@@ -13,6 +12,7 @@ class CoverageWidget extends StatelessWidget {
this.coveragePercent = 0, this.coveragePercent = 0,
this.subtitle, this.subtitle,
}); });
/// The total number of shifts needed. /// The total number of shifts needed.
final int totalNeeded; final int totalNeeded;
@@ -65,8 +65,10 @@ class CoverageWidget extends StatelessWidget {
color: backgroundColor, color: backgroundColor,
borderRadius: UiConstants.radiusLg, borderRadius: UiConstants.radiusLg,
), ),
child: Text( child: Text(
t.client_home.dashboard.percent_covered(percent: coveragePercent), t.client_home.dashboard.percent_covered(
percent: coveragePercent,
),
style: UiTypography.footnote2b.copyWith(color: textColor), style: UiTypography.footnote2b.copyWith(color: textColor),
), ),
), ),
@@ -115,7 +117,6 @@ class CoverageWidget extends StatelessWidget {
} }
class _MetricCard extends StatelessWidget { class _MetricCard extends StatelessWidget {
const _MetricCard({ const _MetricCard({
required this.icon, required this.icon,
required this.iconColor, required this.iconColor,
@@ -136,7 +137,7 @@ class _MetricCard extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.cardViewBackground, color: UiColors.cardViewBackground,
borderRadius: UiConstants.radiusLg, borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border), border: Border.all(color: UiColors.border, width: 0.5),
boxShadow: <BoxShadow>[ boxShadow: <BoxShadow>[
BoxShadow( BoxShadow(
color: UiColors.black.withValues(alpha: 0.02), color: UiColors.black.withValues(alpha: 0.02),