refactor: Decompose AttireCapturePage into dedicated widgets for info, image preview, and footer sections, and refine attestation and verification status logic.
This commit is contained in:
@@ -8,19 +8,23 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_cubit.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_state.dart';
|
||||
|
||||
import '../widgets/attestation_checkbox.dart';
|
||||
import '../widgets/attire_capture_page/attire_image_preview.dart';
|
||||
import '../widgets/attire_capture_page/attire_upload_buttons.dart';
|
||||
import '../widgets/attire_capture_page/attire_verification_status_card.dart';
|
||||
import '../widgets/attire_capture_page/footer_section.dart';
|
||||
import '../widgets/attire_capture_page/image_preview_section.dart';
|
||||
import '../widgets/attire_capture_page/info_section.dart';
|
||||
|
||||
/// The [AttireCapturePage] allows users to capture or upload a photo of a specific attire item.
|
||||
class AttireCapturePage extends StatefulWidget {
|
||||
/// Creates an [AttireCapturePage].
|
||||
const AttireCapturePage({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.initialPhotoUrl,
|
||||
});
|
||||
|
||||
/// The attire item being captured.
|
||||
final AttireItem item;
|
||||
|
||||
/// Optional initial photo URL if it was already uploaded.
|
||||
final String? initialPhotoUrl;
|
||||
|
||||
@override
|
||||
@@ -30,13 +34,21 @@ class AttireCapturePage extends StatefulWidget {
|
||||
class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
String? _selectedLocalPath;
|
||||
|
||||
/// Whether a verification status is already present for this item.
|
||||
bool get _hasVerificationStatus => widget.item.verificationStatus != null;
|
||||
|
||||
/// Whether the item is currently pending verification.
|
||||
bool get _isPending =>
|
||||
widget.item.verificationStatus == AttireVerificationStatus.pending;
|
||||
|
||||
/// On gallery button press
|
||||
Future<void> _onGallery(BuildContext context) async {
|
||||
final AttireCaptureCubit cubit = BlocProvider.of<AttireCaptureCubit>(
|
||||
context,
|
||||
);
|
||||
|
||||
if (!cubit.state.isAttested) {
|
||||
// Skip attestation check if we already have a verification status
|
||||
if (!_hasVerificationStatus && !cubit.state.isAttested) {
|
||||
_showAttestationWarning(context);
|
||||
return;
|
||||
}
|
||||
@@ -62,7 +74,8 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
context,
|
||||
);
|
||||
|
||||
if (!cubit.state.isAttested) {
|
||||
// Skip attestation check if we already have a verification status
|
||||
if (!_hasVerificationStatus && !cubit.state.isAttested) {
|
||||
_showAttestationWarning(context);
|
||||
return;
|
||||
}
|
||||
@@ -82,6 +95,36 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a bottom sheet for reuploading options.
|
||||
void _onReupload(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext sheetContext) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('Gallery'),
|
||||
onTap: () {
|
||||
Modular.to.pop();
|
||||
_onGallery(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Camera'),
|
||||
onTap: () {
|
||||
Modular.to.pop();
|
||||
_onCamera(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAttestationWarning(BuildContext context) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
@@ -119,6 +162,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
return switch (widget.item.verificationStatus) {
|
||||
AttireVerificationStatus.approved => 'Approved',
|
||||
AttireVerificationStatus.rejected => 'Rejected',
|
||||
AttireVerificationStatus.pending => 'Pending Verification',
|
||||
_ => hasUploadedPhoto ? 'Pending Verification' : 'Not Uploaded',
|
||||
};
|
||||
}
|
||||
@@ -127,6 +171,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
return switch (widget.item.verificationStatus) {
|
||||
AttireVerificationStatus.approved => UiColors.textSuccess,
|
||||
AttireVerificationStatus.rejected => UiColors.textError,
|
||||
AttireVerificationStatus.pending => UiColors.textWarning,
|
||||
_ => hasUploadedPhoto ? UiColors.textWarning : UiColors.textInactive,
|
||||
};
|
||||
}
|
||||
@@ -155,16 +200,10 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, AttireCaptureState state) {
|
||||
final bool isUploading =
|
||||
state.status == AttireCaptureStatus.uploading;
|
||||
final String? currentPhotoUrl =
|
||||
state.photoUrl ?? widget.initialPhotoUrl;
|
||||
final bool hasUploadedPhoto = currentPhotoUrl != null;
|
||||
|
||||
final String statusText = _getStatusText(hasUploadedPhoto);
|
||||
|
||||
final Color statusColor = _getStatusColor(hasUploadedPhoto);
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
@@ -172,139 +211,38 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Image Preview (Toggle between example, review, and uploaded)
|
||||
if (_selectedLocalPath != null) ...<Widget>[
|
||||
Text(
|
||||
'Review the attire item',
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
AttireImagePreview(localPath: _selectedLocalPath),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Reference Example',
|
||||
style: UiTypography.body2b.textSecondary,
|
||||
ImagePreviewSection(
|
||||
selectedLocalPath: _selectedLocalPath,
|
||||
currentPhotoUrl: currentPhotoUrl,
|
||||
referenceImageUrl: widget.item.imageUrl,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
child: Image.network(
|
||||
widget.item.imageUrl ?? '',
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, _, _) =>
|
||||
const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (hasUploadedPhoto) ...<Widget>[
|
||||
Text(
|
||||
'Your Uploaded Photo',
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
AttireImagePreview(imageUrl: currentPhotoUrl),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Reference Example',
|
||||
style: UiTypography.body2b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
child: Image.network(
|
||||
widget.item.imageUrl ?? '',
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, _, _) =>
|
||||
const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...<Widget>[
|
||||
AttireImagePreview(
|
||||
imageUrl: widget.item.imageUrl,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Example of the item that you need to upload.',
|
||||
style: UiTypography.body1b.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
if (widget.item.description != null)
|
||||
Text(
|
||||
widget.item.description!,
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
|
||||
// Verification info
|
||||
AttireVerificationStatusCard(
|
||||
statusText: statusText,
|
||||
statusColor: statusColor,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
AttestationCheckbox(
|
||||
isChecked: state.isAttested,
|
||||
onChanged: (bool? val) {
|
||||
InfoSection(
|
||||
description: widget.item.description,
|
||||
statusText: _getStatusText(hasUploadedPhoto),
|
||||
statusColor: _getStatusColor(hasUploadedPhoto),
|
||||
isPending: _isPending,
|
||||
showCheckbox: !_hasVerificationStatus,
|
||||
isAttested: state.isAttested,
|
||||
onAttestationChanged: (bool? val) {
|
||||
cubit.toggleAttestation(val ?? false);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (isUploading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else ...<Widget>[
|
||||
AttireUploadButtons(
|
||||
FooterSection(
|
||||
isUploading:
|
||||
state.status == AttireCaptureStatus.uploading,
|
||||
selectedLocalPath: _selectedLocalPath,
|
||||
hasVerificationStatus: _hasVerificationStatus,
|
||||
hasUploadedPhoto: hasUploadedPhoto,
|
||||
updatedItem: state.updatedItem,
|
||||
onGallery: () => _onGallery(context),
|
||||
onCamera: () => _onCamera(context),
|
||||
),
|
||||
if (_selectedLocalPath != null) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiButton.primary(
|
||||
fullWidth: true,
|
||||
text: 'Submit Image',
|
||||
onPressed: () => _onSubmit(context),
|
||||
),
|
||||
] else if (hasUploadedPhoto) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiButton.primary(
|
||||
fullWidth: true,
|
||||
text: 'Submit Image',
|
||||
onPressed: () {
|
||||
Modular.to.pop(state.updatedItem);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
onSubmit: () => _onSubmit(context),
|
||||
onReupload: () => _onReupload(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'attire_upload_buttons.dart';
|
||||
|
||||
/// Handles the primary actions at the bottom of the page.
|
||||
class FooterSection extends StatelessWidget {
|
||||
/// Creates a [FooterSection].
|
||||
const FooterSection({
|
||||
super.key,
|
||||
required this.isUploading,
|
||||
this.selectedLocalPath,
|
||||
required this.hasVerificationStatus,
|
||||
required this.hasUploadedPhoto,
|
||||
this.updatedItem,
|
||||
required this.onGallery,
|
||||
required this.onCamera,
|
||||
required this.onSubmit,
|
||||
required this.onReupload,
|
||||
});
|
||||
|
||||
/// Whether a photo is currently being uploaded.
|
||||
final bool isUploading;
|
||||
|
||||
/// The local path of the selected photo.
|
||||
final String? selectedLocalPath;
|
||||
|
||||
/// Whether the item already has a verification status.
|
||||
final bool hasVerificationStatus;
|
||||
|
||||
/// Whether the item has an uploaded photo.
|
||||
final bool hasUploadedPhoto;
|
||||
|
||||
/// The updated attire item, if any.
|
||||
final AttireItem? updatedItem;
|
||||
|
||||
/// Callback to open the gallery.
|
||||
final VoidCallback onGallery;
|
||||
|
||||
/// Callback to open the camera.
|
||||
final VoidCallback onCamera;
|
||||
|
||||
/// Callback to submit the photo.
|
||||
final VoidCallback onSubmit;
|
||||
|
||||
/// Callback to trigger the re-upload flow.
|
||||
final VoidCallback onReupload;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (isUploading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
if (selectedLocalPath != null) {
|
||||
return UiButton.primary(
|
||||
fullWidth: true,
|
||||
text: 'Submit Image',
|
||||
onPressed: onSubmit,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasVerificationStatus) {
|
||||
return UiButton.secondary(
|
||||
fullWidth: true,
|
||||
text: 'Re Upload',
|
||||
onPressed: onReupload,
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
AttireUploadButtons(onGallery: onGallery, onCamera: onCamera),
|
||||
if (hasUploadedPhoto) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiButton.primary(
|
||||
fullWidth: true,
|
||||
text: 'Submit Image',
|
||||
onPressed: () {
|
||||
if (updatedItem != null) {
|
||||
Modular.to.pop(updatedItem);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'attire_image_preview.dart';
|
||||
|
||||
/// Displays the comparison between the reference example and the user's photo.
|
||||
class ImagePreviewSection extends StatelessWidget {
|
||||
/// Creates an [ImagePreviewSection].
|
||||
const ImagePreviewSection({
|
||||
super.key,
|
||||
this.selectedLocalPath,
|
||||
this.currentPhotoUrl,
|
||||
this.referenceImageUrl,
|
||||
});
|
||||
|
||||
/// The local file path of the selected image.
|
||||
final String? selectedLocalPath;
|
||||
|
||||
/// The URL of the currently uploaded photo.
|
||||
final String? currentPhotoUrl;
|
||||
|
||||
/// The URL of the reference example image.
|
||||
final String? referenceImageUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (selectedLocalPath != null) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Review the attire item',
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
AttireImagePreview(localPath: selectedLocalPath),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
ReferenceExample(imageUrl: referenceImageUrl),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (currentPhotoUrl != null) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Text('Your Uploaded Photo', style: UiTypography.body1b.textPrimary),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
AttireImagePreview(imageUrl: currentPhotoUrl),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
ReferenceExample(imageUrl: referenceImageUrl),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
AttireImagePreview(imageUrl: referenceImageUrl),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Example of the item that you need to upload.',
|
||||
style: UiTypography.body1b.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the reference item photo as an example.
|
||||
class ReferenceExample extends StatelessWidget {
|
||||
/// Creates a [ReferenceExample].
|
||||
const ReferenceExample({super.key, this.imageUrl});
|
||||
|
||||
/// The URL of the image to display.
|
||||
final String? imageUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Text('Reference Example', style: UiTypography.body2b.textSecondary),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
child: Image.network(
|
||||
imageUrl ?? '',
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, _, _) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../attestation_checkbox.dart';
|
||||
import 'attire_verification_status_card.dart';
|
||||
|
||||
/// Displays the item details, verification status, and attestation checkbox.
|
||||
class InfoSection extends StatelessWidget {
|
||||
/// Creates an [InfoSection].
|
||||
const InfoSection({
|
||||
super.key,
|
||||
this.description,
|
||||
required this.statusText,
|
||||
required this.statusColor,
|
||||
required this.isPending,
|
||||
required this.showCheckbox,
|
||||
required this.isAttested,
|
||||
required this.onAttestationChanged,
|
||||
});
|
||||
|
||||
/// The description of the attire item.
|
||||
final String? description;
|
||||
|
||||
/// The text to display for the verification status.
|
||||
final String statusText;
|
||||
|
||||
/// The color to use for the verification status text.
|
||||
final Color statusColor;
|
||||
|
||||
/// Whether the item is currently pending verification.
|
||||
final bool isPending;
|
||||
|
||||
/// Whether to show the attestation checkbox.
|
||||
final bool showCheckbox;
|
||||
|
||||
/// Whether the user has attested to owning the item.
|
||||
final bool isAttested;
|
||||
|
||||
/// Callback when the attestation status changes.
|
||||
final ValueChanged<bool?> onAttestationChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (description != null)
|
||||
Text(
|
||||
description!,
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
|
||||
// Pending Banner
|
||||
if (isPending) ...<Widget>[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagPending,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Text(
|
||||
'A Manager will Verify This Item',
|
||||
style: UiTypography.body2b.textWarning,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
],
|
||||
|
||||
// Verification info
|
||||
AttireVerificationStatusCard(
|
||||
statusText: statusText,
|
||||
statusColor: statusColor,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
if (showCheckbox) ...<Widget>[
|
||||
AttestationCheckbox(
|
||||
isChecked: isAttested,
|
||||
onChanged: onAttestationChanged,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user