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: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_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_event.dart'; import '../blocs/billing_event.dart';
import '../models/billing_invoice_model.dart'; import '../models/billing_invoice_model.dart';
@@ -14,7 +13,8 @@ class ShiftCompletionReviewPage extends StatefulWidget {
final BillingInvoice? invoice; final BillingInvoice? invoice;
@override @override
State<ShiftCompletionReviewPage> createState() => _ShiftCompletionReviewPageState(); State<ShiftCompletionReviewPage> createState() =>
_ShiftCompletionReviewPageState();
} }
class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> { class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
@@ -31,19 +31,22 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
@override @override
Widget build(BuildContext context) { 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; if (searchQuery.isEmpty) return true;
return w.workerName.toLowerCase().contains(searchQuery.toLowerCase()) || return w.workerName.toLowerCase().contains(searchQuery.toLowerCase()) ||
w.roleName.toLowerCase().contains(searchQuery.toLowerCase()); w.roleName.toLowerCase().contains(searchQuery.toLowerCase());
}).toList(); }).toList();
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8FAFC), appBar: UiAppBar(
title: invoice.title,
subtitle: invoice.clientName,
showBackButton: true,
),
body: SafeArea( body: SafeArea(
child: Column( child: Expanded(
children: <Widget>[
_buildHeader(context),
Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
child: Column( child: Column(
@@ -58,7 +61,9 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
_buildSearchAndTabs(), _buildSearchAndTabs(),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
...filteredWorkers.map((BillingWorkerRecord worker) => _buildWorkerCard(worker)), ...filteredWorkers.map(
(BillingWorkerRecord worker) => _buildWorkerCard(worker),
),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
_buildActionButtons(context), _buildActionButtons(context),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
@@ -68,47 +73,6 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
), ),
), ),
), ),
],
),
),
);
}
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(),
),
],
),
],
), ),
); );
} }
@@ -116,14 +80,13 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
Widget _buildInvoiceInfoCard() { Widget _buildInvoiceInfoCard() {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space1,
children: <Widget>[ 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), _buildInfoRow(UiIcons.calendar, invoice.date),
const SizedBox(height: UiConstants.space2), _buildInfoRow(
_buildInfoRow(UiIcons.clock, '${invoice.startTime ?? "--"} - ${invoice.endTime ?? "--"}'), UiIcons.clock,
const SizedBox(height: UiConstants.space2), '${invoice.startTime ?? "--"} - ${invoice.endTime ?? "--"}',
),
_buildInfoRow(UiIcons.mapPin, invoice.locationAddress), _buildInfoRow(UiIcons.mapPin, invoice.locationAddress),
], ],
); );
@@ -194,7 +157,11 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
child: TextField( child: TextField(
onChanged: (String val) => setState(() => searchQuery = val), onChanged: (String val) => setState(() => searchQuery = val),
decoration: InputDecoration( 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, hintText: t.client_billing.workers_tab.search_hint,
hintStyle: UiTypography.body2r.textSecondary, hintStyle: UiTypography.body2r.textSecondary,
border: InputBorder.none, border: InputBorder.none,
@@ -205,11 +172,17 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
Row( Row(
children: <Widget>[ children: <Widget>[
Expanded( 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), const SizedBox(width: UiConstants.space3),
Expanded( 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( decoration: BoxDecoration(
color: isSelected ? const Color(0xFF2563EB) : Colors.white, color: isSelected ? const Color(0xFF2563EB) : Colors.white,
borderRadius: UiConstants.radiusMd, 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: Center(
child: Text( child: Text(
@@ -257,24 +232,44 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
CircleAvatar( CircleAvatar(
radius: 20, radius: 20,
backgroundColor: UiColors.bgSecondary, backgroundColor: UiColors.bgSecondary,
backgroundImage: worker.workerAvatarUrl != null ? NetworkImage(worker.workerAvatarUrl!) : null, backgroundImage: worker.workerAvatarUrl != null
child: worker.workerAvatarUrl == null ? const Icon(UiIcons.user, size: 20, color: UiColors.iconSecondary) : null, ? NetworkImage(worker.workerAvatarUrl!)
: null,
child: worker.workerAvatarUrl == null
? const Icon(
UiIcons.user,
size: 20,
color: UiColors.iconSecondary,
)
: null,
), ),
const SizedBox(width: UiConstants.space3), const SizedBox(width: UiConstants.space3),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text(worker.workerName, style: UiTypography.body1b.textPrimary), Text(
Text(worker.roleName, style: UiTypography.footnote2r.textSecondary), worker.workerName,
style: UiTypography.body1b.textPrimary,
),
Text(
worker.roleName,
style: UiTypography.footnote2r.textSecondary,
),
], ],
), ),
), ),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[ children: <Widget>[
Text('\$${worker.totalAmount.toStringAsFixed(2)}', style: UiTypography.body1b.textPrimary), Text(
Text('${worker.hours}h x \$${worker.rate.toStringAsFixed(2)}/hr', style: UiTypography.footnote2r.textSecondary), '\$${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( Row(
children: <Widget>[ children: <Widget>[
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: UiConstants.radiusMd, borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border), 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), const SizedBox(width: UiConstants.space2),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: UiConstants.radiusMd, borderRadius: UiConstants.radiusMd,
@@ -301,22 +305,23 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
), ),
child: Row( child: Row(
children: [ children: [
const Icon(UiIcons.coffee, size: 12, color: UiColors.iconSecondary), const Icon(
UiIcons.coffee,
size: 12,
color: UiColors.iconSecondary,
),
const SizedBox(width: 4), 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(), const Spacer(),
UiIconButton.secondary( UiIconButton.secondary(icon: UiIcons.edit, onTap: () {}),
icon: UiIcons.edit,
onTap: () {},
),
const SizedBox(width: UiConstants.space2), const SizedBox(width: UiConstants.space2),
UiIconButton.secondary( UiIconButton.secondary(icon: UiIcons.warning, onTap: () {}),
icon: UiIcons.warning,
onTap: () {},
),
], ],
), ),
], ],
@@ -333,9 +338,15 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
text: t.client_billing.actions.approve_pay, text: t.client_billing.actions.approve_pay,
leadingIcon: UiIcons.checkCircle, leadingIcon: UiIcons.checkCircle,
onPressed: () { onPressed: () {
Modular.get<BillingBloc>().add(BillingInvoiceApproved(invoice.id)); Modular.get<BillingBloc>().add(
BillingInvoiceApproved(invoice.id),
);
Modular.to.pop(); 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, size: UiButtonSize.large,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@@ -409,7 +420,11 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
); );
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
Modular.to.pop(); 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), 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/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 '../blocs/billing_bloc.dart'; import '../blocs/billing_bloc.dart';
import '../blocs/billing_event.dart'; import '../blocs/billing_event.dart';
import '../blocs/billing_state.dart'; import '../blocs/billing_state.dart';
@@ -14,7 +15,7 @@ class InvoiceReadyPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<BillingBloc>.value( return BlocProvider<BillingBloc>.value(
value: Modular.get<BillingBloc>()..add(const BillingLoadStarted()), value: Modular.get<BillingBloc>()..add(const BillingLoadStarted()),
child: const InvoiceReadyView(), child: const Placeholder(),
); );
} }
} }
@@ -25,13 +26,7 @@ class InvoiceReadyView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: const UiAppBar(title: 'Invoices Ready', showBackButton: true),
title: const Text('Invoices Ready'),
leading: UiIconButton.secondary(
icon: UiIcons.arrowLeft,
onTap: () => Modular.to.pop(),
),
),
body: BlocBuilder<BillingBloc, BillingState>( body: BlocBuilder<BillingBloc, BillingState>(
builder: (context, state) { builder: (context, state) {
if (state.status == BillingStatus.loading) { if (state.status == BillingStatus.loading) {
@@ -43,7 +38,11 @@ class InvoiceReadyView extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(UiIcons.file, size: 64, color: UiColors.iconSecondary), const Icon(
UiIcons.file,
size: 64,
color: UiColors.iconSecondary,
),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
Text( Text(
'No invoices ready yet', 'No invoices ready yet',
@@ -96,26 +95,31 @@ class _InvoiceSummaryCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.success.withValues(alpha: 0.1), color: UiColors.success.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Text(
'READY', '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), const SizedBox(height: 16),
Text(invoice.title, style: UiTypography.title2b.textPrimary), Text(invoice.title, style: UiTypography.title2b.textPrimary),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(invoice.locationAddress, style: UiTypography.body2r.textSecondary), Text(
invoice.locationAddress,
style: UiTypography.body2r.textSecondary,
),
const Divider(height: 32), const Divider(height: 32),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -123,8 +127,14 @@ class _InvoiceSummaryCard extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('TOTAL AMOUNT', style: UiTypography.titleUppercase4m.textSecondary), Text(
Text('\$${invoice.totalAmount.toStringAsFixed(2)}', style: UiTypography.title2b.primary), 'TOTAL AMOUNT',
style: UiTypography.titleUppercase4m.textSecondary,
),
Text(
'\$${invoice.totalAmount.toStringAsFixed(2)}',
style: UiTypography.title2b.primary,
),
], ],
), ),
UiButton.primary( UiButton.primary(

View File

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

View File

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