feat: Enable the rapid order type, refactor the invoice ready page to use UiAppBar, and adjust rapid action widget colors.

This commit is contained in:
Achintha Isuru
2026-02-28 12:11:46 -05:00
parent 1ed6d27ca7
commit 752f60405e
4 changed files with 161 additions and 138 deletions

View File

@@ -1,9 +1,8 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import '../blocs/billing_bloc.dart';
import '../blocs/billing_event.dart';
import '../models/billing_invoice_model.dart';
@@ -14,7 +13,8 @@ class ShiftCompletionReviewPage extends StatefulWidget {
final BillingInvoice? invoice;
@override
State<ShiftCompletionReviewPage> createState() => _ShiftCompletionReviewPageState();
State<ShiftCompletionReviewPage> createState() =>
_ShiftCompletionReviewPageState();
}
class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
@@ -31,99 +31,62 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
@override
Widget build(BuildContext context) {
final List<BillingWorkerRecord> filteredWorkers = invoice.workers.where((BillingWorkerRecord w) {
final List<BillingWorkerRecord> filteredWorkers = invoice.workers.where((
BillingWorkerRecord w,
) {
if (searchQuery.isEmpty) return true;
return w.workerName.toLowerCase().contains(searchQuery.toLowerCase()) ||
w.roleName.toLowerCase().contains(searchQuery.toLowerCase());
w.roleName.toLowerCase().contains(searchQuery.toLowerCase());
}).toList();
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: UiAppBar(
title: invoice.title,
subtitle: invoice.clientName,
showBackButton: true,
),
body: SafeArea(
child: Column(
children: <Widget>[
_buildHeader(context),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: UiConstants.space4),
_buildInvoiceInfoCard(),
const SizedBox(height: UiConstants.space4),
_buildAmountCard(),
const SizedBox(height: UiConstants.space6),
_buildWorkersHeader(),
const SizedBox(height: UiConstants.space4),
_buildSearchAndTabs(),
const SizedBox(height: UiConstants.space4),
...filteredWorkers.map((BillingWorkerRecord worker) => _buildWorkerCard(worker)),
const SizedBox(height: UiConstants.space6),
_buildActionButtons(context),
const SizedBox(height: UiConstants.space4),
_buildDownloadLink(),
const SizedBox(height: UiConstants.space8),
],
child: Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: UiConstants.space4),
_buildInvoiceInfoCard(),
const SizedBox(height: UiConstants.space4),
_buildAmountCard(),
const SizedBox(height: UiConstants.space6),
_buildWorkersHeader(),
const SizedBox(height: UiConstants.space4),
_buildSearchAndTabs(),
const SizedBox(height: UiConstants.space4),
...filteredWorkers.map(
(BillingWorkerRecord worker) => _buildWorkerCard(worker),
),
),
const SizedBox(height: UiConstants.space6),
_buildActionButtons(context),
const SizedBox(height: UiConstants.space4),
_buildDownloadLink(),
const SizedBox(height: UiConstants.space8),
],
),
],
),
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(UiConstants.space5, UiConstants.space4, UiConstants.space5, UiConstants.space4),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(bottom: BorderSide(color: UiColors.border)),
),
child: Column(
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: UiColors.border,
borderRadius: UiConstants.radiusFull,
),
),
const SizedBox(height: UiConstants.space4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(t.client_billing.invoice_ready, style: UiTypography.headline4b.textPrimary),
Text(t.client_billing.review_and_approve_subtitle, style: UiTypography.body2r.textSecondary),
],
),
UiIconButton.secondary(
icon: UiIcons.close,
onTap: () => Navigator.of(context).pop(),
),
],
),
],
),
);
}
Widget _buildInvoiceInfoCard() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space1,
children: <Widget>[
Text(invoice.title, style: UiTypography.headline4b.textPrimary),
Text(invoice.clientName, style: UiTypography.body2r.textSecondary),
const SizedBox(height: UiConstants.space4),
_buildInfoRow(UiIcons.calendar, invoice.date),
const SizedBox(height: UiConstants.space2),
_buildInfoRow(UiIcons.clock, '${invoice.startTime ?? "--"} - ${invoice.endTime ?? "--"}'),
const SizedBox(height: UiConstants.space2),
_buildInfoRow(
UiIcons.clock,
'${invoice.startTime ?? "--"} - ${invoice.endTime ?? "--"}',
),
_buildInfoRow(UiIcons.mapPin, invoice.locationAddress),
],
);
@@ -194,7 +157,11 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
child: TextField(
onChanged: (String val) => setState(() => searchQuery = val),
decoration: InputDecoration(
icon: const Icon(UiIcons.search, size: 18, color: UiColors.iconSecondary),
icon: const Icon(
UiIcons.search,
size: 18,
color: UiColors.iconSecondary,
),
hintText: t.client_billing.workers_tab.search_hint,
hintStyle: UiTypography.body2r.textSecondary,
border: InputBorder.none,
@@ -205,11 +172,17 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
Row(
children: <Widget>[
Expanded(
child: _buildTabButton(t.client_billing.workers_tab.needs_review(count: 0), 0),
child: _buildTabButton(
t.client_billing.workers_tab.needs_review(count: 0),
0,
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _buildTabButton(t.client_billing.workers_tab.all(count: invoice.workersCount), 1),
child: _buildTabButton(
t.client_billing.workers_tab.all(count: invoice.workersCount),
1,
),
),
],
),
@@ -226,7 +199,9 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF2563EB) : Colors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: isSelected ? const Color(0xFF2563EB) : UiColors.border),
border: Border.all(
color: isSelected ? const Color(0xFF2563EB) : UiColors.border,
),
),
child: Center(
child: Text(
@@ -257,24 +232,44 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
CircleAvatar(
radius: 20,
backgroundColor: UiColors.bgSecondary,
backgroundImage: worker.workerAvatarUrl != null ? NetworkImage(worker.workerAvatarUrl!) : null,
child: worker.workerAvatarUrl == null ? const Icon(UiIcons.user, size: 20, color: UiColors.iconSecondary) : null,
backgroundImage: worker.workerAvatarUrl != null
? NetworkImage(worker.workerAvatarUrl!)
: null,
child: worker.workerAvatarUrl == null
? const Icon(
UiIcons.user,
size: 20,
color: UiColors.iconSecondary,
)
: null,
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(worker.workerName, style: UiTypography.body1b.textPrimary),
Text(worker.roleName, style: UiTypography.footnote2r.textSecondary),
Text(
worker.workerName,
style: UiTypography.body1b.textPrimary,
),
Text(
worker.roleName,
style: UiTypography.footnote2r.textSecondary,
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text('\$${worker.totalAmount.toStringAsFixed(2)}', style: UiTypography.body1b.textPrimary),
Text('${worker.hours}h x \$${worker.rate.toStringAsFixed(2)}/hr', style: UiTypography.footnote2r.textSecondary),
Text(
'\$${worker.totalAmount.toStringAsFixed(2)}',
style: UiTypography.body1b.textPrimary,
),
Text(
'${worker.hours}h x \$${worker.rate.toStringAsFixed(2)}/hr',
style: UiTypography.footnote2r.textSecondary,
),
],
),
],
@@ -283,17 +278,26 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
Row(
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: Text('${worker.startTime} - ${worker.endTime}', style: UiTypography.footnote2b.textPrimary),
child: Text(
'${worker.startTime} - ${worker.endTime}',
style: UiTypography.footnote2b.textPrimary,
),
),
const SizedBox(width: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: UiConstants.radiusMd,
@@ -301,22 +305,23 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
),
child: Row(
children: [
const Icon(UiIcons.coffee, size: 12, color: UiColors.iconSecondary),
const Icon(
UiIcons.coffee,
size: 12,
color: UiColors.iconSecondary,
),
const SizedBox(width: 4),
Text('${worker.breakMinutes} ${t.client_billing.workers_tab.min_break}', style: UiTypography.footnote2r.textSecondary),
Text(
'${worker.breakMinutes} ${t.client_billing.workers_tab.min_break}',
style: UiTypography.footnote2r.textSecondary,
),
],
),
),
const Spacer(),
UiIconButton.secondary(
icon: UiIcons.edit,
onTap: () {},
),
UiIconButton.secondary(icon: UiIcons.edit, onTap: () {}),
const SizedBox(width: UiConstants.space2),
UiIconButton.secondary(
icon: UiIcons.warning,
onTap: () {},
),
UiIconButton.secondary(icon: UiIcons.warning, onTap: () {}),
],
),
],
@@ -333,9 +338,15 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
text: t.client_billing.actions.approve_pay,
leadingIcon: UiIcons.checkCircle,
onPressed: () {
Modular.get<BillingBloc>().add(BillingInvoiceApproved(invoice.id));
Modular.get<BillingBloc>().add(
BillingInvoiceApproved(invoice.id),
);
Modular.to.pop();
UiSnackbar.show(context, message: t.client_billing.approved_success, type: UiSnackbarType.success);
UiSnackbar.show(
context,
message: t.client_billing.approved_success,
type: UiSnackbarType.success,
);
},
size: UiButtonSize.large,
style: ElevatedButton.styleFrom(
@@ -350,8 +361,8 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
width: double.infinity,
child: Container(
decoration: BoxDecoration(
borderRadius: UiConstants.radiusMd,
border: Border.all(color: Colors.orange, width: 2),
borderRadius: UiConstants.radiusMd,
border: Border.all(color: Colors.orange, width: 2),
),
child: UiButton.secondary(
text: t.client_billing.actions.flag_review,
@@ -409,7 +420,11 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
);
Navigator.pop(dialogContext);
Modular.to.pop();
UiSnackbar.show(context, message: t.client_billing.flagged_success, type: UiSnackbarType.warning);
UiSnackbar.show(
context,
message: t.client_billing.flagged_success,
type: UiSnackbarType.warning,
);
}
},
child: Text(t.client_billing.flag_dialog.button),

View File

@@ -2,6 +2,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/billing_bloc.dart';
import '../blocs/billing_event.dart';
import '../blocs/billing_state.dart';
@@ -14,7 +15,7 @@ class InvoiceReadyPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider<BillingBloc>.value(
value: Modular.get<BillingBloc>()..add(const BillingLoadStarted()),
child: const InvoiceReadyView(),
child: const Placeholder(),
);
}
}
@@ -25,13 +26,7 @@ class InvoiceReadyView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Invoices Ready'),
leading: UiIconButton.secondary(
icon: UiIcons.arrowLeft,
onTap: () => Modular.to.pop(),
),
),
appBar: const UiAppBar(title: 'Invoices Ready', showBackButton: true),
body: BlocBuilder<BillingBloc, BillingState>(
builder: (context, state) {
if (state.status == BillingStatus.loading) {
@@ -43,12 +38,16 @@ class InvoiceReadyView extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(UiIcons.file, size: 64, color: UiColors.iconSecondary),
const SizedBox(height: UiConstants.space4),
Text(
'No invoices ready yet',
style: UiTypography.body1m.textSecondary,
),
const Icon(
UiIcons.file,
size: 64,
color: UiColors.iconSecondary,
),
const SizedBox(height: UiConstants.space4),
Text(
'No invoices ready yet',
style: UiTypography.body1m.textSecondary,
),
],
),
);
@@ -96,26 +95,31 @@ class _InvoiceSummaryCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: UiColors.success.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'READY',
style: UiTypography.titleUppercase4b.copyWith(color: UiColors.success),
style: UiTypography.titleUppercase4b.copyWith(
color: UiColors.success,
),
),
),
Text(
invoice.date,
style: UiTypography.footnote2r.textTertiary,
),
Text(invoice.date, style: UiTypography.footnote2r.textTertiary),
],
),
const SizedBox(height: 16),
Text(invoice.title, style: UiTypography.title2b.textPrimary),
const SizedBox(height: 8),
Text(invoice.locationAddress, style: UiTypography.body2r.textSecondary),
Text(
invoice.locationAddress,
style: UiTypography.body2r.textSecondary,
),
const Divider(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -123,8 +127,14 @@ class _InvoiceSummaryCard extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('TOTAL AMOUNT', style: UiTypography.titleUppercase4m.textSecondary),
Text('\$${invoice.totalAmount.toStringAsFixed(2)}', style: UiTypography.title2b.primary),
Text(
'TOTAL AMOUNT',
style: UiTypography.titleUppercase4m.textSecondary,
),
Text(
'\$${invoice.totalAmount.toStringAsFixed(2)}',
style: UiTypography.title2b.primary,
),
],
),
UiButton.primary(

View File

@@ -25,7 +25,7 @@ class ActionsWidget extends StatelessWidget {
title: i18n.rapid,
subtitle: i18n.rapid_subtitle,
icon: UiIcons.zap,
color: UiColors.tagError,
color: UiColors.tagError.withValues(alpha: 0.5),
borderColor: UiColors.borderError.withValues(alpha: 0.3),
iconBgColor: UiColors.white,
iconColor: UiColors.textError,

View File

@@ -12,18 +12,16 @@ class UiOrderType {
/// Order type constants for the create order feature
const List<UiOrderType> orderTypes = <UiOrderType>[
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
// UiOrderType(
// id: 'rapid',
// titleKey: 'client_create_order.types.rapid',
// descriptionKey: 'client_create_order.types.rapid_desc',
// ),
UiOrderType(
id: 'rapid',
titleKey: 'client_create_order.types.rapid',
descriptionKey: 'client_create_order.types.rapid_desc',
),
UiOrderType(
id: 'one-time',
titleKey: 'client_create_order.types.one_time',
descriptionKey: 'client_create_order.types.one_time_desc',
),
UiOrderType(
id: 'recurring',
titleKey: 'client_create_order.types.recurring',