diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 2ecac88b..e0544de4 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -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": { diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 00d8d979..599bfa23 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -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": { diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/completion_review_page.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/completion_review_page.dart index 3856857f..d12efc0a 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/completion_review_page.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/completion_review_page.dart @@ -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 { 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 { showBackButton: true, ), body: SafeArea( - child: Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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: [ - _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: [ - 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: [ - 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: [ - 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: [ - 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: [ - 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: [ - Row( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - 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: [ - Text( - worker.workerName, - style: UiTypography.body1b.textPrimary, - ), - Text( - worker.roleName, - style: UiTypography.footnote2r.textSecondary, - ), - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - 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: [ - 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: [ - SizedBox( - width: double.infinity, - child: UiButton.primary( - text: t.client_billing.actions.approve_pay, - leadingIcon: UiIcons.checkCircle, - onPressed: () { - Modular.get().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().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)), ), ); } diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/invoice_ready_page.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/invoice_ready_page.dart index 7ae7b9bf..b1b3bce4 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/invoice_ready_page.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/invoice_ready_page.dart @@ -15,7 +15,7 @@ class InvoiceReadyPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider.value( value: Modular.get()..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( - 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); }, ); diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_actions.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_actions.dart new file mode 100644 index 00000000..c04ce60e --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_actions.dart @@ -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: [ + 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().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: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(t.common.cancel), + ), + TextButton( + onPressed: () { + if (controller.text.isNotEmpty) { + Modular.get().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), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_amount.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_amount.dart new file mode 100644 index 00000000..48f81801 --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_amount.dart @@ -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: [ + 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, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_info.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_info.dart new file mode 100644 index 00000000..6f40f884 --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_info.dart @@ -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: [ + _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: [ + Icon(icon, size: 16, color: UiColors.iconSecondary), + const SizedBox(width: UiConstants.space3), + Text(text, style: UiTypography.body2r.textSecondary), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_search_and_tabs.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_search_and_tabs.dart new file mode 100644 index 00000000..eca816a3 --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_search_and_tabs.dart @@ -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 onTabChanged; + final ValueChanged onSearchChanged; + final int workersCount; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + 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: [ + 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, + ), + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_worker_card.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_worker_card.dart new file mode 100644 index 00000000..f2490ab2 --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_worker_card.dart @@ -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: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: [ + Text( + worker.workerName, + style: UiTypography.body1b.textPrimary, + ), + Text( + worker.roleName, + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + 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: [ + 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: () {}), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_workers_header.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_workers_header.dart new file mode 100644 index 00000000..c743dd99 --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/completion_review/completion_review_workers_header.dart @@ -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: [ + 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, + ), + ], + ); + } +}