refactor: extract document upload file selector, attestation checkbox, and footer into dedicated widgets for improved modularity.

This commit is contained in:
Achintha Isuru
2026-02-26 16:21:02 -05:00
parent 1aa5132abe
commit 4995ff435d
4 changed files with 196 additions and 117 deletions

View File

@@ -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<DocumentUploadPage> {
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: <Widget>[
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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<Color>(
UiColors.primary,
),
),
),
)
else
UiButton.primary(
fullWidth: true,
onPressed: _selectedFilePath != null && state.isAttested
? () => BlocProvider.of<DocumentUploadCubit>(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: <Widget>[
DocumentAttestationCheckbox(
isAttested: state.isAttested,
onChanged: (bool value) =>
BlocProvider.of<DocumentUploadCubit>(
context,
).setAttested(value),
),
const SizedBox(height: UiConstants.space4),
DocumentUploadFooter(
isUploading:
state.status == DocumentUploadStatus.uploading,
canSubmit: _selectedFilePath != null && state.isAttested,
onSubmit: () => BlocProvider.of<DocumentUploadCubit>(
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: <Widget>[
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: <Widget>[
Checkbox(
value: state.isAttested,
onChanged: (bool? value) => BlocProvider.of<DocumentUploadCubit>(
context,
).setAttested(value ?? false),
activeColor: UiColors.primary,
),
Expanded(
child: Text(
t.staff_documents.upload.attestation,
style: UiTypography.body2r.textPrimary,
),
),
],
);
}
}

View File

@@ -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<bool> onChanged;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Checkbox(
value: isAttested,
onChanged: (bool? value) => onChanged(value ?? false),
activeColor: UiColors.primary,
),
Expanded(
child: Text(
t.staff_documents.upload.attestation,
style: UiTypography.body2r.textPrimary,
),
),
],
);
}
}

View File

@@ -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: <Widget>[
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,
),
],
),
),
),
);
}
}

View File

@@ -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<Color>(UiColors.primary),
),
),
);
}
return UiButton.primary(
fullWidth: true,
onPressed: canSubmit ? onSubmit : null,
text: t.staff_documents.upload.submit,
);
}
}