refactor: extract document upload file selector, attestation checkbox, and footer into dedicated widgets for improved modularity.
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user