refactor: update billing module routes to use ClientPaths.childRoute and refactor PendingInvoicesPage to use UiAppBar and ListView.builder.
This commit is contained in:
@@ -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(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user