feat: add staff attire management feature
- Introduced localization for staff attire in English and Spanish. - Created AttireItem entity to represent attire items. - Implemented StaffAttireModule with repository, use cases, and BLoC for state management. - Developed UI components including AttirePage, AttireGrid, AttireInfoCard, AttestationCheckbox, and AttireBottomBar. - Added navigation for attire management and integrated with existing staff profile flow. - Implemented functionality for selecting attire items, uploading photos, and saving selections with validation.
This commit is contained in:
@@ -640,5 +640,28 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm": "Remove"
|
"confirm": "Remove"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"staff_profile_attire": {
|
||||||
|
"title": "Attire",
|
||||||
|
"info_card": {
|
||||||
|
"title": "Your Wardrobe",
|
||||||
|
"description": "Select the attire items you own. This helps us match you with shifts that fit your wardrobe."
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"required": "REQUIRED",
|
||||||
|
"add_photo": "Add Photo",
|
||||||
|
"added": "Added",
|
||||||
|
"pending": "⏳ Pending verification"
|
||||||
|
},
|
||||||
|
"attestation": "I certify that I own these items and will wear them to my shifts. I understand that items are pending manager verification at my first shift.",
|
||||||
|
"actions": {
|
||||||
|
"save": "Save Attire"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"select_required": "✓ Select all required items",
|
||||||
|
"upload_required": "✓ Upload photos of required items",
|
||||||
|
"accept_attestation": "✓ Accept attestation"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -639,5 +639,27 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm": "Remove"
|
"confirm": "Remove"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"staff_profile_attire": {
|
||||||
|
"title": "Vestimenta",
|
||||||
|
"info_card": {
|
||||||
|
"title": "Tu Vestuario",
|
||||||
|
"description": "Selecciona los artículos de vestimenta que posees. Esto nos ayuda a asignarte turnos que se ajusten a tu vestuario."
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"required": "REQUERIDO",
|
||||||
|
"add_photo": "Añadir Foto",
|
||||||
|
"added": "Añadido",
|
||||||
|
"pending": "⏳ Verificación pendiente"
|
||||||
|
},
|
||||||
|
"attestation": "Certifico que poseo estos artículos y los usaré en mis turnos. Entiendo que los artículos están pendientes de verificación por el gerente en mi primer turno.",
|
||||||
|
"actions": {
|
||||||
|
"save": "Guardar Vestimenta"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"select_required": "✓ Seleccionar todos los artículos requeridos",
|
||||||
|
"upload_required": "✓ Subir fotos de artículos requeridos",
|
||||||
|
"accept_attestation": "✓ Aceptar certificación"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export 'src/entities/financial/staff_payment.dart';
|
|||||||
|
|
||||||
// Profile
|
// Profile
|
||||||
export 'src/entities/profile/staff_document.dart';
|
export 'src/entities/profile/staff_document.dart';
|
||||||
|
export 'src/entities/profile/attire_item.dart';
|
||||||
|
|
||||||
// Ratings & Penalties
|
// Ratings & Penalties
|
||||||
export 'src/entities/ratings/staff_rating.dart';
|
export 'src/entities/ratings/staff_rating.dart';
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Represents an attire item that a staff member might need or possess.
|
||||||
|
///
|
||||||
|
/// Attire items are specific clothing or equipment required for jobs.
|
||||||
|
class AttireItem extends Equatable {
|
||||||
|
/// Unique identifier of the attire item.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Display name of the item.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// Name of the icon to display (mapped in UI).
|
||||||
|
final String? iconName;
|
||||||
|
|
||||||
|
/// URL of the reference image.
|
||||||
|
final String? imageUrl;
|
||||||
|
|
||||||
|
/// Whether this item is mandatory for onboarding.
|
||||||
|
final bool isMandatory;
|
||||||
|
|
||||||
|
/// Creates an [AttireItem].
|
||||||
|
const AttireItem({
|
||||||
|
required this.id,
|
||||||
|
required this.label,
|
||||||
|
this.iconName,
|
||||||
|
this.imageUrl,
|
||||||
|
this.isMandatory = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[id, label, iconName, imageUrl, isMandatory];
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ extension ProfileNavigator on IModularNavigator {
|
|||||||
|
|
||||||
/// Navigates to the attire page.
|
/// Navigates to the attire page.
|
||||||
void pushAttire() {
|
void pushAttire() {
|
||||||
pushNamed('../onboarding/attire');
|
pushNamed('../attire');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the documents page.
|
/// Navigates to the documents page.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
include: ../../../../../../analysis_options.yaml
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_data_connect/krow_data_connect.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 {
|
||||||
|
@override
|
||||||
|
void binds(Injector i) {
|
||||||
|
// Repository
|
||||||
|
i.addLazySingleton<AttireRepository>(
|
||||||
|
() => AttireRepositoryImpl(ExampleConnector.instance),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use Cases
|
||||||
|
i.addLazySingleton(GetAttireOptionsUseCase.new);
|
||||||
|
i.addLazySingleton(SaveAttireUseCase.new);
|
||||||
|
i.addLazySingleton(UploadAttirePhotoUseCase.new);
|
||||||
|
|
||||||
|
// BLoC
|
||||||
|
i.addLazySingleton(AttireCubit.new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void routes(RouteManager r) {
|
||||||
|
r.child('/', child: (_) => const AttirePage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||||
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import '../../domain/repositories/attire_repository.dart';
|
||||||
|
|
||||||
|
/// Implementation of [AttireRepository].
|
||||||
|
///
|
||||||
|
/// Delegates data access to [ExampleConnector] from `data_connect`.
|
||||||
|
class AttireRepositoryImpl implements AttireRepository {
|
||||||
|
/// The Data Connect connector instance.
|
||||||
|
final ExampleConnector _connector;
|
||||||
|
|
||||||
|
/// Creates an [AttireRepositoryImpl].
|
||||||
|
AttireRepositoryImpl(this._connector);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<AttireItem>> getAttireOptions() async {
|
||||||
|
final QueryResult<ListAttireOptionsData, void> result = await _connector.listAttireOptions().execute();
|
||||||
|
return result.data.attireOptions.map((ListAttireOptionsAttireOptions e) => AttireItem(
|
||||||
|
id: e.itemId,
|
||||||
|
label: e.label,
|
||||||
|
iconName: e.icon,
|
||||||
|
imageUrl: e.imageUrl,
|
||||||
|
isMandatory: e.isMandatory ?? false,
|
||||||
|
)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveAttire({
|
||||||
|
required List<String> selectedItemIds,
|
||||||
|
required Map<String, String> photoUrls,
|
||||||
|
}) async {
|
||||||
|
// TODO: Connect to actual backend mutation when available.
|
||||||
|
// For now, simulate network delay as per prototype behavior.
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> uploadPhoto(String itemId) async {
|
||||||
|
// TODO: Connect to actual storage service/mutation when available.
|
||||||
|
// For now, simulate upload delay and return mock URL.
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
|
return 'mock_url_for_$itemId';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
/// Arguments for saving staff attire selections.
|
||||||
|
class SaveAttireArguments extends UseCaseArgument {
|
||||||
|
/// List of selected attire item IDs.
|
||||||
|
final List<String> selectedItemIds;
|
||||||
|
|
||||||
|
/// Map of item IDs to uploaded photo URLs.
|
||||||
|
final Map<String, String> photoUrls;
|
||||||
|
|
||||||
|
/// Creates a [SaveAttireArguments].
|
||||||
|
const SaveAttireArguments({
|
||||||
|
required this.selectedItemIds,
|
||||||
|
required this.photoUrls,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[selectedItemIds, photoUrls];
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
/// Arguments for uploading an attire photo.
|
||||||
|
class UploadAttirePhotoArguments extends UseCaseArgument {
|
||||||
|
/// The ID of the attire item being uploaded.
|
||||||
|
final String itemId;
|
||||||
|
// Note: typically we'd pass a File or path here too, but the prototype likely picks it internally or mocking it.
|
||||||
|
// The current logic takes "itemId" and returns a mock URL.
|
||||||
|
// We'll stick to that signature for now to "preserve behavior".
|
||||||
|
|
||||||
|
/// Creates a [UploadAttirePhotoArguments].
|
||||||
|
const UploadAttirePhotoArguments({required this.itemId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[itemId];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
abstract interface class AttireRepository {
|
||||||
|
/// Fetches the list of available attire options.
|
||||||
|
Future<List<AttireItem>> getAttireOptions();
|
||||||
|
|
||||||
|
/// Simulates uploading a photo for a specific attire item.
|
||||||
|
Future<String> uploadPhoto(String itemId);
|
||||||
|
|
||||||
|
/// Saves the user's attire selection and attestations.
|
||||||
|
Future<void> saveAttire({
|
||||||
|
required List<String> selectedItemIds,
|
||||||
|
required Map<String, String> photoUrls,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import '../repositories/attire_repository.dart';
|
||||||
|
|
||||||
|
/// Use case to fetch available attire options.
|
||||||
|
class GetAttireOptionsUseCase extends NoInputUseCase<List<AttireItem>> {
|
||||||
|
final AttireRepository _repository;
|
||||||
|
|
||||||
|
/// Creates a [GetAttireOptionsUseCase].
|
||||||
|
GetAttireOptionsUseCase(this._repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<AttireItem>> call() {
|
||||||
|
return _repository.getAttireOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
import '../arguments/save_attire_arguments.dart';
|
||||||
|
import '../repositories/attire_repository.dart';
|
||||||
|
|
||||||
|
/// Use case to save user's attire selections.
|
||||||
|
class SaveAttireUseCase extends UseCase<SaveAttireArguments, void> {
|
||||||
|
final AttireRepository _repository;
|
||||||
|
|
||||||
|
/// Creates a [SaveAttireUseCase].
|
||||||
|
SaveAttireUseCase(this._repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> call(SaveAttireArguments arguments) {
|
||||||
|
return _repository.saveAttire(
|
||||||
|
selectedItemIds: arguments.selectedItemIds,
|
||||||
|
photoUrls: arguments.photoUrls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../arguments/upload_attire_photo_arguments.dart';
|
||||||
|
import '../repositories/attire_repository.dart';
|
||||||
|
|
||||||
|
/// Use case to upload a photo for an attire item.
|
||||||
|
class UploadAttirePhotoUseCase extends UseCase<UploadAttirePhotoArguments, String> {
|
||||||
|
final AttireRepository _repository;
|
||||||
|
|
||||||
|
/// Creates a [UploadAttirePhotoUseCase].
|
||||||
|
UploadAttirePhotoUseCase(this._repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> call(UploadAttirePhotoArguments arguments) {
|
||||||
|
return _repository.uploadPhoto(arguments.itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.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> {
|
||||||
|
final GetAttireOptionsUseCase _getAttireOptionsUseCase;
|
||||||
|
final SaveAttireUseCase _saveAttireUseCase;
|
||||||
|
final UploadAttirePhotoUseCase _uploadAttirePhotoUseCase;
|
||||||
|
|
||||||
|
AttireCubit(
|
||||||
|
this._getAttireOptionsUseCase,
|
||||||
|
this._saveAttireUseCase,
|
||||||
|
this._uploadAttirePhotoUseCase,
|
||||||
|
) : super(const AttireState()) {
|
||||||
|
loadOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadOptions() async {
|
||||||
|
emit(state.copyWith(status: AttireStatus.loading));
|
||||||
|
try {
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(status: AttireStatus.failure, errorMessage: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUploading[itemId] = false;
|
||||||
|
emit(state.copyWith(
|
||||||
|
uploadingStatus: currentUploading,
|
||||||
|
photoUrls: currentPhotos,
|
||||||
|
selectedIds: currentSelection,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
currentUploading[itemId] = false;
|
||||||
|
emit(state.copyWith(
|
||||||
|
uploadingStatus: currentUploading,
|
||||||
|
// Could handle error specifically via snackbar event
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> save() async {
|
||||||
|
if (!state.canSave) return;
|
||||||
|
|
||||||
|
emit(state.copyWith(status: AttireStatus.saving));
|
||||||
|
try {
|
||||||
|
await _saveAttireUseCase(SaveAttireArguments(
|
||||||
|
selectedItemIds: state.selectedIds,
|
||||||
|
photoUrls: state.photoUrls,
|
||||||
|
));
|
||||||
|
emit(state.copyWith(status: AttireStatus.saved));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(status: AttireStatus.failure, errorMessage: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
enum AttireStatus { initial, loading, success, failure, saving, saved }
|
||||||
|
|
||||||
|
class AttireState extends Equatable {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validation logic
|
||||||
|
bool get allMandatorySelected {
|
||||||
|
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);
|
||||||
|
return mandatoryIds.every((String id) => photoUrls.containsKey(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get canSave => allMandatorySelected && allMandatoryHavePhotos && attestationChecked && !uploading;
|
||||||
|
|
||||||
|
AttireState copyWith({
|
||||||
|
AttireStatus? status,
|
||||||
|
List<AttireItem>? options,
|
||||||
|
List<String>? selectedIds,
|
||||||
|
Map<String, String>? photoUrls,
|
||||||
|
Map<String, bool>? uploadingStatus,
|
||||||
|
bool? attestationChecked,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return AttireState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
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
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
|
/// Extension on [IModularNavigator] to provide strongly-typed navigation
|
||||||
|
/// for the staff attire feature.
|
||||||
|
extension AttireNavigator on IModularNavigator {
|
||||||
|
/// Navigates back.
|
||||||
|
void popAttire() {
|
||||||
|
pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
|
import '../blocs/attire_cubit.dart';
|
||||||
|
import '../blocs/attire_state.dart';
|
||||||
|
import '../widgets/attestation_checkbox.dart';
|
||||||
|
import '../widgets/attire_bottom_bar.dart';
|
||||||
|
import '../widgets/attire_grid.dart';
|
||||||
|
import '../widgets/attire_info_card.dart';
|
||||||
|
|
||||||
|
class AttirePage extends StatelessWidget {
|
||||||
|
const AttirePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Note: t.staff_profile_attire is available via re-export of core_localization
|
||||||
|
final AttireCubit cubit = Modular.get<AttireCubit>();
|
||||||
|
|
||||||
|
return BlocProvider<AttireCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: UiColors.background, // FAFBFC
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: UiColors.white,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
|
||||||
|
onPressed: () => Modular.to.pop(),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
t.staff_profile_attire.title,
|
||||||
|
style: UiTypography.headline3m.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1.0),
|
||||||
|
child: Container(color: UiColors.border, height: 1.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: BlocConsumer<AttireCubit, AttireState>(
|
||||||
|
listener: (BuildContext context, AttireState state) {
|
||||||
|
if (state.status == AttireStatus.failure) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(state.errorMessage ?? 'Error')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state.status == AttireStatus.saved) {
|
||||||
|
Modular.to.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (BuildContext context, AttireState state) {
|
||||||
|
if (state.status == AttireStatus.loading && state.options.isEmpty) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const AttireInfoCard(),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
AttireGrid(
|
||||||
|
items: state.options,
|
||||||
|
selectedIds: state.selectedIds,
|
||||||
|
photoUrls: state.photoUrls,
|
||||||
|
uploadingStatus: state.uploadingStatus,
|
||||||
|
onToggle: cubit.toggleSelection,
|
||||||
|
onUpload: cubit.uploadPhoto,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
AttestationCheckbox(
|
||||||
|
isChecked: state.attestationChecked,
|
||||||
|
onChanged: (bool? val) => cubit.toggleAttestation(val ?? false),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AttireBottomBar(
|
||||||
|
canSave: state.canSave,
|
||||||
|
allMandatorySelected: state.allMandatorySelected,
|
||||||
|
allMandatoryHavePhotos: state.allMandatoryHavePhotos,
|
||||||
|
attestationChecked: state.attestationChecked,
|
||||||
|
onSave: cubit.save,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
|
class AttestationCheckbox extends StatelessWidget {
|
||||||
|
final bool isChecked;
|
||||||
|
final ValueChanged<bool?> onChanged;
|
||||||
|
|
||||||
|
const AttestationCheckbox({
|
||||||
|
super.key,
|
||||||
|
required this.isChecked,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: Checkbox(
|
||||||
|
value: isChecked,
|
||||||
|
onChanged: onChanged,
|
||||||
|
activeColor: UiColors.primary,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
t.staff_profile_attire.attestation,
|
||||||
|
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
|
class AttireBottomBar extends StatelessWidget {
|
||||||
|
final bool canSave;
|
||||||
|
final bool allMandatorySelected;
|
||||||
|
final bool allMandatoryHavePhotos;
|
||||||
|
final bool attestationChecked;
|
||||||
|
final VoidCallback onSave;
|
||||||
|
|
||||||
|
const AttireBottomBar({
|
||||||
|
super.key,
|
||||||
|
required this.canSave,
|
||||||
|
required this.allMandatorySelected,
|
||||||
|
required this.allMandatoryHavePhotos,
|
||||||
|
required this.attestationChecked,
|
||||||
|
required this.onSave,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
border: Border(top: BorderSide(color: UiColors.border)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
if (!canSave)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
if (!allMandatorySelected)
|
||||||
|
_buildValidationError(t.staff_profile_attire.validation.select_required),
|
||||||
|
if (!allMandatoryHavePhotos)
|
||||||
|
_buildValidationError(t.staff_profile_attire.validation.upload_required),
|
||||||
|
if (!attestationChecked)
|
||||||
|
_buildValidationError(t.staff_profile_attire.validation.accept_attestation),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
UiButton.primary(
|
||||||
|
text: t.staff_profile_attire.actions.save,
|
||||||
|
onPressed: canSave ? onSave : null, // UiButton handles disabled/null?
|
||||||
|
// UiButton usually takes nullable onPressed to disable.
|
||||||
|
fullWidth: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildValidationError(String text) {
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
style: UiTypography.body3r.copyWith(color: UiColors.destructive),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
class AttireGrid extends StatelessWidget {
|
||||||
|
final List<AttireItem> items;
|
||||||
|
final List<String> selectedIds;
|
||||||
|
final Map<String, String> photoUrls;
|
||||||
|
final Map<String, bool> uploadingStatus;
|
||||||
|
final Function(String id) onToggle;
|
||||||
|
final Function(String id) onUpload;
|
||||||
|
|
||||||
|
const AttireGrid({
|
||||||
|
super.key,
|
||||||
|
required this.items,
|
||||||
|
required this.selectedIds,
|
||||||
|
required this.photoUrls,
|
||||||
|
required this.uploadingStatus,
|
||||||
|
required this.onToggle,
|
||||||
|
required this.onUpload,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
crossAxisSpacing: UiConstants.space3,
|
||||||
|
mainAxisSpacing: UiConstants.space3,
|
||||||
|
childAspectRatio: 0.8,
|
||||||
|
),
|
||||||
|
itemCount: items.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final AttireItem item = items[index];
|
||||||
|
final bool isSelected = selectedIds.contains(item.id);
|
||||||
|
final bool hasPhoto = photoUrls.containsKey(item.id);
|
||||||
|
final bool isUploading = uploadingStatus[item.id] ?? false;
|
||||||
|
|
||||||
|
return _buildCard(item, isSelected, hasPhoto, isUploading);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCard(
|
||||||
|
AttireItem item,
|
||||||
|
bool isSelected,
|
||||||
|
bool hasPhoto,
|
||||||
|
bool isUploading,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? UiColors.primary.withOpacity(0.1) : Colors.transparent,
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? UiColors.primary : UiColors.border,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
if (item.isMandatory)
|
||||||
|
Positioned(
|
||||||
|
top: UiConstants.space2,
|
||||||
|
left: UiConstants.space2,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.destructive, // Red
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
t.staff_profile_attire.status.required,
|
||||||
|
style: UiTypography.body3m.copyWith( // 12px Medium -> Bold
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 9,
|
||||||
|
color: UiColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasPhoto)
|
||||||
|
Positioned(
|
||||||
|
top: UiConstants.space2,
|
||||||
|
right: UiConstants.space2,
|
||||||
|
child: Container(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.check,
|
||||||
|
color: UiColors.white,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => onToggle(item.id),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
item.imageUrl != null
|
||||||
|
? Container(
|
||||||
|
height: 80,
|
||||||
|
width: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
image: DecorationImage(
|
||||||
|
image: NetworkImage(item.imageUrl!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
_getIcon(item.iconName),
|
||||||
|
size: 48,
|
||||||
|
color: UiColors.textPrimary, // Was charcoal
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
item.label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
InkWell(
|
||||||
|
onTap: () => onUpload(item.id),
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: UiConstants.space2,
|
||||||
|
horizontal: UiConstants.space3,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: hasPhoto
|
||||||
|
? UiColors.primary.withOpacity(0.05)
|
||||||
|
: UiColors.white,
|
||||||
|
border: Border.all(
|
||||||
|
color: hasPhoto ? UiColors.primary : UiColors.border,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
if (isUploading)
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(UiColors.primary),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (hasPhoto)
|
||||||
|
const Icon(
|
||||||
|
UiIcons.check,
|
||||||
|
size: 12,
|
||||||
|
color: UiColors.primary,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Icon(
|
||||||
|
UiIcons.camera,
|
||||||
|
size: 12,
|
||||||
|
color: UiColors.textSecondary, // Was muted
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
isUploading
|
||||||
|
? '...'
|
||||||
|
: hasPhoto
|
||||||
|
? t.staff_profile_attire.status.added
|
||||||
|
: t.staff_profile_attire.status.add_photo,
|
||||||
|
style: UiTypography.body3m.copyWith(
|
||||||
|
color: hasPhoto ? UiColors.primary : UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasPhoto)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
t.staff_profile_attire.status.pending,
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
fontSize: 10,
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getIcon(String? name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'footprints':
|
||||||
|
return LucideIcons.footprints;
|
||||||
|
case 'scissors':
|
||||||
|
return LucideIcons.scissors;
|
||||||
|
case 'user':
|
||||||
|
return LucideIcons.user;
|
||||||
|
case 'shirt':
|
||||||
|
return LucideIcons.shirt;
|
||||||
|
case 'hardHat':
|
||||||
|
return LucideIcons.hardHat;
|
||||||
|
case 'chefHat':
|
||||||
|
return LucideIcons.chefHat;
|
||||||
|
default:
|
||||||
|
return LucideIcons.helpCircle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
|
class AttireInfoCard extends StatelessWidget {
|
||||||
|
const AttireInfoCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Using LucideIcons for domain specific content icon not in UiIcons
|
||||||
|
const Icon(LucideIcons.shirt, color: UiColors.primary, size: 24),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
t.staff_profile_attire.info_card.title,
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
t.staff_profile_attire.info_card.description,
|
||||||
|
style: UiTypography.body2r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
library staff_attire;
|
||||||
|
|
||||||
|
export 'src/attire_module.dart';
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: staff_attire
|
||||||
|
description: "Feature package for Staff Attire management"
|
||||||
|
version: 0.0.1
|
||||||
|
publish_to: none
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.10.0 <4.0.0'
|
||||||
|
flutter: ">=3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_bloc: ^8.1.0
|
||||||
|
flutter_modular: ^6.0.0
|
||||||
|
equatable: ^2.0.5
|
||||||
|
firebase_data_connect: ^0.2.2+1
|
||||||
|
|
||||||
|
# Internal packages
|
||||||
|
krow_core:
|
||||||
|
path: ../../../../../core
|
||||||
|
krow_domain:
|
||||||
|
path: ../../../../../domain
|
||||||
|
krow_data_connect:
|
||||||
|
path: ../../../../../data_connect
|
||||||
|
design_system:
|
||||||
|
path: ../../../../../design_system
|
||||||
|
core_localization:
|
||||||
|
path: ../../../../../core_localization
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
@@ -9,6 +9,7 @@ import 'package:staff_bank_account/staff_bank_account.dart';
|
|||||||
import 'package:staff_tax_forms/staff_tax_forms.dart';
|
import 'package:staff_tax_forms/staff_tax_forms.dart';
|
||||||
import 'package:staff_documents/staff_documents.dart';
|
import 'package:staff_documents/staff_documents.dart';
|
||||||
import 'package:staff_certificates/staff_certificates.dart';
|
import 'package:staff_certificates/staff_certificates.dart';
|
||||||
|
import 'package:staff_attire/staff_attire.dart';
|
||||||
|
|
||||||
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
|
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
|
||||||
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
||||||
@@ -55,6 +56,7 @@ class StaffMainModule extends Module {
|
|||||||
r.module('/onboarding', module: StaffProfileInfoModule());
|
r.module('/onboarding', module: StaffProfileInfoModule());
|
||||||
r.module('/emergency-contact', module: StaffEmergencyContactModule());
|
r.module('/emergency-contact', module: StaffEmergencyContactModule());
|
||||||
r.module('/experience', module: StaffProfileExperienceModule());
|
r.module('/experience', module: StaffProfileExperienceModule());
|
||||||
|
r.module('/attire', module: StaffAttireModule());
|
||||||
r.module('/bank-account', module: StaffBankAccountModule());
|
r.module('/bank-account', module: StaffBankAccountModule());
|
||||||
r.module('/tax-forms', module: StaffTaxFormsModule());
|
r.module('/tax-forms', module: StaffTaxFormsModule());
|
||||||
r.module(
|
r.module(
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ dependencies:
|
|||||||
path: ../profile_sections/compliance/documents
|
path: ../profile_sections/compliance/documents
|
||||||
staff_certificates:
|
staff_certificates:
|
||||||
path: ../profile_sections/compliance/certificates
|
path: ../profile_sections/compliance/certificates
|
||||||
|
staff_attire:
|
||||||
|
path: ../profile_sections/onboarding/attire
|
||||||
# staff_shifts:
|
# staff_shifts:
|
||||||
# path: ../shifts
|
# path: ../shifts
|
||||||
# staff_payments:
|
# staff_payments:
|
||||||
|
|||||||
@@ -1072,6 +1072,13 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
version: "1.12.1"
|
||||||
|
staff_attire:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
path: "packages/features/staff/profile_sections/onboarding/attire"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
staff_bank_account:
|
staff_bank_account:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user