feat: introduce completion review UI components for actions, amount, info, search, and worker listing.

This commit is contained in:
Achintha Isuru
2026-02-28 12:49:51 -05:00
parent 752f60405e
commit 119b6cc000
10 changed files with 453 additions and 387 deletions

View File

@@ -541,8 +541,8 @@
"min_break": "min break" "min_break": "min break"
}, },
"actions": { "actions": {
"approve_pay": "Approve & Process Payment", "approve_pay": "Approve",
"flag_review": "Flag for Review", "flag_review": "Review",
"download_pdf": "Download Invoice PDF" "download_pdf": "Download Invoice PDF"
}, },
"flag_dialog": { "flag_dialog": {

View File

@@ -536,8 +536,8 @@
"min_break": "min de descanso" "min_break": "min de descanso"
}, },
"actions": { "actions": {
"approve_pay": "Aprobar y Procesar Pago", "approve_pay": "Aprobar",
"flag_review": "Marcar para Revisi\u00f3n", "flag_review": "Revisi\u00f3n",
"download_pdf": "Descargar PDF de Factura" "download_pdf": "Descargar PDF de Factura"
}, },
"flag_dialog": { "flag_dialog": {

View File

@@ -1,12 +1,16 @@
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_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/billing_bloc.dart';
import '../blocs/billing_event.dart';
import '../models/billing_invoice_model.dart'; import '../models/billing_invoice_model.dart';
import '../widgets/completion_review/completion_review_actions.dart';
import '../widgets/completion_review/completion_review_amount.dart';
import '../widgets/completion_review/completion_review_info.dart';
import '../widgets/completion_review/completion_review_search_and_tabs.dart';
import '../widgets/completion_review/completion_review_worker_card.dart';
import '../widgets/completion_review/completion_review_workers_header.dart';
class ShiftCompletionReviewPage extends StatefulWidget { class ShiftCompletionReviewPage extends StatefulWidget {
const ShiftCompletionReviewPage({this.invoice, super.key}); const ShiftCompletionReviewPage({this.invoice, super.key});
@@ -26,7 +30,7 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
void initState() { void initState() {
super.initState(); super.initState();
// Use widget.invoice if provided, else try to get from arguments // Use widget.invoice if provided, else try to get from arguments
invoice = widget.invoice ?? Modular.args!.data as BillingInvoice; invoice = widget.invoice ?? Modular.args.data as BillingInvoice;
} }
@override @override
@@ -46,390 +50,45 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
showBackButton: true, showBackButton: true,
), ),
body: SafeArea( body: SafeArea(
child: Expanded( child: SingleChildScrollView(
child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), child: Column(
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 _buildInvoiceInfoCard() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space1,
children: <Widget>[
_buildInfoRow(UiIcons.calendar, invoice.date),
_buildInfoRow(
UiIcons.clock,
'${invoice.startTime ?? "--"} - ${invoice.endTime ?? "--"}',
),
_buildInfoRow(UiIcons.mapPin, invoice.locationAddress),
],
);
}
Widget _buildInfoRow(IconData icon, String text) {
return Row(
children: <Widget>[
Icon(icon, size: 16, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space3),
Text(text, style: UiTypography.body2r.textSecondary),
],
);
}
Widget _buildAmountCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF),
borderRadius: UiConstants.radiusLg,
border: Border.all(color: const Color(0xFFDBEAFE)),
),
child: Column(
children: <Widget>[
Text(
t.client_billing.total_amount_label,
style: UiTypography.body2b.copyWith(color: const Color(0xFF2563EB)),
),
const SizedBox(height: UiConstants.space2),
Text(
'\$${invoice.totalAmount.toStringAsFixed(2)}',
style: UiTypography.headline1b.textPrimary.copyWith(fontSize: 40),
),
const SizedBox(height: UiConstants.space1),
Text(
'${invoice.totalHours.toStringAsFixed(1)} ${t.client_billing.hours_suffix}\$${(invoice.totalAmount / (invoice.totalHours > 0.1 ? invoice.totalHours : 1)).toStringAsFixed(2)}${t.client_billing.avg_rate_suffix}',
style: UiTypography.footnote2b.textSecondary,
),
],
),
);
}
Widget _buildWorkersHeader() {
return Row(
children: <Widget>[
const Icon(UiIcons.users, size: 18, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space2),
Text(
t.client_billing.workers_tab.title(count: invoice.workersCount),
style: UiTypography.title2b.textPrimary,
),
],
);
}
Widget _buildSearchAndTabs() {
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: UiConstants.radiusMd,
),
child: TextField(
onChanged: (String val) => setState(() => searchQuery = val),
decoration: InputDecoration(
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,
),
),
),
const SizedBox(height: UiConstants.space4),
Row(
children: <Widget>[
Expanded(
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,
),
),
],
),
],
);
}
Widget _buildTabButton(String text, int index) {
final bool isSelected = selectedTab == index;
return GestureDetector(
onTap: () => setState(() => selectedTab = index),
child: Container(
height: 40,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF2563EB) : Colors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(
color: isSelected ? const Color(0xFF2563EB) : UiColors.border,
),
),
child: Center(
child: Text(
text,
style: UiTypography.body2b.copyWith(
color: isSelected ? Colors.white : UiColors.textSecondary,
),
),
),
),
);
}
Widget _buildWorkerCard(BillingWorkerRecord worker) {
return Container(
margin: const EdgeInsets.only(bottom: UiConstants.space3),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border.withOpacity(0.5)),
),
child: Column(
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
CircleAvatar( const SizedBox(height: UiConstants.space4),
radius: 20, CompletionReviewInfo(invoice: invoice),
backgroundColor: UiColors.bgSecondary, const SizedBox(height: UiConstants.space4),
backgroundImage: worker.workerAvatarUrl != null CompletionReviewAmount(invoice: invoice),
? NetworkImage(worker.workerAvatarUrl!) const SizedBox(height: UiConstants.space6),
: null, CompletionReviewWorkersHeader(workersCount: invoice.workersCount),
child: worker.workerAvatarUrl == null const SizedBox(height: UiConstants.space4),
? const Icon( CompletionReviewSearchAndTabs(
UiIcons.user, selectedTab: selectedTab,
size: 20, workersCount: invoice.workersCount,
color: UiColors.iconSecondary, onTabChanged: (int index) =>
) setState(() => selectedTab = index),
: null, onSearchChanged: (String val) =>
setState(() => searchQuery = val),
), ),
const SizedBox(width: UiConstants.space3), const SizedBox(height: UiConstants.space4),
Expanded( ...filteredWorkers.map(
child: Column( (BillingWorkerRecord worker) =>
crossAxisAlignment: CrossAxisAlignment.start, CompletionReviewWorkerCard(worker: worker),
children: <Widget>[
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,
),
],
), ),
const SizedBox(height: UiConstants.space4),
], ],
), ),
const SizedBox(height: UiConstants.space4),
Row(
children: <Widget>[
Container(
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,
),
),
const SizedBox(width: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: Row(
children: [
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,
),
],
),
),
const Spacer(),
UiIconButton.secondary(icon: UiIcons.edit, onTap: () {}),
const SizedBox(width: UiConstants.space2),
UiIconButton.secondary(icon: UiIcons.warning, onTap: () {}),
],
),
],
),
);
}
Widget _buildActionButtons(BuildContext context) {
return Column(
children: <Widget>[
SizedBox(
width: double.infinity,
child: UiButton.primary(
text: t.client_billing.actions.approve_pay,
leadingIcon: UiIcons.checkCircle,
onPressed: () {
Modular.get<BillingBloc>().add(
BillingInvoiceApproved(invoice.id),
);
Modular.to.pop();
UiSnackbar.show(
context,
message: t.client_billing.approved_success,
type: UiSnackbarType.success,
);
},
size: UiButtonSize.large,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF22C55E),
foregroundColor: Colors.white,
textStyle: UiTypography.body1b.copyWith(fontSize: 16),
),
),
),
const SizedBox(height: UiConstants.space3),
SizedBox(
width: double.infinity,
child: Container(
decoration: BoxDecoration(
borderRadius: UiConstants.radiusMd,
border: Border.all(color: Colors.orange, width: 2),
),
child: UiButton.secondary(
text: t.client_billing.actions.flag_review,
leadingIcon: UiIcons.warning,
onPressed: () => _showFlagDialog(context),
size: UiButtonSize.large,
style: OutlinedButton.styleFrom(
foregroundColor: Colors.orange,
side: BorderSide.none,
textStyle: UiTypography.body1b.copyWith(fontSize: 16),
),
),
),
),
],
);
}
Widget _buildDownloadLink() {
return Center(
child: TextButton.icon(
onPressed: () {},
icon: const Icon(UiIcons.download, size: 16, color: Color(0xFF2563EB)),
label: Text(
t.client_billing.actions.download_pdf,
style: UiTypography.body2b.copyWith(color: const Color(0xFF2563EB)),
), ),
), ),
); bottomNavigationBar: Container(
} padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
void _showFlagDialog(BuildContext context) { color: Colors.white,
final controller = TextEditingController(); border: Border(
showDialog( top: BorderSide(color: UiColors.border.withValues(alpha: 0.5)),
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(t.client_billing.flag_dialog.title),
content: TextField(
controller: controller,
decoration: InputDecoration(
hintText: t.client_billing.flag_dialog.hint,
), ),
maxLines: 3,
), ),
actions: [ child: SafeArea(child: CompletionReviewActions(invoiceId: invoice.id)),
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(t.common.cancel),
),
TextButton(
onPressed: () {
if (controller.text.isNotEmpty) {
Modular.get<BillingBloc>().add(
BillingInvoiceDisputed(invoice.id, controller.text),
);
Navigator.pop(dialogContext);
Modular.to.pop();
UiSnackbar.show(
context,
message: t.client_billing.flagged_success,
type: UiSnackbarType.warning,
);
}
},
child: Text(t.client_billing.flag_dialog.button),
),
],
), ),
); );
} }

View File

@@ -15,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 Placeholder(), child: const InvoiceReadyView(),
); );
} }
} }
@@ -28,7 +28,7 @@ class InvoiceReadyView extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: const UiAppBar(title: 'Invoices Ready', showBackButton: true), appBar: const UiAppBar(title: 'Invoices Ready', showBackButton: true),
body: BlocBuilder<BillingBloc, BillingState>( body: BlocBuilder<BillingBloc, BillingState>(
builder: (context, state) { builder: (BuildContext context, BillingState state) {
if (state.status == BillingStatus.loading) { if (state.status == BillingStatus.loading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
@@ -56,9 +56,10 @@ class InvoiceReadyView extends StatelessWidget {
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(UiConstants.space5), padding: const EdgeInsets.all(UiConstants.space5),
itemCount: state.invoiceHistory.length, itemCount: state.invoiceHistory.length,
separatorBuilder: (context, index) => const SizedBox(height: 16), separatorBuilder: (BuildContext context, int index) =>
itemBuilder: (context, index) { const SizedBox(height: 16),
final invoice = state.invoiceHistory[index]; itemBuilder: (BuildContext context, int index) {
final BillingInvoice invoice = state.invoiceHistory[index];
return _InvoiceSummaryCard(invoice: invoice); return _InvoiceSummaryCard(invoice: invoice);
}, },
); );

View File

@@ -0,0 +1,90 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../../blocs/billing_bloc.dart';
import '../../blocs/billing_event.dart';
class CompletionReviewActions extends StatelessWidget {
const CompletionReviewActions({required this.invoiceId, super.key});
final String invoiceId;
@override
Widget build(BuildContext context) {
return Row(
spacing: UiConstants.space2,
children: <Widget>[
Expanded(
child: UiButton.secondary(
text: t.client_billing.actions.flag_review,
leadingIcon: UiIcons.warning,
onPressed: () => _showFlagDialog(context),
size: UiButtonSize.large,
style: OutlinedButton.styleFrom(
foregroundColor: UiColors.destructive,
side: BorderSide.none,
),
),
),
Expanded(
child: UiButton.primary(
text: t.client_billing.actions.approve_pay,
leadingIcon: UiIcons.checkCircle,
onPressed: () {
Modular.get<BillingBloc>().add(BillingInvoiceApproved(invoiceId));
Modular.to.pop();
UiSnackbar.show(
context,
message: t.client_billing.approved_success,
type: UiSnackbarType.success,
);
},
size: UiButtonSize.large,
),
),
],
);
}
void _showFlagDialog(BuildContext context) {
final TextEditingController controller = TextEditingController();
showDialog(
context: context,
builder: (BuildContext dialogContext) => AlertDialog(
title: Text(t.client_billing.flag_dialog.title),
content: TextField(
controller: controller,
decoration: InputDecoration(
hintText: t.client_billing.flag_dialog.hint,
),
maxLines: 3,
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(t.common.cancel),
),
TextButton(
onPressed: () {
if (controller.text.isNotEmpty) {
Modular.get<BillingBloc>().add(
BillingInvoiceDisputed(invoiceId, controller.text),
);
Navigator.pop(dialogContext);
Modular.to.pop();
UiSnackbar.show(
context,
message: t.client_billing.flagged_success,
type: UiSnackbarType.warning,
);
}
},
child: Text(t.client_billing.flag_dialog.button),
),
],
),
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import '../../models/billing_invoice_model.dart';
class CompletionReviewAmount extends StatelessWidget {
const CompletionReviewAmount({required this.invoice, super.key});
final BillingInvoice invoice;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF),
borderRadius: UiConstants.radiusLg,
border: Border.all(color: const Color(0xFFDBEAFE)),
),
child: Column(
children: <Widget>[
Text(
t.client_billing.total_amount_label,
style: UiTypography.body2b.copyWith(color: const Color(0xFF2563EB)),
),
const SizedBox(height: UiConstants.space2),
Text(
'\$${invoice.totalAmount.toStringAsFixed(2)}',
style: UiTypography.headline1b.textPrimary.copyWith(fontSize: 40),
),
const SizedBox(height: UiConstants.space1),
Text(
'${invoice.totalHours.toStringAsFixed(1)} ${t.client_billing.hours_suffix}\$${(invoice.totalAmount / (invoice.totalHours > 0.1 ? invoice.totalHours : 1)).toStringAsFixed(2)}${t.client_billing.avg_rate_suffix}',
style: UiTypography.footnote2b.textSecondary,
),
],
),
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import '../../models/billing_invoice_model.dart';
class CompletionReviewInfo extends StatelessWidget {
const CompletionReviewInfo({required this.invoice, super.key});
final BillingInvoice invoice;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space1,
children: <Widget>[
_buildInfoRow(UiIcons.calendar, invoice.date),
_buildInfoRow(
UiIcons.clock,
'${invoice.startTime ?? "--"} - ${invoice.endTime ?? "--"}',
),
_buildInfoRow(UiIcons.mapPin, invoice.locationAddress),
],
);
}
Widget _buildInfoRow(IconData icon, String text) {
return Row(
children: <Widget>[
Icon(icon, size: 16, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space3),
Text(text, style: UiTypography.body2r.textSecondary),
],
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
class CompletionReviewSearchAndTabs extends StatelessWidget {
const CompletionReviewSearchAndTabs({
required this.selectedTab,
required this.onTabChanged,
required this.onSearchChanged,
required this.workersCount,
super.key,
});
final int selectedTab;
final ValueChanged<int> onTabChanged;
final ValueChanged<String> onSearchChanged;
final int workersCount;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: UiConstants.radiusMd,
),
child: TextField(
onChanged: onSearchChanged,
decoration: InputDecoration(
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,
),
),
),
const SizedBox(height: UiConstants.space4),
Row(
children: <Widget>[
Expanded(
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: workersCount),
1,
),
),
],
),
],
);
}
Widget _buildTabButton(String text, int index) {
final bool isSelected = selectedTab == index;
return GestureDetector(
onTap: () => onTabChanged(index),
child: Container(
height: 40,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF2563EB) : Colors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(
color: isSelected ? const Color(0xFF2563EB) : UiColors.border,
),
),
child: Center(
child: Text(
text,
style: UiTypography.body2b.copyWith(
color: isSelected ? Colors.white : UiColors.textSecondary,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,126 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import '../../models/billing_invoice_model.dart';
class CompletionReviewWorkerCard extends StatelessWidget {
const CompletionReviewWorkerCard({required this.worker, super.key});
final BillingWorkerRecord worker;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: UiConstants.space3),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border.withValues(alpha: 0.5)),
),
child: Column(
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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,
),
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,
),
],
),
),
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,
),
],
),
],
),
const SizedBox(height: UiConstants.space4),
Row(
children: <Widget>[
Container(
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,
),
),
const SizedBox(width: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: Row(
children: <Widget>[
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,
),
],
),
),
const Spacer(),
UiIconButton.secondary(icon: UiIcons.edit, onTap: () {}),
const SizedBox(width: UiConstants.space2),
UiIconButton.secondary(icon: UiIcons.warning, onTap: () {}),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
class CompletionReviewWorkersHeader extends StatelessWidget {
const CompletionReviewWorkersHeader({required this.workersCount, super.key});
final int workersCount;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
const Icon(UiIcons.users, size: 18, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space2),
Text(
t.client_billing.workers_tab.title(count: workersCount),
style: UiTypography.title2b.textPrimary,
),
],
);
}
}