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"
},
"actions": {
"approve_pay": "Approve & Process Payment",
"flag_review": "Flag for Review",
"approve_pay": "Approve",
"flag_review": "Review",
"download_pdf": "Download Invoice PDF"
},
"flag_dialog": {

View File

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

View File

@@ -1,12 +1,16 @@
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';
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 {
const ShiftCompletionReviewPage({this.invoice, super.key});
@@ -26,7 +30,7 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
void initState() {
super.initState();
// 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
@@ -46,390 +50,45 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
showBackButton: true,
),
body: SafeArea(
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 _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(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
child: Column(
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(height: UiConstants.space4),
CompletionReviewInfo(invoice: invoice),
const SizedBox(height: UiConstants.space4),
CompletionReviewAmount(invoice: invoice),
const SizedBox(height: UiConstants.space6),
CompletionReviewWorkersHeader(workersCount: invoice.workersCount),
const SizedBox(height: UiConstants.space4),
CompletionReviewSearchAndTabs(
selectedTab: selectedTab,
workersCount: invoice.workersCount,
onTabChanged: (int index) =>
setState(() => selectedTab = index),
onSearchChanged: (String val) =>
setState(() => searchQuery = val),
),
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),
...filteredWorkers.map(
(BillingWorkerRecord worker) =>
CompletionReviewWorkerCard(worker: worker),
),
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)),
),
),
);
}
void _showFlagDialog(BuildContext context) {
final controller = TextEditingController();
showDialog(
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,
bottomNavigationBar: Container(
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(color: UiColors.border.withValues(alpha: 0.5)),
),
maxLines: 3,
),
actions: [
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),
),
],
child: SafeArea(child: CompletionReviewActions(invoiceId: invoice.id)),
),
);
}

View File

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