feat: Extract attire photo capture logic into AttireCaptureCubit and reorganize existing attire BLoC into a dedicated subdirectory.
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire/attire_cubit.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_cubit.dart';
|
||||
|
||||
import 'data/repositories_impl/attire_repository_impl.dart';
|
||||
import 'domain/repositories/attire_repository.dart';
|
||||
import 'domain/usecases/get_attire_options_usecase.dart';
|
||||
import 'domain/usecases/save_attire_usecase.dart';
|
||||
import 'domain/usecases/upload_attire_photo_usecase.dart';
|
||||
import 'presentation/blocs/attire_cubit.dart';
|
||||
import 'presentation/pages/attire_page.dart';
|
||||
|
||||
class StaffAttireModule extends Module {
|
||||
@@ -19,9 +20,10 @@ class StaffAttireModule extends Module {
|
||||
i.addLazySingleton(GetAttireOptionsUseCase.new);
|
||||
i.addLazySingleton(SaveAttireUseCase.new);
|
||||
i.addLazySingleton(UploadAttirePhotoUseCase.new);
|
||||
|
||||
|
||||
// BLoC
|
||||
i.addLazySingleton(AttireCubit.new);
|
||||
i.add(AttireCaptureCubit.new);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_attire/src/domain/arguments/save_attire_arguments.dart';
|
||||
import 'package:staff_attire/src/domain/usecases/get_attire_options_usecase.dart';
|
||||
import 'package:staff_attire/src/domain/usecases/save_attire_usecase.dart';
|
||||
|
||||
import 'attire_state.dart';
|
||||
|
||||
class AttireCubit extends Cubit<AttireState>
|
||||
with BlocErrorHandler<AttireState> {
|
||||
AttireCubit(this._getAttireOptionsUseCase, this._saveAttireUseCase)
|
||||
: super(const AttireState()) {
|
||||
loadOptions();
|
||||
}
|
||||
final GetAttireOptionsUseCase _getAttireOptionsUseCase;
|
||||
final SaveAttireUseCase _saveAttireUseCase;
|
||||
|
||||
Future<void> loadOptions() async {
|
||||
emit(state.copyWith(status: AttireStatus.loading));
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final List<AttireItem> options = await _getAttireOptionsUseCase();
|
||||
|
||||
// Auto-select mandatory items initially as per prototype
|
||||
final List<String> mandatoryIds = options
|
||||
.where((AttireItem e) => e.isMandatory)
|
||||
.map((AttireItem e) => e.id)
|
||||
.toList();
|
||||
|
||||
final List<String> initialSelection = List<String>.from(
|
||||
state.selectedIds,
|
||||
);
|
||||
for (final String id in mandatoryIds) {
|
||||
if (!initialSelection.contains(id)) {
|
||||
initialSelection.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: AttireStatus.success,
|
||||
options: options,
|
||||
selectedIds: initialSelection,
|
||||
),
|
||||
);
|
||||
},
|
||||
onError: (String errorKey) =>
|
||||
state.copyWith(status: AttireStatus.failure, errorMessage: errorKey),
|
||||
);
|
||||
}
|
||||
|
||||
void toggleSelection(String id) {
|
||||
// Prevent unselecting mandatory items
|
||||
if (state.isMandatory(id)) return;
|
||||
|
||||
final List<String> currentSelection = List<String>.from(state.selectedIds);
|
||||
if (currentSelection.contains(id)) {
|
||||
currentSelection.remove(id);
|
||||
} else {
|
||||
currentSelection.add(id);
|
||||
}
|
||||
emit(state.copyWith(selectedIds: currentSelection));
|
||||
}
|
||||
|
||||
void syncCapturedPhoto(String itemId, String url) {
|
||||
final Map<String, String> currentPhotos = Map<String, String>.from(
|
||||
state.photoUrls,
|
||||
);
|
||||
currentPhotos[itemId] = url;
|
||||
|
||||
// Auto-select item on upload success if not selected
|
||||
final List<String> currentSelection = List<String>.from(state.selectedIds);
|
||||
if (!currentSelection.contains(itemId)) {
|
||||
currentSelection.add(itemId);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(photoUrls: currentPhotos, selectedIds: currentSelection),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
if (!state.canSave) return;
|
||||
|
||||
emit(state.copyWith(status: AttireStatus.saving));
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
await _saveAttireUseCase(
|
||||
SaveAttireArguments(
|
||||
selectedItemIds: state.selectedIds,
|
||||
photoUrls: state.photoUrls,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(status: AttireStatus.saved));
|
||||
},
|
||||
onError: (String errorKey) =>
|
||||
state.copyWith(status: AttireStatus.failure, errorMessage: errorKey),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,51 +4,51 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
enum AttireStatus { initial, loading, success, failure, saving, saved }
|
||||
|
||||
class AttireState extends Equatable {
|
||||
|
||||
const AttireState({
|
||||
this.status = AttireStatus.initial,
|
||||
this.options = const <AttireItem>[],
|
||||
this.selectedIds = const <String>[],
|
||||
this.photoUrls = const <String, String>{},
|
||||
this.uploadingStatus = const <String, bool>{},
|
||||
this.attestationChecked = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
final AttireStatus status;
|
||||
final List<AttireItem> options;
|
||||
final List<String> selectedIds;
|
||||
final Map<String, String> photoUrls;
|
||||
final Map<String, bool> uploadingStatus;
|
||||
final bool attestationChecked;
|
||||
final String? errorMessage;
|
||||
|
||||
bool get uploading => uploadingStatus.values.any((bool u) => u);
|
||||
|
||||
/// Helper to check if item is mandatory
|
||||
bool isMandatory(String id) {
|
||||
return options.firstWhere((AttireItem e) => e.id == id, orElse: () => const AttireItem(id: '', label: '')).isMandatory;
|
||||
return options
|
||||
.firstWhere(
|
||||
(AttireItem e) => e.id == id,
|
||||
orElse: () => const AttireItem(id: '', label: ''),
|
||||
)
|
||||
.isMandatory;
|
||||
}
|
||||
|
||||
/// Validation logic
|
||||
bool get allMandatorySelected {
|
||||
final Iterable<String> mandatoryIds = options.where((AttireItem e) => e.isMandatory).map((AttireItem e) => e.id);
|
||||
final Iterable<String> mandatoryIds = options
|
||||
.where((AttireItem e) => e.isMandatory)
|
||||
.map((AttireItem e) => e.id);
|
||||
return mandatoryIds.every((String id) => selectedIds.contains(id));
|
||||
}
|
||||
|
||||
bool get allMandatoryHavePhotos {
|
||||
final Iterable<String> mandatoryIds = options.where((AttireItem e) => e.isMandatory).map((AttireItem e) => e.id);
|
||||
final Iterable<String> mandatoryIds = options
|
||||
.where((AttireItem e) => e.isMandatory)
|
||||
.map((AttireItem e) => e.id);
|
||||
return mandatoryIds.every((String id) => photoUrls.containsKey(id));
|
||||
}
|
||||
|
||||
bool get canSave => allMandatorySelected && allMandatoryHavePhotos && attestationChecked && !uploading;
|
||||
bool get canSave => allMandatorySelected && allMandatoryHavePhotos;
|
||||
|
||||
AttireState copyWith({
|
||||
AttireStatus? status,
|
||||
List<AttireItem>? options,
|
||||
List<String>? selectedIds,
|
||||
Map<String, String>? photoUrls,
|
||||
Map<String, bool>? uploadingStatus,
|
||||
bool? attestationChecked,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return AttireState(
|
||||
@@ -56,20 +56,16 @@ class AttireState extends Equatable {
|
||||
options: options ?? this.options,
|
||||
selectedIds: selectedIds ?? this.selectedIds,
|
||||
photoUrls: photoUrls ?? this.photoUrls,
|
||||
uploadingStatus: uploadingStatus ?? this.uploadingStatus,
|
||||
attestationChecked: attestationChecked ?? this.attestationChecked,
|
||||
errorMessage: errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
status,
|
||||
options,
|
||||
selectedIds,
|
||||
photoUrls,
|
||||
uploadingStatus,
|
||||
attestationChecked,
|
||||
errorMessage
|
||||
];
|
||||
status,
|
||||
options,
|
||||
selectedIds,
|
||||
photoUrls,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:staff_attire/src/domain/arguments/upload_attire_photo_arguments.dart';
|
||||
import 'package:staff_attire/src/domain/usecases/upload_attire_photo_usecase.dart';
|
||||
|
||||
import 'attire_capture_state.dart';
|
||||
|
||||
class AttireCaptureCubit extends Cubit<AttireCaptureState>
|
||||
with BlocErrorHandler<AttireCaptureState> {
|
||||
AttireCaptureCubit(this._uploadAttirePhotoUseCase)
|
||||
: super(const AttireCaptureState());
|
||||
|
||||
final UploadAttirePhotoUseCase _uploadAttirePhotoUseCase;
|
||||
|
||||
void toggleAttestation(bool value) {
|
||||
emit(state.copyWith(isAttested: value));
|
||||
}
|
||||
|
||||
Future<void> uploadPhoto(String itemId) async {
|
||||
emit(state.copyWith(status: AttireCaptureStatus.uploading));
|
||||
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final String url = await _uploadAttirePhotoUseCase(
|
||||
UploadAttirePhotoArguments(itemId: itemId),
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(status: AttireCaptureStatus.success, photoUrl: url),
|
||||
);
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: AttireCaptureStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
enum AttireCaptureStatus { initial, uploading, success, failure }
|
||||
|
||||
class AttireCaptureState extends Equatable {
|
||||
const AttireCaptureState({
|
||||
this.status = AttireCaptureStatus.initial,
|
||||
this.isAttested = false,
|
||||
this.photoUrl,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final AttireCaptureStatus status;
|
||||
final bool isAttested;
|
||||
final String? photoUrl;
|
||||
final String? errorMessage;
|
||||
|
||||
AttireCaptureState copyWith({
|
||||
AttireCaptureStatus? status,
|
||||
bool? isAttested,
|
||||
String? photoUrl,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return AttireCaptureState(
|
||||
status: status ?? this.status,
|
||||
isAttested: isAttested ?? this.isAttested,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
errorMessage: errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
status,
|
||||
isAttested,
|
||||
photoUrl,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../domain/arguments/save_attire_arguments.dart';
|
||||
import '../../domain/arguments/upload_attire_photo_arguments.dart';
|
||||
import '../../domain/usecases/get_attire_options_usecase.dart';
|
||||
import '../../domain/usecases/save_attire_usecase.dart';
|
||||
import '../../domain/usecases/upload_attire_photo_usecase.dart';
|
||||
import 'attire_state.dart';
|
||||
|
||||
class AttireCubit extends Cubit<AttireState>
|
||||
with BlocErrorHandler<AttireState> {
|
||||
|
||||
AttireCubit(
|
||||
this._getAttireOptionsUseCase,
|
||||
this._saveAttireUseCase,
|
||||
this._uploadAttirePhotoUseCase,
|
||||
) : super(const AttireState()) {
|
||||
loadOptions();
|
||||
}
|
||||
final GetAttireOptionsUseCase _getAttireOptionsUseCase;
|
||||
final SaveAttireUseCase _saveAttireUseCase;
|
||||
final UploadAttirePhotoUseCase _uploadAttirePhotoUseCase;
|
||||
|
||||
Future<void> loadOptions() async {
|
||||
emit(state.copyWith(status: AttireStatus.loading));
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final List<AttireItem> options = await _getAttireOptionsUseCase();
|
||||
|
||||
// Auto-select mandatory items initially as per prototype
|
||||
final List<String> mandatoryIds =
|
||||
options
|
||||
.where((AttireItem e) => e.isMandatory)
|
||||
.map((AttireItem e) => e.id)
|
||||
.toList();
|
||||
|
||||
final List<String> initialSelection = List<String>.from(
|
||||
state.selectedIds,
|
||||
);
|
||||
for (final String id in mandatoryIds) {
|
||||
if (!initialSelection.contains(id)) {
|
||||
initialSelection.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: AttireStatus.success,
|
||||
options: options,
|
||||
selectedIds: initialSelection,
|
||||
),
|
||||
);
|
||||
},
|
||||
onError:
|
||||
(String errorKey) => state.copyWith(
|
||||
status: AttireStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void toggleSelection(String id) {
|
||||
// Prevent unselecting mandatory items
|
||||
if (state.isMandatory(id)) return;
|
||||
|
||||
final List<String> currentSelection = List<String>.from(state.selectedIds);
|
||||
if (currentSelection.contains(id)) {
|
||||
currentSelection.remove(id);
|
||||
} else {
|
||||
currentSelection.add(id);
|
||||
}
|
||||
emit(state.copyWith(selectedIds: currentSelection));
|
||||
}
|
||||
|
||||
void toggleAttestation(bool value) {
|
||||
emit(state.copyWith(attestationChecked: value));
|
||||
}
|
||||
|
||||
Future<void> uploadPhoto(String itemId) async {
|
||||
final Map<String, bool> currentUploading = Map<String, bool>.from(
|
||||
state.uploadingStatus,
|
||||
);
|
||||
currentUploading[itemId] = true;
|
||||
emit(state.copyWith(uploadingStatus: currentUploading));
|
||||
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final String url = await _uploadAttirePhotoUseCase(
|
||||
UploadAttirePhotoArguments(itemId: itemId),
|
||||
);
|
||||
|
||||
final Map<String, String> currentPhotos = Map<String, String>.from(
|
||||
state.photoUrls,
|
||||
);
|
||||
currentPhotos[itemId] = url;
|
||||
|
||||
// Auto-select item on upload success if not selected
|
||||
final List<String> currentSelection = List<String>.from(
|
||||
state.selectedIds,
|
||||
);
|
||||
if (!currentSelection.contains(itemId)) {
|
||||
currentSelection.add(itemId);
|
||||
}
|
||||
|
||||
final Map<String, bool> updatedUploading = Map<String, bool>.from(
|
||||
state.uploadingStatus,
|
||||
);
|
||||
updatedUploading[itemId] = false;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
uploadingStatus: updatedUploading,
|
||||
photoUrls: currentPhotos,
|
||||
selectedIds: currentSelection,
|
||||
),
|
||||
);
|
||||
},
|
||||
onError: (String errorKey) {
|
||||
final Map<String, bool> updatedUploading = Map<String, bool>.from(
|
||||
state.uploadingStatus,
|
||||
);
|
||||
updatedUploading[itemId] = false;
|
||||
// Could handle error specifically via snackbar event
|
||||
// For now, attaching the error message but keeping state generally usable
|
||||
return state.copyWith(
|
||||
uploadingStatus: updatedUploading,
|
||||
errorMessage: errorKey,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
if (!state.canSave) return;
|
||||
|
||||
emit(state.copyWith(status: AttireStatus.saving));
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
await _saveAttireUseCase(
|
||||
SaveAttireArguments(
|
||||
selectedItemIds: state.selectedIds,
|
||||
photoUrls: state.photoUrls,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(status: AttireStatus.saved));
|
||||
},
|
||||
onError:
|
||||
(String errorKey) => state.copyWith(
|
||||
status: AttireStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
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 '../blocs/attire_cubit.dart';
|
||||
import '../blocs/attire_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';
|
||||
|
||||
class AttireCapturePage extends StatefulWidget {
|
||||
const AttireCapturePage({super.key, required this.item});
|
||||
const AttireCapturePage({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.initialPhotoUrl,
|
||||
});
|
||||
|
||||
final AttireItem item;
|
||||
final String? initialPhotoUrl;
|
||||
|
||||
@override
|
||||
State<AttireCapturePage> createState() => _AttireCapturePageState();
|
||||
}
|
||||
|
||||
class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
bool _isAttested = false;
|
||||
|
||||
void _onUpload(BuildContext context) {
|
||||
if (!_isAttested) {
|
||||
final AttireCaptureCubit cubit = BlocProvider.of<AttireCaptureCubit>(
|
||||
context,
|
||||
);
|
||||
if (!cubit.state.isAttested) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Please attest that you own this item.',
|
||||
@@ -35,100 +41,106 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
return;
|
||||
}
|
||||
// Call the upload via cubit
|
||||
final AttireCubit cubit = Modular.get<AttireCubit>();
|
||||
cubit.uploadPhoto(widget.item.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AttireCubit cubit = Modular.get<AttireCubit>();
|
||||
return BlocProvider<AttireCaptureCubit>(
|
||||
create: (_) => Modular.get<AttireCaptureCubit>(),
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final AttireCaptureCubit cubit = BlocProvider.of<AttireCaptureCubit>(
|
||||
context,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
appBar: UiAppBar(title: widget.item.label, showBackButton: true),
|
||||
body: BlocConsumer<AttireCubit, AttireState>(
|
||||
bloc: cubit,
|
||||
listener: (BuildContext context, AttireState state) {
|
||||
if (state.status == AttireStatus.failure) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage ?? 'Error'),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, AttireState state) {
|
||||
final bool isUploading =
|
||||
state.uploadingStatus[widget.item.id] ?? false;
|
||||
final bool hasPhoto = state.photoUrls.containsKey(widget.item.id);
|
||||
final String statusText = hasPhoto
|
||||
? 'Pending Verification'
|
||||
: 'Not Uploaded';
|
||||
final Color statusColor = hasPhoto
|
||||
? UiColors.textWarning
|
||||
: UiColors.textInactive;
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
appBar: UiAppBar(title: widget.item.label, showBackButton: true),
|
||||
body: BlocConsumer<AttireCaptureCubit, AttireCaptureState>(
|
||||
bloc: cubit,
|
||||
listener: (BuildContext context, AttireCaptureState state) {
|
||||
if (state.status == AttireCaptureStatus.failure) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage ?? 'Error'),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, AttireCaptureState state) {
|
||||
final bool isUploading =
|
||||
state.status == AttireCaptureStatus.uploading;
|
||||
final bool hasPhoto =
|
||||
state.photoUrl != null || widget.initialPhotoUrl != null;
|
||||
final String statusText = hasPhoto
|
||||
? 'Pending Verification'
|
||||
: 'Not Uploaded';
|
||||
final Color statusColor = hasPhoto
|
||||
? UiColors.textWarning
|
||||
: UiColors.textInactive;
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Image Preview
|
||||
// Image Preview
|
||||
AttireImagePreview(imageUrl: widget.item.imageUrl),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Image Preview
|
||||
AttireImagePreview(imageUrl: widget.item.imageUrl),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
Text(
|
||||
widget.item.description ?? '',
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
widget.item.description ?? '',
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Verification info
|
||||
AttireVerificationStatusCard(
|
||||
statusText: statusText,
|
||||
statusColor: statusColor,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Verification info
|
||||
AttireVerificationStatusCard(
|
||||
statusText: statusText,
|
||||
statusColor: statusColor,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
AttestationCheckbox(
|
||||
isChecked: _isAttested,
|
||||
onChanged: (bool? val) {
|
||||
setState(() {
|
||||
_isAttested = val ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
AttestationCheckbox(
|
||||
isChecked: state.isAttested,
|
||||
onChanged: (bool? val) {
|
||||
cubit.toggleAttestation(val ?? false);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
if (isUploading)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (!hasPhoto ||
|
||||
true) // Show options even if has photo (allows re-upload)
|
||||
AttireUploadButtons(onUpload: _onUpload),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasPhoto)
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: UiButton.primary(
|
||||
text: 'Submit Image',
|
||||
onPressed: () {
|
||||
Modular.to.pop();
|
||||
},
|
||||
if (isUploading)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (!hasPhoto ||
|
||||
true) // Show options even if has photo (allows re-upload)
|
||||
AttireUploadButtons(onUpload: _onUpload),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (hasPhoto)
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: UiButton.primary(
|
||||
text: 'Submit Image',
|
||||
onPressed: () {
|
||||
Modular.to.pop(state.photoUrl);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -4,9 +4,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire/attire_cubit.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire/attire_state.dart';
|
||||
|
||||
import '../blocs/attire_cubit.dart';
|
||||
import '../blocs/attire_state.dart';
|
||||
import '../widgets/attire_filter_chips.dart';
|
||||
import '../widgets/attire_info_card.dart';
|
||||
import '../widgets/attire_item_card.dart';
|
||||
@@ -87,17 +87,25 @@ class _AttirePageState extends State<AttirePage> {
|
||||
),
|
||||
child: AttireItemCard(
|
||||
item: item,
|
||||
isUploading:
|
||||
state.uploadingStatus[item.id] ?? false,
|
||||
isUploading: false,
|
||||
uploadedPhotoUrl: state.photoUrls[item.id],
|
||||
onTap: () {
|
||||
Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext ctx) =>
|
||||
AttireCapturePage(item: item),
|
||||
),
|
||||
);
|
||||
onTap: () async {
|
||||
final String? resultUrl =
|
||||
await Navigator.push<String?>(
|
||||
context,
|
||||
MaterialPageRoute<String?>(
|
||||
builder: (BuildContext ctx) =>
|
||||
AttireCapturePage(
|
||||
item: item,
|
||||
initialPhotoUrl:
|
||||
state.photoUrls[item.id],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (resultUrl != null && mounted) {
|
||||
cubit.syncCapturedPhoto(item.id, resultUrl);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user