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:
@@ -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,19 +31,22 @@ 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());
|
||||
}).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: Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||
child: Column(
|
||||
@@ -58,7 +61,9 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildSearchAndTabs(),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
...filteredWorkers.map((BillingWorkerRecord worker) => _buildWorkerCard(worker)),
|
||||
...filteredWorkers.map(
|
||||
(BillingWorkerRecord worker) => _buildWorkerCard(worker),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
_buildActionButtons(context),
|
||||
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() {
|
||||
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(
|
||||
@@ -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),
|
||||
|
||||
@@ -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,7 +38,11 @@ class InvoiceReadyView extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(UiIcons.file, size: 64, color: UiColors.iconSecondary),
|
||||
const Icon(
|
||||
UiIcons.file,
|
||||
size: 64,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'No invoices ready yet',
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user