feat: update profile setup and bank account management; enhance API integration and data handling

This commit is contained in:
Achintha Isuru
2026-03-17 14:32:26 -04:00
parent b6a655a261
commit de388c9a77
21 changed files with 142 additions and 85 deletions

View File

@@ -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?,
);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -56,6 +56,7 @@ class StaffAuthenticationModule extends Module {
() => ProfileSetupBloc(
submitProfileSetup: i.get<SubmitProfileSetup>(),
searchCities: i.get<SearchCitiesUseCase>(),
phoneNumber: i.get<AuthBloc>().state.phoneNumber,
),
);
}

View File

@@ -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?,

View File

@@ -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>))

View File

@@ -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>))

View File

@@ -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>))

View File

@@ -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(),
);
}
}

View File

@@ -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,
];
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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>))

View File

@@ -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,
);
}
}
}
}

View File

@@ -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? ?? '';
}
}

View File

@@ -27,8 +27,6 @@ class StaffProfileInfoModule extends Module {
i.addLazySingleton<PersonalInfoRepositoryInterface>(
() => PersonalInfoRepositoryImpl(
apiService: i.get<BaseApiService>(),
uploadService: i.get<FileUploadService>(),
signedUrlService: i.get<SignedUrlService>(),
),
);

View File

@@ -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

View File

@@ -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