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:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
// ignore: depend_on_referenced_packages
|
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
import '../blocs/document_upload/document_upload_cubit.dart';
|
import '../blocs/document_upload/document_upload_cubit.dart';
|
||||||
import '../blocs/document_upload/document_upload_state.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.
|
/// Allows staff to select and submit a single PDF document for verification.
|
||||||
///
|
///
|
||||||
@@ -72,134 +74,59 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: UiAppBar(
|
appBar: UiAppBar(
|
||||||
title: widget.document.name,
|
title: widget.document.name,
|
||||||
|
subtitle: widget.document.description,
|
||||||
onLeadingPressed: () => Modular.to.toDocuments(),
|
onLeadingPressed: () => Modular.to.toDocuments(),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Text(
|
||||||
child: SingleChildScrollView(
|
t.staff_documents.upload.instructions,
|
||||||
child: Column(
|
style: UiTypography.body1m.textPrimary,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: <Widget>[
|
const SizedBox(height: UiConstants.space6),
|
||||||
Text(
|
DocumentFileSelector(
|
||||||
t.staff_documents.upload.instructions,
|
selectedFilePath: _selectedFilePath,
|
||||||
style: UiTypography.body1m.textPrimary,
|
onTap: _pickFile,
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
if (widget.document.description != null)
|
|
||||||
Text(
|
|
||||||
widget.document.description!,
|
|
||||||
style: UiTypography.body2r.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
_buildFileSelector(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
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