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:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.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 'data/repositories_impl/attire_repository_impl.dart';
|
||||||
import 'domain/repositories/attire_repository.dart';
|
import 'domain/repositories/attire_repository.dart';
|
||||||
import 'domain/usecases/get_attire_options_usecase.dart';
|
import 'domain/usecases/get_attire_options_usecase.dart';
|
||||||
import 'domain/usecases/save_attire_usecase.dart';
|
import 'domain/usecases/save_attire_usecase.dart';
|
||||||
import 'domain/usecases/upload_attire_photo_usecase.dart';
|
import 'domain/usecases/upload_attire_photo_usecase.dart';
|
||||||
import 'presentation/blocs/attire_cubit.dart';
|
|
||||||
import 'presentation/pages/attire_page.dart';
|
import 'presentation/pages/attire_page.dart';
|
||||||
|
|
||||||
class StaffAttireModule extends Module {
|
class StaffAttireModule extends Module {
|
||||||
@@ -19,9 +20,10 @@ class StaffAttireModule extends Module {
|
|||||||
i.addLazySingleton(GetAttireOptionsUseCase.new);
|
i.addLazySingleton(GetAttireOptionsUseCase.new);
|
||||||
i.addLazySingleton(SaveAttireUseCase.new);
|
i.addLazySingleton(SaveAttireUseCase.new);
|
||||||
i.addLazySingleton(UploadAttirePhotoUseCase.new);
|
i.addLazySingleton(UploadAttirePhotoUseCase.new);
|
||||||
|
|
||||||
// BLoC
|
// BLoC
|
||||||
i.addLazySingleton(AttireCubit.new);
|
i.addLazySingleton(AttireCubit.new);
|
||||||
|
i.add(AttireCaptureCubit.new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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 }
|
enum AttireStatus { initial, loading, success, failure, saving, saved }
|
||||||
|
|
||||||
class AttireState extends Equatable {
|
class AttireState extends Equatable {
|
||||||
|
|
||||||
const AttireState({
|
const AttireState({
|
||||||
this.status = AttireStatus.initial,
|
this.status = AttireStatus.initial,
|
||||||
this.options = const <AttireItem>[],
|
this.options = const <AttireItem>[],
|
||||||
this.selectedIds = const <String>[],
|
this.selectedIds = const <String>[],
|
||||||
this.photoUrls = const <String, String>{},
|
this.photoUrls = const <String, String>{},
|
||||||
this.uploadingStatus = const <String, bool>{},
|
|
||||||
this.attestationChecked = false,
|
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
final AttireStatus status;
|
final AttireStatus status;
|
||||||
final List<AttireItem> options;
|
final List<AttireItem> options;
|
||||||
final List<String> selectedIds;
|
final List<String> selectedIds;
|
||||||
final Map<String, String> photoUrls;
|
final Map<String, String> photoUrls;
|
||||||
final Map<String, bool> uploadingStatus;
|
|
||||||
final bool attestationChecked;
|
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
bool get uploading => uploadingStatus.values.any((bool u) => u);
|
|
||||||
|
|
||||||
/// Helper to check if item is mandatory
|
/// Helper to check if item is mandatory
|
||||||
bool isMandatory(String id) {
|
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
|
/// Validation logic
|
||||||
bool get allMandatorySelected {
|
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));
|
return mandatoryIds.every((String id) => selectedIds.contains(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get allMandatoryHavePhotos {
|
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));
|
return mandatoryIds.every((String id) => photoUrls.containsKey(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get canSave => allMandatorySelected && allMandatoryHavePhotos && attestationChecked && !uploading;
|
bool get canSave => allMandatorySelected && allMandatoryHavePhotos;
|
||||||
|
|
||||||
AttireState copyWith({
|
AttireState copyWith({
|
||||||
AttireStatus? status,
|
AttireStatus? status,
|
||||||
List<AttireItem>? options,
|
List<AttireItem>? options,
|
||||||
List<String>? selectedIds,
|
List<String>? selectedIds,
|
||||||
Map<String, String>? photoUrls,
|
Map<String, String>? photoUrls,
|
||||||
Map<String, bool>? uploadingStatus,
|
|
||||||
bool? attestationChecked,
|
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
}) {
|
}) {
|
||||||
return AttireState(
|
return AttireState(
|
||||||
@@ -56,20 +56,16 @@ class AttireState extends Equatable {
|
|||||||
options: options ?? this.options,
|
options: options ?? this.options,
|
||||||
selectedIds: selectedIds ?? this.selectedIds,
|
selectedIds: selectedIds ?? this.selectedIds,
|
||||||
photoUrls: photoUrls ?? this.photoUrls,
|
photoUrls: photoUrls ?? this.photoUrls,
|
||||||
uploadingStatus: uploadingStatus ?? this.uploadingStatus,
|
|
||||||
attestationChecked: attestationChecked ?? this.attestationChecked,
|
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
status,
|
status,
|
||||||
options,
|
options,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
photoUrls,
|
photoUrls,
|
||||||
uploadingStatus,
|
errorMessage,
|
||||||
attestationChecked,
|
];
|
||||||
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:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
|
||||||
import 'package:flutter_modular/flutter_modular.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/attestation_checkbox.dart';
|
||||||
import '../widgets/attire_capture_page/attire_image_preview.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_upload_buttons.dart';
|
||||||
import '../widgets/attire_capture_page/attire_verification_status_card.dart';
|
import '../widgets/attire_capture_page/attire_verification_status_card.dart';
|
||||||
|
|
||||||
class AttireCapturePage extends StatefulWidget {
|
class AttireCapturePage extends StatefulWidget {
|
||||||
const AttireCapturePage({super.key, required this.item});
|
const AttireCapturePage({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
this.initialPhotoUrl,
|
||||||
|
});
|
||||||
|
|
||||||
final AttireItem item;
|
final AttireItem item;
|
||||||
|
final String? initialPhotoUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AttireCapturePage> createState() => _AttireCapturePageState();
|
State<AttireCapturePage> createState() => _AttireCapturePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttireCapturePageState extends State<AttireCapturePage> {
|
class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||||
bool _isAttested = false;
|
|
||||||
|
|
||||||
void _onUpload(BuildContext context) {
|
void _onUpload(BuildContext context) {
|
||||||
if (!_isAttested) {
|
final AttireCaptureCubit cubit = BlocProvider.of<AttireCaptureCubit>(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (!cubit.state.isAttested) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message: 'Please attest that you own this item.',
|
message: 'Please attest that you own this item.',
|
||||||
@@ -35,100 +41,106 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Call the upload via cubit
|
// Call the upload via cubit
|
||||||
final AttireCubit cubit = Modular.get<AttireCubit>();
|
|
||||||
cubit.uploadPhoto(widget.item.id);
|
cubit.uploadPhoto(widget.item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.background,
|
backgroundColor: UiColors.background,
|
||||||
appBar: UiAppBar(title: widget.item.label, showBackButton: true),
|
appBar: UiAppBar(title: widget.item.label, showBackButton: true),
|
||||||
body: BlocConsumer<AttireCubit, AttireState>(
|
body: BlocConsumer<AttireCaptureCubit, AttireCaptureState>(
|
||||||
bloc: cubit,
|
bloc: cubit,
|
||||||
listener: (BuildContext context, AttireState state) {
|
listener: (BuildContext context, AttireCaptureState state) {
|
||||||
if (state.status == AttireStatus.failure) {
|
if (state.status == AttireCaptureStatus.failure) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message: translateErrorKey(state.errorMessage ?? 'Error'),
|
message: translateErrorKey(state.errorMessage ?? 'Error'),
|
||||||
type: UiSnackbarType.error,
|
type: UiSnackbarType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (BuildContext context, AttireState state) {
|
builder: (BuildContext context, AttireCaptureState state) {
|
||||||
final bool isUploading =
|
final bool isUploading =
|
||||||
state.uploadingStatus[widget.item.id] ?? false;
|
state.status == AttireCaptureStatus.uploading;
|
||||||
final bool hasPhoto = state.photoUrls.containsKey(widget.item.id);
|
final bool hasPhoto =
|
||||||
final String statusText = hasPhoto
|
state.photoUrl != null || widget.initialPhotoUrl != null;
|
||||||
? 'Pending Verification'
|
final String statusText = hasPhoto
|
||||||
: 'Not Uploaded';
|
? 'Pending Verification'
|
||||||
final Color statusColor = hasPhoto
|
: 'Not Uploaded';
|
||||||
? UiColors.textWarning
|
final Color statusColor = hasPhoto
|
||||||
: UiColors.textInactive;
|
? UiColors.textWarning
|
||||||
|
: UiColors.textInactive;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Image Preview
|
// Image Preview
|
||||||
// Image Preview
|
AttireImagePreview(imageUrl: widget.item.imageUrl),
|
||||||
AttireImagePreview(imageUrl: widget.item.imageUrl),
|
const SizedBox(height: UiConstants.space6),
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
widget.item.description ?? '',
|
widget.item.description ?? '',
|
||||||
style: UiTypography.body1r.textSecondary,
|
style: UiTypography.body1r.textSecondary,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
// Verification info
|
// Verification info
|
||||||
AttireVerificationStatusCard(
|
AttireVerificationStatusCard(
|
||||||
statusText: statusText,
|
statusText: statusText,
|
||||||
statusColor: statusColor,
|
statusColor: statusColor,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
AttestationCheckbox(
|
AttestationCheckbox(
|
||||||
isChecked: _isAttested,
|
isChecked: state.isAttested,
|
||||||
onChanged: (bool? val) {
|
onChanged: (bool? val) {
|
||||||
setState(() {
|
cubit.toggleAttestation(val ?? false);
|
||||||
_isAttested = val ?? false;
|
},
|
||||||
});
|
),
|
||||||
},
|
const SizedBox(height: UiConstants.space6),
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
|
|
||||||
if (isUploading)
|
if (isUploading)
|
||||||
const Center(child: CircularProgressIndicator())
|
const Center(child: CircularProgressIndicator())
|
||||||
else if (!hasPhoto ||
|
else if (!hasPhoto ||
|
||||||
true) // Show options even if has photo (allows re-upload)
|
true) // Show options even if has photo (allows re-upload)
|
||||||
AttireUploadButtons(onUpload: _onUpload),
|
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 (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_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_domain/krow_domain.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_filter_chips.dart';
|
||||||
import '../widgets/attire_info_card.dart';
|
import '../widgets/attire_info_card.dart';
|
||||||
import '../widgets/attire_item_card.dart';
|
import '../widgets/attire_item_card.dart';
|
||||||
@@ -87,17 +87,25 @@ class _AttirePageState extends State<AttirePage> {
|
|||||||
),
|
),
|
||||||
child: AttireItemCard(
|
child: AttireItemCard(
|
||||||
item: item,
|
item: item,
|
||||||
isUploading:
|
isUploading: false,
|
||||||
state.uploadingStatus[item.id] ?? false,
|
|
||||||
uploadedPhotoUrl: state.photoUrls[item.id],
|
uploadedPhotoUrl: state.photoUrls[item.id],
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
Navigator.push<void>(
|
final String? resultUrl =
|
||||||
context,
|
await Navigator.push<String?>(
|
||||||
MaterialPageRoute<void>(
|
context,
|
||||||
builder: (BuildContext ctx) =>
|
MaterialPageRoute<String?>(
|
||||||
AttireCapturePage(item: item),
|
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