diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/document_upload_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/document_upload_page.dart index b5d7e806..b9eee85a 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/document_upload_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/pages/document_upload_page.dart @@ -4,11 +4,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; -// ignore: depend_on_referenced_packages import 'package:core_localization/core_localization.dart'; import '../blocs/document_upload/document_upload_cubit.dart'; import '../blocs/document_upload/document_upload_state.dart'; +import '../widgets/document_upload/document_attestation_checkbox.dart'; +import '../widgets/document_upload/document_file_selector.dart'; +import '../widgets/document_upload/document_upload_footer.dart'; /// Allows staff to select and submit a single PDF document for verification. /// @@ -72,134 +74,59 @@ class _DocumentUploadPageState extends State { return Scaffold( appBar: UiAppBar( title: widget.document.name, + subtitle: widget.document.description, onLeadingPressed: () => Modular.to.toDocuments(), ), - body: Padding( + body: SingleChildScrollView( padding: const EdgeInsets.all(UiConstants.space5), child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - t.staff_documents.upload.instructions, - style: UiTypography.body1m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - if (widget.document.description != null) - Text( - widget.document.description!, - style: UiTypography.body2r.textSecondary, - ), - const SizedBox(height: UiConstants.space6), - _buildFileSelector(), - ], - ), - ), + Text( + t.staff_documents.upload.instructions, + style: UiTypography.body1m.textPrimary, + ), + const SizedBox(height: UiConstants.space6), + DocumentFileSelector( + selectedFilePath: _selectedFilePath, + onTap: _pickFile, ), - const SizedBox(height: UiConstants.space4), - _buildAttestationCheckbox(context, state), - const SizedBox(height: UiConstants.space4), - if (state.status == DocumentUploadStatus.uploading) - const Center( - child: Padding( - padding: EdgeInsets.all(UiConstants.space4), - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - UiColors.primary, - ), - ), - ), - ) - else - UiButton.primary( - fullWidth: true, - onPressed: _selectedFilePath != null && state.isAttested - ? () => BlocProvider.of(context) - .uploadDocument( - widget.document.id, - _selectedFilePath!, - ) - : null, - text: t.staff_documents.upload.submit, - ), ], ), ), + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + vertical: UiConstants.space4, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DocumentAttestationCheckbox( + isAttested: state.isAttested, + onChanged: (bool value) => + BlocProvider.of( + context, + ).setAttested(value), + ), + const SizedBox(height: UiConstants.space4), + DocumentUploadFooter( + isUploading: + state.status == DocumentUploadStatus.uploading, + canSubmit: _selectedFilePath != null && state.isAttested, + onSubmit: () => BlocProvider.of( + context, + ).uploadDocument(widget.document.id, _selectedFilePath!), + ), + ], + ), + ), + ), ); }, ), ); } - - Widget _buildFileSelector() { - return InkWell( - onTap: _pickFile, - borderRadius: UiConstants.radiusLg, - child: Container( - height: 180, - decoration: BoxDecoration( - color: UiColors.bgPopup, - borderRadius: UiConstants.radiusLg, - border: Border.all( - color: _selectedFilePath != null - ? UiColors.primary - : UiColors.border, - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - _selectedFilePath != null ? UiIcons.file : UiIcons.upload, - size: 48, - color: _selectedFilePath != null - ? UiColors.primary - : UiColors.textSecondary, - ), - const SizedBox(height: UiConstants.space2), - Text( - _selectedFilePath != null - ? _selectedFilePath!.split('/').last - : t.staff_documents.upload.select_pdf, - style: UiTypography.body2m.copyWith( - color: _selectedFilePath != null - ? UiColors.primary - : UiColors.textSecondary, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ); - } - - Widget _buildAttestationCheckbox( - BuildContext context, - DocumentUploadState state, - ) { - return Row( - children: [ - Checkbox( - value: state.isAttested, - onChanged: (bool? value) => BlocProvider.of( - context, - ).setAttested(value ?? false), - activeColor: UiColors.primary, - ), - Expanded( - child: Text( - t.staff_documents.upload.attestation, - style: UiTypography.body2r.textPrimary, - ), - ), - ], - ); - } } diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_attestation_checkbox.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_attestation_checkbox.dart new file mode 100644 index 00000000..51f14338 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_attestation_checkbox.dart @@ -0,0 +1,41 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +// ignore: depend_on_referenced_packages +import 'package:core_localization/core_localization.dart'; + +/// A labeled checkbox confirming the document is genuine before submission. +/// +/// Renders an attestation statement alongside a checkbox. The [onChanged] +/// callback is fired whenever the user toggles the checkbox. +class DocumentAttestationCheckbox extends StatelessWidget { + const DocumentAttestationCheckbox({ + super.key, + required this.isAttested, + required this.onChanged, + }); + + /// Whether the user has currently checked the attestation box. + final bool isAttested; + + /// Called with the new value when the checkbox is toggled. + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Checkbox( + value: isAttested, + onChanged: (bool? value) => onChanged(value ?? false), + activeColor: UiColors.primary, + ), + Expanded( + child: Text( + t.staff_documents.upload.attestation, + style: UiTypography.body2r.textPrimary, + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_file_selector.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_file_selector.dart new file mode 100644 index 00000000..f21c379f --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_file_selector.dart @@ -0,0 +1,64 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +// ignore: depend_on_referenced_packages +import 'package:core_localization/core_localization.dart'; + +/// Displays a tappable card that prompts the user to pick a PDF file. +/// +/// Shows the selected file name when a file has been chosen, or an +/// upload icon with a prompt when no file is selected yet. +class DocumentFileSelector extends StatelessWidget { + const DocumentFileSelector({ + super.key, + required this.onTap, + this.selectedFilePath, + }); + + /// Called when the user taps the selector to pick a file. + final VoidCallback onTap; + + /// The local path of the currently selected file, or null if none chosen. + final String? selectedFilePath; + + bool get _hasFile => selectedFilePath != null; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: UiConstants.radiusLg, + child: Container( + height: 180, + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: UiConstants.radiusLg, + border: Border.all( + color: _hasFile ? UiColors.primary : UiColors.border, + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _hasFile ? UiIcons.file : UiIcons.upload, + size: 48, + color: _hasFile ? UiColors.primary : UiColors.textSecondary, + ), + const SizedBox(height: UiConstants.space2), + Text( + _hasFile + ? selectedFilePath!.split('/').last + : t.staff_documents.upload.select_pdf, + style: UiTypography.body2m.copyWith( + color: _hasFile ? UiColors.primary : UiColors.textSecondary, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_upload_footer.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_upload_footer.dart new file mode 100644 index 00000000..314932ff --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_upload_footer.dart @@ -0,0 +1,47 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +// ignore: depend_on_referenced_packages +import 'package:core_localization/core_localization.dart'; + +/// Renders the bottom action area of the document upload page. +/// +/// Shows a [CircularProgressIndicator] while [isUploading] is true, +/// otherwise shows a primary submit button. The button is only enabled +/// when both a file has been selected and the user has attested. +class DocumentUploadFooter extends StatelessWidget { + const DocumentUploadFooter({ + super.key, + required this.isUploading, + required this.canSubmit, + required this.onSubmit, + }); + + /// Whether the upload is currently in progress. + final bool isUploading; + + /// Whether all preconditions (file selected + attested) have been met. + final bool canSubmit; + + /// Called when the user taps the submit button. + final VoidCallback onSubmit; + + @override + Widget build(BuildContext context) { + if (isUploading) { + return const Center( + child: Padding( + padding: EdgeInsets.all(UiConstants.space4), + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(UiColors.primary), + ), + ), + ); + } + + return UiButton.primary( + fullWidth: true, + onPressed: canSubmit ? onSubmit : null, + text: t.staff_documents.upload.submit, + ); + } +}