feat: update profile setup and bank account management; enhance API integration and data handling
This commit is contained in:
@@ -61,7 +61,7 @@ class StaffSession extends Equatable {
|
||||
vendorId: staff['vendorId'] as String?,
|
||||
workforceNumber: staff['workforceNumber'] as String?,
|
||||
metadata: (staff['metadata'] as Map<String, dynamic>?) ?? const <String, dynamic>{},
|
||||
userId: user['userId'] as String?,
|
||||
userId: (user['userId'] ?? user['id']) as String?,
|
||||
tenantName: tenant['tenantName'] as String?,
|
||||
tenantSlug: tenant['tenantSlug'] as String?,
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
@@ -20,18 +21,32 @@ class ProfileSetupRepositoryImpl implements ProfileSetupRepository {
|
||||
@override
|
||||
Future<void> submitProfile({
|
||||
required String fullName,
|
||||
required String phoneNumber,
|
||||
String? bio,
|
||||
required List<String> preferredLocations,
|
||||
required double maxDistanceMiles,
|
||||
required List<String> industries,
|
||||
required List<String> skills,
|
||||
}) async {
|
||||
// Convert location label strings to the object shape the V2 API expects.
|
||||
// The backend zod schema requires: { label, city?, state?, ... }.
|
||||
final List<Map<String, String>> locationObjects = preferredLocations
|
||||
.map((String label) => <String, String>{'label': label})
|
||||
.toList();
|
||||
|
||||
// Resolve the phone number: prefer the explicit parameter, but fall back
|
||||
// to the Firebase Auth current user's phone if the caller passed empty.
|
||||
final String resolvedPhone = phoneNumber.isNotEmpty
|
||||
? phoneNumber
|
||||
: (FirebaseAuth.instance.currentUser?.phoneNumber ?? '');
|
||||
|
||||
final ApiResponse response = await _apiService.post(
|
||||
StaffEndpoints.profileSetup,
|
||||
data: <String, dynamic>{
|
||||
'fullName': fullName,
|
||||
'phoneNumber': resolvedPhone,
|
||||
if (bio != null && bio.isNotEmpty) 'bio': bio,
|
||||
'preferredLocations': preferredLocations,
|
||||
'preferredLocations': locationObjects,
|
||||
'maxDistanceMiles': maxDistanceMiles.toInt(),
|
||||
'industries': industries,
|
||||
'skills': skills,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
|
||||
/// Interface for the staff profile setup repository.
|
||||
abstract class ProfileSetupRepository {
|
||||
/// Submits the staff profile setup data to the backend.
|
||||
Future<void> submitProfile({
|
||||
required String fullName,
|
||||
required String phoneNumber,
|
||||
String? bio,
|
||||
required List<String> preferredLocations,
|
||||
required double maxDistanceMiles,
|
||||
|
||||
@@ -13,6 +13,7 @@ class SubmitProfileSetup {
|
||||
/// Submits the profile setup with the given data.
|
||||
Future<void> call({
|
||||
required String fullName,
|
||||
required String phoneNumber,
|
||||
String? bio,
|
||||
required List<String> preferredLocations,
|
||||
required double maxDistanceMiles,
|
||||
@@ -21,6 +22,7 @@ class SubmitProfileSetup {
|
||||
}) {
|
||||
return repository.submitProfile(
|
||||
fullName: fullName,
|
||||
phoneNumber: phoneNumber,
|
||||
bio: bio,
|
||||
preferredLocations: preferredLocations,
|
||||
maxDistanceMiles: maxDistanceMiles,
|
||||
|
||||
@@ -12,11 +12,16 @@ export 'package:staff_authentication/src/presentation/blocs/profile_setup/profil
|
||||
class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState>
|
||||
with BlocErrorHandler<ProfileSetupState> {
|
||||
/// Creates a [ProfileSetupBloc].
|
||||
///
|
||||
/// [phoneNumber] is the authenticated user's phone from the sign-up flow,
|
||||
/// required by the V2 profile-setup endpoint.
|
||||
ProfileSetupBloc({
|
||||
required SubmitProfileSetup submitProfileSetup,
|
||||
required SearchCitiesUseCase searchCities,
|
||||
required String phoneNumber,
|
||||
}) : _submitProfileSetup = submitProfileSetup,
|
||||
_searchCities = searchCities,
|
||||
_phoneNumber = phoneNumber,
|
||||
super(const ProfileSetupState()) {
|
||||
on<ProfileSetupFullNameChanged>(_onFullNameChanged);
|
||||
on<ProfileSetupBioChanged>(_onBioChanged);
|
||||
@@ -35,6 +40,9 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState>
|
||||
/// The use case for searching cities.
|
||||
final SearchCitiesUseCase _searchCities;
|
||||
|
||||
/// The user's phone number from the sign-up flow.
|
||||
final String _phoneNumber;
|
||||
|
||||
/// Handles the [ProfileSetupFullNameChanged] event.
|
||||
void _onFullNameChanged(
|
||||
ProfileSetupFullNameChanged event,
|
||||
@@ -95,6 +103,7 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState>
|
||||
action: () async {
|
||||
await _submitProfileSetup(
|
||||
fullName: state.fullName,
|
||||
phoneNumber: _phoneNumber,
|
||||
bio: state.bio.isEmpty ? null : state.bio,
|
||||
preferredLocations: state.preferredLocations,
|
||||
maxDistanceMiles: state.maxDistanceMiles,
|
||||
|
||||
@@ -56,6 +56,7 @@ class StaffAuthenticationModule extends Module {
|
||||
() => ProfileSetupBloc(
|
||||
submitProfileSetup: i.get<SubmitProfileSetup>(),
|
||||
searchCities: i.get<SearchCitiesUseCase>(),
|
||||
phoneNumber: i.get<AuthBloc>().state.phoneNumber,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
id: json['shiftId'] as String,
|
||||
orderId: json['orderId'] as String? ?? '',
|
||||
title: json['clientName'] as String? ?? json['roleName'] as String? ?? '',
|
||||
status: ShiftStatus.fromJson(json['attendanceStatus'] as String?),
|
||||
status: ShiftStatus.assigned,
|
||||
startsAt: DateTime.parse(json['startTime'] as String),
|
||||
endsAt: DateTime.parse(json['endTime'] as String),
|
||||
locationName: json['location'] as String?,
|
||||
|
||||
@@ -29,7 +29,7 @@ class CertificatesRepositoryImpl implements CertificatesRepository {
|
||||
final ApiResponse response =
|
||||
await _api.get(StaffEndpoints.certificates);
|
||||
final List<dynamic> items =
|
||||
response.data['certificates'] as List<dynamic>? ?? <dynamic>[];
|
||||
response.data['items'] as List<dynamic>? ?? <dynamic>[];
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
StaffCertificate.fromJson(json as Map<String, dynamic>))
|
||||
|
||||
@@ -28,7 +28,7 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
|
||||
Future<List<ProfileDocument>> getDocuments() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(StaffEndpoints.documents);
|
||||
final List<dynamic> items = response.data['documents'] as List<dynamic>? ?? <dynamic>[];
|
||||
final List<dynamic> items = response.data['items'] as List<dynamic>? ?? <dynamic>[];
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
ProfileDocument.fromJson(json as Map<String, dynamic>))
|
||||
|
||||
@@ -19,7 +19,7 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
|
||||
Future<List<TaxForm>> getTaxForms() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(StaffEndpoints.taxForms);
|
||||
final List<dynamic> items = response.data['taxForms'] as List<dynamic>? ?? <dynamic>[];
|
||||
final List<dynamic> items = response.data['items'] as List<dynamic>? ?? <dynamic>[];
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
TaxForm.fromJson(json as Map<String, dynamic>))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:staff_bank_account/src/domain/arguments/add_bank_account_params.dart';
|
||||
import 'package:staff_bank_account/src/domain/repositories/bank_account_repository.dart';
|
||||
|
||||
/// Implementation of [BankAccountRepository] using the V2 API.
|
||||
@@ -17,7 +18,7 @@ class BankAccountRepositoryImpl implements BankAccountRepository {
|
||||
Future<List<BankAccount>> getAccounts() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(StaffEndpoints.bankAccounts);
|
||||
final List<dynamic> items = response.data['accounts'] as List<dynamic>? ?? <dynamic>[];
|
||||
final List<dynamic> items = response.data['items'] as List<dynamic>? ?? <dynamic>[];
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
BankAccount.fromJson(json as Map<String, dynamic>))
|
||||
@@ -25,10 +26,10 @@ class BankAccountRepositoryImpl implements BankAccountRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addAccount(BankAccount account) async {
|
||||
Future<void> addAccount(AddBankAccountParams params) async {
|
||||
await _api.post(
|
||||
StaffEndpoints.bankAccounts,
|
||||
data: account.toJson(),
|
||||
data: params.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show AccountType;
|
||||
|
||||
/// Arguments for adding a bank account.
|
||||
class AddBankAccountParams extends UseCaseArgument with EquatableMixin {
|
||||
/// Parameters for creating a new bank account via the V2 API.
|
||||
///
|
||||
/// Maps directly to the `bankAccountCreateSchema` zod schema:
|
||||
/// `{ bankName, accountNumber, routingNumber, accountType }`.
|
||||
class AddBankAccountParams extends UseCaseArgument {
|
||||
/// Creates an [AddBankAccountParams].
|
||||
const AddBankAccountParams({
|
||||
required this.bankName,
|
||||
required this.accountNumber,
|
||||
required this.routingNumber,
|
||||
required this.accountType,
|
||||
});
|
||||
|
||||
const AddBankAccountParams({required this.account});
|
||||
final BankAccount account;
|
||||
/// Name of the bank / financial institution.
|
||||
final String bankName;
|
||||
|
||||
/// Full account number.
|
||||
final String accountNumber;
|
||||
|
||||
/// Routing / transit number.
|
||||
final String routingNumber;
|
||||
|
||||
/// Account type (checking or savings).
|
||||
final AccountType accountType;
|
||||
|
||||
/// Serialises to the V2 API request body.
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
'bankName': bankName,
|
||||
'accountNumber': accountNumber,
|
||||
'routingNumber': routingNumber,
|
||||
'accountType': accountType.toJson(),
|
||||
};
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[account];
|
||||
|
||||
@override
|
||||
bool? get stringify => true;
|
||||
List<Object?> get props => <Object?>[
|
||||
bankName,
|
||||
accountNumber,
|
||||
routingNumber,
|
||||
accountType,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show BankAccount;
|
||||
|
||||
import '../arguments/add_bank_account_params.dart';
|
||||
|
||||
/// Repository interface for managing bank accounts.
|
||||
///
|
||||
/// Uses [BankAccount] from the V2 domain layer.
|
||||
abstract class BankAccountRepository {
|
||||
/// Fetches the list of bank accounts for the current staff member.
|
||||
Future<List<BankAccount>> getAccounts();
|
||||
|
||||
/// Adds a new bank account.
|
||||
Future<void> addAccount(BankAccount account);
|
||||
/// Creates a new bank account with the given [params].
|
||||
Future<void> addAccount(AddBankAccountParams params);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../repositories/bank_account_repository.dart';
|
||||
|
||||
import '../arguments/add_bank_account_params.dart';
|
||||
import '../repositories/bank_account_repository.dart';
|
||||
|
||||
/// Use case to add a bank account.
|
||||
class AddBankAccountUseCase implements UseCase<AddBankAccountParams, void> {
|
||||
|
||||
/// Creates an [AddBankAccountUseCase].
|
||||
AddBankAccountUseCase(this._repository);
|
||||
|
||||
final BankAccountRepository _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(AddBankAccountParams params) {
|
||||
return _repository.addAccount(params.account);
|
||||
return _repository.addAccount(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show AccountType, BankAccount;
|
||||
|
||||
import '../../domain/arguments/add_bank_account_params.dart';
|
||||
import '../../domain/usecases/add_bank_account_usecase.dart';
|
||||
import '../../domain/usecases/get_bank_accounts_usecase.dart';
|
||||
@@ -47,15 +48,10 @@ class BankAccountCubit extends Cubit<BankAccountState>
|
||||
}) async {
|
||||
emit(state.copyWith(status: BankAccountStatus.loading));
|
||||
|
||||
// Create domain entity
|
||||
final BankAccount newAccount = BankAccount(
|
||||
accountId: '', // Generated by server
|
||||
final AddBankAccountParams params = AddBankAccountParams(
|
||||
bankName: bankName,
|
||||
providerReference: routingNumber,
|
||||
last4: accountNumber.length > 4
|
||||
? accountNumber.substring(accountNumber.length - 4)
|
||||
: accountNumber,
|
||||
isPrimary: false,
|
||||
accountNumber: accountNumber,
|
||||
routingNumber: routingNumber,
|
||||
accountType: type == 'CHECKING'
|
||||
? AccountType.checking
|
||||
: AccountType.savings,
|
||||
@@ -64,7 +60,7 @@ class BankAccountCubit extends Cubit<BankAccountState>
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
await _addBankAccountUseCase(AddBankAccountParams(account: newAccount));
|
||||
await _addBankAccountUseCase(params);
|
||||
|
||||
// Re-fetch to get latest state including server-generated IDs
|
||||
await loadAccounts();
|
||||
|
||||
@@ -22,7 +22,7 @@ class TimeCardRepositoryImpl implements TimeCardRepository {
|
||||
'month': month.month,
|
||||
},
|
||||
);
|
||||
final List<dynamic> items = response.data['entries'] as List<dynamic>? ?? <dynamic>[];
|
||||
final List<dynamic> items = response.data['items'] as List<dynamic>? ?? <dynamic>[];
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
TimeCardEntry.fromJson(json as Map<String, dynamic>))
|
||||
|
||||
@@ -18,7 +18,8 @@ class EmergencyContactRepositoryImpl
|
||||
Future<List<EmergencyContact>> getContacts() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(StaffEndpoints.emergencyContacts);
|
||||
final List<dynamic> items = response.data['contacts'] as List<dynamic>? ?? <dynamic>[];
|
||||
final List<dynamic> items =
|
||||
response.data['items'] as List<dynamic>? ?? <dynamic>[];
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
EmergencyContact.fromJson(json as Map<String, dynamic>))
|
||||
@@ -27,12 +28,27 @@ class EmergencyContactRepositoryImpl
|
||||
|
||||
@override
|
||||
Future<void> saveContacts(List<EmergencyContact> contacts) async {
|
||||
for (final EmergencyContact contact in contacts) {
|
||||
final Map<String, dynamic> body = <String, dynamic>{
|
||||
'fullName': contact.fullName,
|
||||
'phone': contact.phone,
|
||||
'relationshipType': contact.relationshipType,
|
||||
'isPrimary': contact.isPrimary,
|
||||
};
|
||||
|
||||
if (contact.contactId.isNotEmpty) {
|
||||
// Existing contact — update via PUT.
|
||||
await _api.put(
|
||||
StaffEndpoints.emergencyContactUpdate(contact.contactId),
|
||||
data: body,
|
||||
);
|
||||
} else {
|
||||
// New contact — create via POST.
|
||||
await _api.post(
|
||||
StaffEndpoints.emergencyContacts,
|
||||
data: <String, dynamic>{
|
||||
'contacts':
|
||||
contacts.map((EmergencyContact c) => c.toJson()).toList(),
|
||||
},
|
||||
data: body,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
@@ -10,20 +11,12 @@ import 'package:staff_profile_info/src/domain/repositories/personal_info_reposit
|
||||
class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
/// Creates a [PersonalInfoRepositoryImpl].
|
||||
///
|
||||
/// Requires the V2 [BaseApiService] for HTTP communication,
|
||||
/// [FileUploadService] for uploading files to cloud storage, and
|
||||
/// [SignedUrlService] for generating signed download URLs.
|
||||
/// Requires the V2 [BaseApiService] for HTTP communication.
|
||||
PersonalInfoRepositoryImpl({
|
||||
required BaseApiService apiService,
|
||||
required FileUploadService uploadService,
|
||||
required SignedUrlService signedUrlService,
|
||||
}) : _api = apiService,
|
||||
_uploadService = uploadService,
|
||||
_signedUrlService = signedUrlService;
|
||||
}) : _api = apiService;
|
||||
|
||||
final BaseApiService _api;
|
||||
final FileUploadService _uploadService;
|
||||
final SignedUrlService _signedUrlService;
|
||||
|
||||
@override
|
||||
Future<StaffPersonalInfo> getStaffProfile() async {
|
||||
@@ -39,39 +32,34 @@ class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
required String staffId,
|
||||
required Map<String, dynamic> data,
|
||||
}) async {
|
||||
final ApiResponse response = await _api.put(
|
||||
// The PUT response returns { staffId, fullName, email, phone, metadata }
|
||||
// which does not match the StaffPersonalInfo shape. Perform the update
|
||||
// and then re-fetch the full profile to return the correct entity.
|
||||
await _api.put(
|
||||
StaffEndpoints.personalInfo,
|
||||
data: data,
|
||||
);
|
||||
final Map<String, dynamic> json =
|
||||
response.data as Map<String, dynamic>;
|
||||
return StaffPersonalInfo.fromJson(json);
|
||||
return getStaffProfile();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> uploadProfilePhoto(String filePath) async {
|
||||
// 1. Upload the file to cloud storage.
|
||||
final FileUploadResponse uploadRes = await _uploadService.uploadFile(
|
||||
filePath: filePath,
|
||||
fileName:
|
||||
'staff_profile_photo_${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||||
visibility: FileVisibility.public,
|
||||
);
|
||||
// The backend expects a multipart file upload at /staff/profile/photo.
|
||||
// It uploads to GCS, updates staff metadata, and returns a signed URL.
|
||||
final String fileName =
|
||||
'staff_profile_photo_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final FormData formData = FormData.fromMap(<String, dynamic>{
|
||||
'file': await MultipartFile.fromFile(filePath, filename: fileName),
|
||||
});
|
||||
|
||||
// 2. Generate a signed URL for the uploaded file.
|
||||
final SignedUrlResponse signedUrlRes =
|
||||
await _signedUrlService.createSignedUrl(fileUri: uploadRes.fileUri);
|
||||
final String photoUrl = signedUrlRes.signedUrl;
|
||||
|
||||
// 3. Submit the photo URL to the V2 API.
|
||||
await _api.post(
|
||||
final ApiResponse response = await _api.post(
|
||||
StaffEndpoints.profilePhoto,
|
||||
data: <String, dynamic>{
|
||||
'fileUri': uploadRes.fileUri,
|
||||
'photoUrl': photoUrl,
|
||||
},
|
||||
data: formData,
|
||||
);
|
||||
final Map<String, dynamic> json =
|
||||
response.data as Map<String, dynamic>;
|
||||
|
||||
return photoUrl;
|
||||
// Backend returns { staffId, fileUri, signedUrl, expiresAt }.
|
||||
return json['signedUrl'] as String? ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ class StaffProfileInfoModule extends Module {
|
||||
i.addLazySingleton<PersonalInfoRepositoryInterface>(
|
||||
() => PersonalInfoRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
uploadService: i.get<FileUploadService>(),
|
||||
signedUrlService: i.get<SignedUrlService>(),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ dependencies:
|
||||
flutter_bloc: ^8.1.0
|
||||
bloc: ^8.1.0
|
||||
flutter_modular: ^6.3.0
|
||||
dio: ^5.9.1
|
||||
equatable: ^2.0.5
|
||||
|
||||
# Architecture Packages
|
||||
|
||||
@@ -96,12 +96,10 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
final ApiResponse response =
|
||||
await _apiService.get(StaffEndpoints.shiftsCompleted);
|
||||
final List<dynamic> items = _extractItems(response.data);
|
||||
var x = items
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
CompletedShift.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user