feat: complete centralized error handling system with documentation
This commit is contained in:
@@ -10,7 +10,9 @@ import '../../domain/repositories/certificates_repository.dart';
|
||||
///
|
||||
/// This class handles the communication with the backend via [ExampleConnector].
|
||||
/// It maps raw generated data types to clean [domain.StaffDocument] entities.
|
||||
class CertificatesRepositoryImpl implements CertificatesRepository {
|
||||
class CertificatesRepositoryImpl
|
||||
with DataErrorHandler
|
||||
implements CertificatesRepository {
|
||||
/// The generated Data Connect SDK client.
|
||||
final ExampleConnector _dataConnect;
|
||||
|
||||
@@ -24,16 +26,17 @@ class CertificatesRepositoryImpl implements CertificatesRepository {
|
||||
required ExampleConnector dataConnect,
|
||||
required FirebaseAuth firebaseAuth,
|
||||
}) : _dataConnect = dataConnect,
|
||||
_firebaseAuth = firebaseAuth;
|
||||
_firebaseAuth = firebaseAuth;
|
||||
|
||||
@override
|
||||
Future<List<domain.StaffDocument>> getCertificates() async {
|
||||
final User? currentUser = _firebaseAuth.currentUser;
|
||||
if (currentUser == null) {
|
||||
throw Exception('User not authenticated');
|
||||
}
|
||||
return executeProtected(() async {
|
||||
final User? currentUser = _firebaseAuth.currentUser;
|
||||
if (currentUser == null) {
|
||||
throw domain.NotAuthenticatedException(
|
||||
technicalMessage: 'User not authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the query via DataConnect generated SDK
|
||||
final QueryResult<ListStaffDocumentsByStaffIdData,
|
||||
ListStaffDocumentsByStaffIdVariables> result =
|
||||
@@ -46,10 +49,7 @@ class CertificatesRepositoryImpl implements CertificatesRepository {
|
||||
.map((ListStaffDocumentsByStaffIdStaffDocuments doc) =>
|
||||
_mapToDomain(doc))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
// In a real app, we would map specific exceptions to domain Failures here.
|
||||
throw Exception('Failed to fetch certificates: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Maps the Data Connect [ListStaffDocumentsByStaffIdStaffDocuments] to a domain [domain.StaffDocument].
|
||||
|
||||
@@ -35,7 +35,19 @@ class CertificatesPage extends StatelessWidget {
|
||||
|
||||
if (state.status == CertificatesStatus.failure) {
|
||||
return Scaffold(
|
||||
body: Center(child: Text('Error: ${state.errorMessage}')),
|
||||
appBar: AppBar(title: const Text('Certificates')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'Error loading certificates',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: UiColors.textSecondary),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import 'package:krow_core/core.dart';
|
||||
import '../../domain/repositories/documents_repository.dart';
|
||||
|
||||
/// Implementation of [DocumentsRepository] using Data Connect.
|
||||
class DocumentsRepositoryImpl implements DocumentsRepository {
|
||||
class DocumentsRepositoryImpl
|
||||
with DataErrorHandler
|
||||
implements DocumentsRepository {
|
||||
final ExampleConnector _dataConnect;
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
|
||||
@@ -19,10 +21,12 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
|
||||
|
||||
@override
|
||||
Future<List<domain.StaffDocument>> getDocuments() async {
|
||||
final User? currentUser = _firebaseAuth.currentUser;
|
||||
if (currentUser == null) {
|
||||
throw Exception('User not authenticated');
|
||||
}
|
||||
return executeProtected(() async {
|
||||
final User? currentUser = _firebaseAuth.currentUser;
|
||||
if (currentUser == null) {
|
||||
throw domain.NotAuthenticatedException(
|
||||
technicalMessage: 'User not authenticated');
|
||||
}
|
||||
|
||||
/// MOCK IMPLEMENTATION
|
||||
/// To be replaced with real data connect query when available
|
||||
@@ -49,22 +53,7 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
|
||||
),
|
||||
];
|
||||
|
||||
/*
|
||||
try {
|
||||
final QueryResult<ListStaffDocumentsByStaffIdData,
|
||||
ListStaffDocumentsByStaffIdVariables> result =
|
||||
await _dataConnect
|
||||
.listStaffDocumentsByStaffId(staffId: currentUser.uid)
|
||||
.execute();
|
||||
|
||||
return result.data.staffDocuments
|
||||
.map((ListStaffDocumentsByStaffIdStaffDocuments doc) =>
|
||||
_mapToDomain(doc))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to fetch documents: $e');
|
||||
}
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
domain.StaffDocument _mapToDomain(
|
||||
|
||||
@@ -52,11 +52,17 @@ class DocumentsPage extends StatelessWidget {
|
||||
}
|
||||
if (state.status == DocumentsStatus.failure) {
|
||||
return Center(
|
||||
child: Text(
|
||||
t.staff_documents.list.error(
|
||||
message: state.errorMessage ?? 'Unknown',
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
state.errorMessage != null
|
||||
? (state.errorMessage!.contains('errors.')
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: t.staff_documents.list.error(message: state.errorMessage!))
|
||||
: t.staff_documents.list.error(message: 'Unknown'),
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body1m.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
style: UiTypography.body1m.copyWith(color: UiColors.textError),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart' as auth;
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
@@ -8,7 +9,9 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/tax_forms_repository.dart';
|
||||
import '../mappers/tax_form_mapper.dart';
|
||||
|
||||
class TaxFormsRepositoryImpl implements TaxFormsRepository {
|
||||
class TaxFormsRepositoryImpl
|
||||
with dc.DataErrorHandler
|
||||
implements TaxFormsRepository {
|
||||
TaxFormsRepositoryImpl({
|
||||
required this.firebaseAuth,
|
||||
required this.dataConnect,
|
||||
@@ -21,46 +24,58 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
|
||||
String _getStaffId() {
|
||||
final auth.User? user = firebaseAuth.currentUser;
|
||||
if (user == null) {
|
||||
throw Exception('User not authenticated');
|
||||
throw const NotAuthenticatedException(
|
||||
technicalMessage: 'Firebase User is null',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
final String? staffId = dc.StaffSessionStore.instance.session?.staff?.id;
|
||||
if (staffId == null || staffId.isEmpty) {
|
||||
throw Exception('Staff profile is missing or session not initialized.');
|
||||
throw const StaffProfileNotFoundException(
|
||||
technicalMessage: 'Staff ID missing in SessionStore',
|
||||
);
|
||||
}
|
||||
return staffId;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TaxForm>> getTaxForms() async {
|
||||
final String staffId = _getStaffId();
|
||||
final QueryResult<dc.GetTaxFormsByStaffIdData, dc.GetTaxFormsByStaffIdVariables>
|
||||
result =
|
||||
await dataConnect.getTaxFormsByStaffId(staffId: staffId).execute();
|
||||
|
||||
final List<TaxForm> forms = result.data.taxForms.map(TaxFormMapper.fromDataConnect).toList();
|
||||
|
||||
// Check if required forms exist, create if not.
|
||||
final Set<TaxFormType> typesPresent = forms.map((TaxForm f) => f.type).toSet();
|
||||
bool createdNew = false;
|
||||
|
||||
if (!typesPresent.contains(TaxFormType.i9)) {
|
||||
await _createInitialForm(staffId, TaxFormType.i9);
|
||||
createdNew = true;
|
||||
}
|
||||
if (!typesPresent.contains(TaxFormType.w4)) {
|
||||
await _createInitialForm(staffId, TaxFormType.w4);
|
||||
createdNew = true;
|
||||
}
|
||||
|
||||
if (createdNew) {
|
||||
return executeProtected(() async {
|
||||
final String staffId = _getStaffId();
|
||||
final QueryResult<dc.GetTaxFormsByStaffIdData, dc.GetTaxFormsByStaffIdVariables>
|
||||
result2 =
|
||||
await dataConnect.getTaxFormsByStaffId(staffId: staffId).execute();
|
||||
return result2.data.taxForms.map(TaxFormMapper.fromDataConnect).toList();
|
||||
}
|
||||
result = await dataConnect
|
||||
.getTaxFormsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
return forms;
|
||||
final List<TaxForm> forms =
|
||||
result.data.taxForms.map(TaxFormMapper.fromDataConnect).toList();
|
||||
|
||||
// Check if required forms exist, create if not.
|
||||
final Set<TaxFormType> typesPresent =
|
||||
forms.map((TaxForm f) => f.type).toSet();
|
||||
bool createdNew = false;
|
||||
|
||||
if (!typesPresent.contains(TaxFormType.i9)) {
|
||||
await _createInitialForm(staffId, TaxFormType.i9);
|
||||
createdNew = true;
|
||||
}
|
||||
if (!typesPresent.contains(TaxFormType.w4)) {
|
||||
await _createInitialForm(staffId, TaxFormType.w4);
|
||||
createdNew = true;
|
||||
}
|
||||
|
||||
if (createdNew) {
|
||||
final QueryResult<
|
||||
dc.GetTaxFormsByStaffIdData,
|
||||
dc.GetTaxFormsByStaffIdVariables> result2 =
|
||||
await dataConnect.getTaxFormsByStaffId(staffId: staffId).execute();
|
||||
return result2.data.taxForms
|
||||
.map(TaxFormMapper.fromDataConnect)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return forms;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _createInitialForm(String staffId, TaxFormType type) async {
|
||||
@@ -80,45 +95,62 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
|
||||
|
||||
@override
|
||||
Future<void> updateI9Form(I9TaxForm form) async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapI9Fields(builder, data);
|
||||
await builder.execute();
|
||||
return executeProtected(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
||||
dataConnect.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapI9Fields(builder, data);
|
||||
await builder.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> submitI9Form(I9TaxForm form) async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapI9Fields(builder, data);
|
||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||
return executeProtected(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
||||
dataConnect.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapI9Fields(builder, data);
|
||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateW4Form(W4TaxForm form) async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapW4Fields(builder, data);
|
||||
await builder.execute();
|
||||
return executeProtected(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
||||
dataConnect.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapW4Fields(builder, data);
|
||||
await builder.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> submitW4Form(W4TaxForm form) async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapW4Fields(builder, data);
|
||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||
return executeProtected(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
||||
dataConnect.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapW4Fields(builder, data);
|
||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||
});
|
||||
}
|
||||
|
||||
void _mapCommonFields(dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
||||
if (data.containsKey('firstName')) builder.firstName(data['firstName'] as String?);
|
||||
if (data.containsKey('lastName')) builder.lastName(data['lastName'] as String?);
|
||||
if (data.containsKey('middleInitial')) builder.mInitial(data['middleInitial'] as String?);
|
||||
if (data.containsKey('otherLastNames')) builder.oLastName(data['otherLastNames'] as String?);
|
||||
void _mapCommonFields(
|
||||
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
||||
if (data.containsKey('firstName'))
|
||||
builder.firstName(data['firstName'] as String?);
|
||||
if (data.containsKey('lastName'))
|
||||
builder.lastName(data['lastName'] as String?);
|
||||
if (data.containsKey('middleInitial'))
|
||||
builder.mInitial(data['middleInitial'] as String?);
|
||||
if (data.containsKey('otherLastNames'))
|
||||
builder.oLastName(data['otherLastNames'] as String?);
|
||||
if (data.containsKey('dob')) {
|
||||
final String dob = data['dob'] as String;
|
||||
// Handle both ISO string and MM/dd/yyyy manual entry
|
||||
@@ -131,8 +163,8 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
|
||||
final List<String> parts = dob.split('/');
|
||||
if (parts.length == 3) {
|
||||
date = DateTime(
|
||||
int.parse(parts[2]),
|
||||
int.parse(parts[0]),
|
||||
int.parse(parts[2]),
|
||||
int.parse(parts[0]),
|
||||
int.parse(parts[1]),
|
||||
);
|
||||
}
|
||||
@@ -145,70 +177,90 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
|
||||
}
|
||||
}
|
||||
if (data.containsKey('ssn') && data['ssn']?.toString().isNotEmpty == true) {
|
||||
builder.socialSN(int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ?? 0);
|
||||
builder.socialSN(
|
||||
int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ??
|
||||
0);
|
||||
}
|
||||
if (data.containsKey('email')) builder.email(data['email'] as String?);
|
||||
if (data.containsKey('phone')) builder.phone(data['phone'] as String?);
|
||||
if (data.containsKey('address')) builder.address(data['address'] as String?);
|
||||
if (data.containsKey('aptNumber')) builder.apt(data['aptNumber'] as String?);
|
||||
if (data.containsKey('address'))
|
||||
builder.address(data['address'] as String?);
|
||||
if (data.containsKey('aptNumber'))
|
||||
builder.apt(data['aptNumber'] as String?);
|
||||
if (data.containsKey('city')) builder.city(data['city'] as String?);
|
||||
if (data.containsKey('state')) builder.state(data['state'] as String?);
|
||||
if (data.containsKey('zipCode')) builder.zipCode(data['zipCode'] as String?);
|
||||
if (data.containsKey('zipCode'))
|
||||
builder.zipCode(data['zipCode'] as String?);
|
||||
}
|
||||
|
||||
void _mapI9Fields(dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
||||
void _mapI9Fields(
|
||||
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
||||
if (data.containsKey('citizenshipStatus')) {
|
||||
final String status = data['citizenshipStatus'] as String;
|
||||
// Map string to enum if possible, or handle otherwise.
|
||||
// Generated enum: CITIZEN, NONCITIZEN_NATIONAL, PERMANENT_RESIDENT, ALIEN_AUTHORIZED
|
||||
try {
|
||||
builder.citizen(dc.CitizenshipStatus.values.byName(status.toUpperCase()));
|
||||
builder.citizen(
|
||||
dc.CitizenshipStatus.values.byName(status.toUpperCase()));
|
||||
} catch (_) {}
|
||||
}
|
||||
if (data.containsKey('uscisNumber')) builder.uscis(data['uscisNumber'] as String?);
|
||||
if (data.containsKey('passportNumber')) builder.passportNumber(data['passportNumber'] as String?);
|
||||
if (data.containsKey('countryIssuance')) builder.countryIssue(data['countryIssuance'] as String?);
|
||||
if (data.containsKey('preparerUsed')) builder.prepartorOrTranslator(data['preparerUsed'] as bool?);
|
||||
if (data.containsKey('signature')) builder.signature(data['signature'] as String?);
|
||||
if (data.containsKey('uscisNumber'))
|
||||
builder.uscis(data['uscisNumber'] as String?);
|
||||
if (data.containsKey('passportNumber'))
|
||||
builder.passportNumber(data['passportNumber'] as String?);
|
||||
if (data.containsKey('countryIssuance'))
|
||||
builder.countryIssue(data['countryIssuance'] as String?);
|
||||
if (data.containsKey('preparerUsed'))
|
||||
builder.prepartorOrTranslator(data['preparerUsed'] as bool?);
|
||||
if (data.containsKey('signature'))
|
||||
builder.signature(data['signature'] as String?);
|
||||
// Note: admissionNumber not in builder based on file read
|
||||
}
|
||||
|
||||
void _mapW4Fields(dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
||||
void _mapW4Fields(
|
||||
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
||||
if (data.containsKey('cityStateZip')) {
|
||||
final String csz = data['cityStateZip'] as String;
|
||||
// Extremely basic split: City, State Zip
|
||||
final List<String> parts = csz.split(',');
|
||||
if (parts.length >= 2) {
|
||||
builder.city(parts[0].trim());
|
||||
final String stateZip = parts[1].trim();
|
||||
final List<String> szParts = stateZip.split(' ');
|
||||
if (szParts.isNotEmpty) builder.state(szParts[0]);
|
||||
if (szParts.length > 1) builder.zipCode(szParts.last);
|
||||
}
|
||||
final String csz = data['cityStateZip'] as String;
|
||||
// Extremely basic split: City, State Zip
|
||||
final List<String> parts = csz.split(',');
|
||||
if (parts.length >= 2) {
|
||||
builder.city(parts[0].trim());
|
||||
final String stateZip = parts[1].trim();
|
||||
final List<String> szParts = stateZip.split(' ');
|
||||
if (szParts.isNotEmpty) builder.state(szParts[0]);
|
||||
if (szParts.length > 1) builder.zipCode(szParts.last);
|
||||
}
|
||||
}
|
||||
if (data.containsKey('filingStatus')) {
|
||||
// MARITIAL_STATUS_SINGLE, MARITIAL_STATUS_MARRIED, MARITIAL_STATUS_HEAD
|
||||
try {
|
||||
final String status = data['filingStatus'] as String;
|
||||
// Simple mapping assumptions:
|
||||
if (status.contains('single')) builder.marital(dc.MaritalStatus.SINGLE);
|
||||
else if (status.contains('married')) builder.marital(dc.MaritalStatus.MARRIED);
|
||||
else if (status.contains('head')) builder.marital(dc.MaritalStatus.HEAD);
|
||||
} catch (_) {}
|
||||
try {
|
||||
final String status = data['filingStatus'] as String;
|
||||
// Simple mapping assumptions:
|
||||
if (status.contains('single')) builder.marital(dc.MaritalStatus.SINGLE);
|
||||
else if (status.contains('married'))
|
||||
builder.marital(dc.MaritalStatus.MARRIED);
|
||||
else if (status.contains('head'))
|
||||
builder.marital(dc.MaritalStatus.HEAD);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (data.containsKey('multipleJobs')) builder.multipleJob(data['multipleJobs'] as bool?);
|
||||
if (data.containsKey('qualifyingChildren')) builder.childrens(data['qualifyingChildren'] as int?);
|
||||
if (data.containsKey('otherDependents')) builder.otherDeps(data['otherDependents'] as int?);
|
||||
if (data.containsKey('multipleJobs'))
|
||||
builder.multipleJob(data['multipleJobs'] as bool?);
|
||||
if (data.containsKey('qualifyingChildren'))
|
||||
builder.childrens(data['qualifyingChildren'] as int?);
|
||||
if (data.containsKey('otherDependents'))
|
||||
builder.otherDeps(data['otherDependents'] as int?);
|
||||
if (data.containsKey('otherIncome')) {
|
||||
builder.otherInconme(double.tryParse(data['otherIncome'].toString()));
|
||||
builder.otherInconme(double.tryParse(data['otherIncome'].toString()));
|
||||
}
|
||||
if (data.containsKey('deductions')) {
|
||||
builder.deductions(double.tryParse(data['deductions'].toString()));
|
||||
builder.deductions(double.tryParse(data['deductions'].toString()));
|
||||
}
|
||||
if (data.containsKey('extraWithholding')) {
|
||||
builder.extraWithholding(double.tryParse(data['extraWithholding'].toString()));
|
||||
builder.extraWithholding(
|
||||
double.tryParse(data['extraWithholding'].toString()));
|
||||
}
|
||||
if (data.containsKey('signature')) builder.signature(data['signature'] as String?);
|
||||
if (data.containsKey('signature'))
|
||||
builder.signature(data['signature'] as String?);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -62,7 +63,15 @@ class TaxFormsPage extends StatelessWidget {
|
||||
|
||||
if (state.status == TaxFormsStatus.failure) {
|
||||
return Center(
|
||||
child: Text(state.errorMessage ?? 'Error loading forms'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||
child: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'Error loading forms',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/bank_account_repository.dart';
|
||||
|
||||
/// Implementation of [BankAccountRepository] that integrates with Data Connect.
|
||||
class BankAccountRepositoryImpl implements BankAccountRepository {
|
||||
class BankAccountRepositoryImpl
|
||||
with DataErrorHandler
|
||||
implements BankAccountRepository {
|
||||
/// Creates a [BankAccountRepositoryImpl].
|
||||
const BankAccountRepositoryImpl({
|
||||
required this.dataConnect,
|
||||
@@ -19,60 +21,65 @@ class BankAccountRepositoryImpl implements BankAccountRepository {
|
||||
|
||||
@override
|
||||
Future<List<BankAccount>> getAccounts() async {
|
||||
final String staffId = _getStaffId();
|
||||
|
||||
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
|
||||
result = await dataConnect
|
||||
.getAccountsByOwnerId(ownerId: staffId)
|
||||
.execute();
|
||||
|
||||
return result.data.accounts.map((GetAccountsByOwnerIdAccounts account) {
|
||||
return BankAccountAdapter.fromPrimitives(
|
||||
id: account.id,
|
||||
userId: account.ownerId,
|
||||
bankName: account.bank,
|
||||
accountNumber: account.accountNumber,
|
||||
last4: account.last4,
|
||||
sortCode: account.routeNumber,
|
||||
type: account.type is Known<AccountType> ? (account.type as Known<AccountType>).value.name : null,
|
||||
isPrimary: account.isPrimary,
|
||||
);
|
||||
}).toList();
|
||||
return executeProtected(() async {
|
||||
final String staffId = _getStaffId();
|
||||
|
||||
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
|
||||
result = await dataConnect
|
||||
.getAccountsByOwnerId(ownerId: staffId)
|
||||
.execute();
|
||||
|
||||
return result.data.accounts.map((GetAccountsByOwnerIdAccounts account) {
|
||||
return BankAccountAdapter.fromPrimitives(
|
||||
id: account.id,
|
||||
userId: account.ownerId,
|
||||
bankName: account.bank,
|
||||
accountNumber: account.accountNumber,
|
||||
last4: account.last4,
|
||||
sortCode: account.routeNumber,
|
||||
type: account.type is Known<AccountType> ? (account.type as Known<AccountType>).value.name : null,
|
||||
isPrimary: account.isPrimary,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addAccount(BankAccount account) async {
|
||||
final String staffId = _getStaffId();
|
||||
return executeProtected(() async {
|
||||
final String staffId = _getStaffId();
|
||||
|
||||
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
|
||||
existingAccounts = await dataConnect
|
||||
.getAccountsByOwnerId(ownerId: staffId)
|
||||
.execute();
|
||||
final bool hasAccounts = existingAccounts.data.accounts.isNotEmpty;
|
||||
final bool isPrimary = !hasAccounts;
|
||||
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
|
||||
existingAccounts = await dataConnect
|
||||
.getAccountsByOwnerId(ownerId: staffId)
|
||||
.execute();
|
||||
final bool hasAccounts = existingAccounts.data.accounts.isNotEmpty;
|
||||
final bool isPrimary = !hasAccounts;
|
||||
|
||||
await dataConnect.createAccount(
|
||||
bank: account.bankName,
|
||||
type: AccountType.values.byName(BankAccountAdapter.typeToString(account.type)),
|
||||
last4: _safeLast4(account.last4, account.accountNumber),
|
||||
ownerId: staffId,
|
||||
)
|
||||
.isPrimary(isPrimary)
|
||||
.accountNumber(account.accountNumber)
|
||||
.routeNumber(account.sortCode)
|
||||
.execute();
|
||||
await dataConnect.createAccount(
|
||||
bank: account.bankName,
|
||||
type: AccountType.values.byName(BankAccountAdapter.typeToString(account.type)),
|
||||
last4: _safeLast4(account.last4, account.accountNumber),
|
||||
ownerId: staffId,
|
||||
)
|
||||
.isPrimary(isPrimary)
|
||||
.accountNumber(account.accountNumber)
|
||||
.routeNumber(account.sortCode)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
/// Helper to get the logged-in staff ID.
|
||||
String _getStaffId() {
|
||||
final auth.User? user = firebaseAuth.currentUser;
|
||||
if (user == null) {
|
||||
throw Exception('User not authenticated');
|
||||
throw const NotAuthenticatedException(
|
||||
technicalMessage: 'User not authenticated');
|
||||
}
|
||||
|
||||
final String? staffId = StaffSessionStore.instance.session?.staff?.id;
|
||||
if (staffId == null || staffId.isEmpty) {
|
||||
throw Exception('Staff profile is missing or session not initialized.');
|
||||
throw const ServerException(technicalMessage: 'Staff profile is missing or session not initialized.');
|
||||
}
|
||||
return staffId;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,17 @@ class BankAccountPage extends StatelessWidget {
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
} else if (state.status == BankAccountStatus.error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, BankAccountState state) {
|
||||
@@ -67,7 +78,18 @@ class BankAccountPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (state.status == BankAccountStatus.error) {
|
||||
return Center(child: Text(state.errorMessage ?? 'Error'));
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'Error',
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body1m.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
|
||||
@@ -9,7 +9,9 @@ import 'package:krow_core/core.dart';
|
||||
import '../../domain/repositories/time_card_repository.dart';
|
||||
|
||||
/// Implementation of [TimeCardRepository] using Firebase Data Connect.
|
||||
class TimeCardRepositoryImpl implements TimeCardRepository {
|
||||
class TimeCardRepositoryImpl
|
||||
with dc.DataErrorHandler
|
||||
implements TimeCardRepository {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final firebase.FirebaseAuth _firebaseAuth;
|
||||
|
||||
@@ -22,57 +24,62 @@ class TimeCardRepositoryImpl implements TimeCardRepository {
|
||||
|
||||
Future<String> _getStaffId() async {
|
||||
final firebase.User? user = _firebaseAuth.currentUser;
|
||||
if (user == null) throw Exception('User not authenticated');
|
||||
if (user == null) {
|
||||
throw const NotAuthenticatedException(
|
||||
technicalMessage: 'User not authenticated');
|
||||
}
|
||||
|
||||
final fdc.QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> result =
|
||||
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||
if (result.data.staffs.isEmpty) {
|
||||
throw Exception('Staff profile not found');
|
||||
throw const ServerException(technicalMessage: 'Staff profile not found');
|
||||
}
|
||||
return result.data.staffs.first.id;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TimeCard>> getTimeCards(DateTime month) async {
|
||||
final String staffId = await _getStaffId();
|
||||
// Fetch applications. Limit can be adjusted, assuming 100 is safe for now.
|
||||
final fdc.QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> result =
|
||||
await _dataConnect.getApplicationsByStaffId(staffId: staffId).limit(100).execute();
|
||||
return executeProtected(() async {
|
||||
final String staffId = await _getStaffId();
|
||||
// Fetch applications. Limit can be adjusted, assuming 100 is safe for now.
|
||||
final fdc.QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> result =
|
||||
await _dataConnect.getApplicationsByStaffId(staffId: staffId).limit(100).execute();
|
||||
|
||||
return result.data.applications
|
||||
.where((dc.GetApplicationsByStaffIdApplications app) {
|
||||
final DateTime? shiftDate = app.shift.date == null
|
||||
? null
|
||||
: DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime());
|
||||
if (shiftDate == null) return false;
|
||||
return shiftDate.year == month.year && shiftDate.month == month.month;
|
||||
})
|
||||
.map((dc.GetApplicationsByStaffIdApplications app) {
|
||||
final DateTime shiftDate =
|
||||
DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime());
|
||||
final String startTime = _formatTime(app.checkInTime) ?? _formatTime(app.shift.startTime) ?? '';
|
||||
final String endTime = _formatTime(app.checkOutTime) ?? _formatTime(app.shift.endTime) ?? '';
|
||||
return result.data.applications
|
||||
.where((dc.GetApplicationsByStaffIdApplications app) {
|
||||
final DateTime? shiftDate = app.shift.date == null
|
||||
? null
|
||||
: DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime());
|
||||
if (shiftDate == null) return false;
|
||||
return shiftDate.year == month.year && shiftDate.month == month.month;
|
||||
})
|
||||
.map((dc.GetApplicationsByStaffIdApplications app) {
|
||||
final DateTime shiftDate =
|
||||
DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime());
|
||||
final String startTime = _formatTime(app.checkInTime) ?? _formatTime(app.shift.startTime) ?? '';
|
||||
final String endTime = _formatTime(app.checkOutTime) ?? _formatTime(app.shift.endTime) ?? '';
|
||||
|
||||
// Prefer shiftRole values for pay/hours
|
||||
final double hours = app.shiftRole.hours ?? 0.0;
|
||||
final double rate = app.shiftRole.role.costPerHour;
|
||||
final double pay = app.shiftRole.totalValue ?? 0.0;
|
||||
// Prefer shiftRole values for pay/hours
|
||||
final double hours = app.shiftRole.hours ?? 0.0;
|
||||
final double rate = app.shiftRole.role.costPerHour;
|
||||
final double pay = app.shiftRole.totalValue ?? 0.0;
|
||||
|
||||
return TimeCardAdapter.fromPrimitives(
|
||||
id: app.id,
|
||||
shiftTitle: app.shift.title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
date: shiftDate,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
totalHours: hours,
|
||||
hourlyRate: rate,
|
||||
totalPay: pay,
|
||||
status: app.status.stringValue,
|
||||
location: app.shift.location,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
return TimeCardAdapter.fromPrimitives(
|
||||
id: app.id,
|
||||
shiftTitle: app.shift.title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
date: shiftDate,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
totalHours: hours,
|
||||
hourlyRate: rate,
|
||||
totalPay: pay,
|
||||
status: app.status.stringValue,
|
||||
location: app.shift.location,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
String? _formatTime(fdc.Timestamp? timestamp) {
|
||||
|
||||
@@ -27,6 +27,7 @@ class _TimeCardPageState extends State<TimeCardPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: Scaffold(
|
||||
@@ -49,12 +50,33 @@ class _TimeCardPageState extends State<TimeCardPage> {
|
||||
child: Container(color: UiColors.border, height: 1.0),
|
||||
),
|
||||
),
|
||||
body: BlocBuilder<TimeCardBloc, TimeCardState>(
|
||||
body: BlocConsumer<TimeCardBloc, TimeCardState>(
|
||||
listener: (context, state) {
|
||||
if (state is TimeCardError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
translateErrorKey(state.message),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is TimeCardLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is TimeCardError) {
|
||||
return Center(child: Text('Error: ${state.message}'));
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
translateErrorKey(state.message),
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body1m.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (state is TimeCardLoaded) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../domain/repositories/emergency_contact_repository_interface.dart';
|
||||
///
|
||||
/// This repository delegates data operations to Firebase Data Connect.
|
||||
class EmergencyContactRepositoryImpl
|
||||
with dc.DataErrorHandler
|
||||
implements EmergencyContactRepositoryInterface {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
@@ -20,64 +21,81 @@ class EmergencyContactRepositoryImpl
|
||||
|
||||
Future<String> _getStaffId() async {
|
||||
final user = _firebaseAuth.currentUser;
|
||||
if (user == null) throw Exception('User not authenticated');
|
||||
if (user == null) {
|
||||
throw const NotAuthenticatedException(
|
||||
technicalMessage: 'User not authenticated');
|
||||
}
|
||||
|
||||
final result =
|
||||
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||
if (result.data.staffs.isEmpty) {
|
||||
throw Exception('Staff profile not found');
|
||||
throw const ServerException(technicalMessage: 'Staff profile not found');
|
||||
}
|
||||
return result.data.staffs.first.id;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<EmergencyContact>> getContacts() async {
|
||||
final staffId = await _getStaffId();
|
||||
final result =
|
||||
await _dataConnect.getEmergencyContactsByStaffId(staffId: staffId).execute();
|
||||
return executeProtected(() async {
|
||||
final staffId = await _getStaffId();
|
||||
final result = await _dataConnect
|
||||
.getEmergencyContactsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
return result.data.emergencyContacts.map((dto) {
|
||||
return EmergencyContactAdapter.fromPrimitives(
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
phone: dto.phone,
|
||||
relationship: dto.relationship.stringValue,
|
||||
);
|
||||
}).toList();
|
||||
return result.data.emergencyContacts.map((dto) {
|
||||
return EmergencyContactAdapter.fromPrimitives(
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
phone: dto.phone,
|
||||
relationship: dto.relationship.stringValue,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveContacts(List<EmergencyContact> contacts) async {
|
||||
final staffId = await _getStaffId();
|
||||
return executeProtected(() async {
|
||||
final staffId = await _getStaffId();
|
||||
|
||||
// 1. Get existing to delete
|
||||
final existingResult =
|
||||
await _dataConnect.getEmergencyContactsByStaffId(staffId: staffId).execute();
|
||||
final existingIds =
|
||||
existingResult.data.emergencyContacts.map((e) => e.id).toList();
|
||||
|
||||
// 2. Delete all existing
|
||||
await Future.wait(existingIds.map(
|
||||
(id) => _dataConnect.deleteEmergencyContact(id: id).execute()));
|
||||
|
||||
// 3. Create new
|
||||
await Future.wait(contacts.map((contact) {
|
||||
dc.RelationshipType rel = dc.RelationshipType.OTHER;
|
||||
switch(contact.relationship) {
|
||||
case RelationshipType.family: rel = dc.RelationshipType.FAMILY; break;
|
||||
case RelationshipType.spouse: rel = dc.RelationshipType.SPOUSE; break;
|
||||
case RelationshipType.friend: rel = dc.RelationshipType.FRIEND; break;
|
||||
case RelationshipType.other: rel = dc.RelationshipType.OTHER; break;
|
||||
}
|
||||
|
||||
return _dataConnect
|
||||
.createEmergencyContact(
|
||||
name: contact.name,
|
||||
phone: contact.phone,
|
||||
relationship: rel,
|
||||
staffId: staffId,
|
||||
)
|
||||
// 1. Get existing to delete
|
||||
final existingResult = await _dataConnect
|
||||
.getEmergencyContactsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
}));
|
||||
final existingIds =
|
||||
existingResult.data.emergencyContacts.map((e) => e.id).toList();
|
||||
|
||||
// 2. Delete all existing
|
||||
await Future.wait(existingIds.map(
|
||||
(id) => _dataConnect.deleteEmergencyContact(id: id).execute()));
|
||||
|
||||
// 3. Create new
|
||||
await Future.wait(contacts.map((contact) {
|
||||
dc.RelationshipType rel = dc.RelationshipType.OTHER;
|
||||
switch (contact.relationship) {
|
||||
case RelationshipType.family:
|
||||
rel = dc.RelationshipType.FAMILY;
|
||||
break;
|
||||
case RelationshipType.spouse:
|
||||
rel = dc.RelationshipType.SPOUSE;
|
||||
break;
|
||||
case RelationshipType.friend:
|
||||
rel = dc.RelationshipType.FRIEND;
|
||||
break;
|
||||
case RelationshipType.other:
|
||||
rel = dc.RelationshipType.OTHER;
|
||||
break;
|
||||
}
|
||||
|
||||
return _dataConnect
|
||||
.createEmergencyContact(
|
||||
name: contact.name,
|
||||
phone: contact.phone,
|
||||
relationship: rel,
|
||||
staffId: staffId,
|
||||
)
|
||||
.execute();
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -8,7 +9,6 @@ import '../widgets/emergency_contact_form_item.dart';
|
||||
import '../widgets/emergency_contact_info_banner.dart';
|
||||
import '../widgets/emergency_contact_save_button.dart';
|
||||
|
||||
|
||||
/// The Staff Emergency Contact screen.
|
||||
///
|
||||
/// This screen allows staff to manage their emergency contacts during onboarding.
|
||||
@@ -19,6 +19,7 @@ class EmergencyContactScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Translations.of(context); // Force rebuild on locale change
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
@@ -38,10 +39,18 @@ class EmergencyContactScreen extends StatelessWidget {
|
||||
body: BlocProvider(
|
||||
create: (context) => Modular.get<EmergencyContactBloc>(),
|
||||
child: BlocConsumer<EmergencyContactBloc, EmergencyContactState>(
|
||||
|
||||
listener: (context, state) {
|
||||
if (state.status == EmergencyContactStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'An error occurred')),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,13 +2,17 @@ import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import '../../domain/repositories/experience_repository_interface.dart';
|
||||
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Implementation of [ExperienceRepositoryInterface] that delegates to Data Connect.
|
||||
class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
|
||||
class ExperienceRepositoryImpl
|
||||
with dc.DataErrorHandler
|
||||
implements ExperienceRepositoryInterface {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
// ignore: unused_field
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
|
||||
/// Creates a [ExperienceRepositoryImpl] using Da a Connect and Auth.
|
||||
/// Creates a [ExperienceRepositoryImpl] using Data Connect and Auth.
|
||||
ExperienceRepositoryImpl({
|
||||
required dc.ExampleConnector dataConnect,
|
||||
required FirebaseAuth firebaseAuth,
|
||||
@@ -17,26 +21,33 @@ class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
|
||||
|
||||
Future<dc.GetStaffByUserIdStaffs> _getStaff() async {
|
||||
final user = _firebaseAuth.currentUser;
|
||||
if (user == null) throw Exception('User not authenticated');
|
||||
if (user == null) {
|
||||
throw const NotAuthenticatedException(
|
||||
technicalMessage: 'User not authenticated');
|
||||
}
|
||||
|
||||
final result =
|
||||
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||
if (result.data.staffs.isEmpty) {
|
||||
throw Exception('Staff profile not found');
|
||||
throw const ServerException(technicalMessage: 'Staff profile not found');
|
||||
}
|
||||
return result.data.staffs.first;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>> getIndustries() async {
|
||||
final staff = await _getStaff();
|
||||
return staff.industries ?? [];
|
||||
return executeProtected(() async {
|
||||
final staff = await _getStaff();
|
||||
return staff.industries ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>> getSkills() async {
|
||||
final staff = await _getStaff();
|
||||
return staff.skills ?? [];
|
||||
return executeProtected(() async {
|
||||
final staff = await _getStaff();
|
||||
return staff.skills ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -44,15 +55,13 @@ class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
|
||||
List<String> industries,
|
||||
List<String> skills,
|
||||
) async {
|
||||
try {
|
||||
final staff = await _getStaff();
|
||||
await _dataConnect
|
||||
.updateStaff(id: staff.id)
|
||||
.industries(industries)
|
||||
.skills(skills)
|
||||
.execute();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to save experience: $e');
|
||||
}
|
||||
return executeProtected(() async {
|
||||
final staff = await _getStaff();
|
||||
await _dataConnect
|
||||
.updateStaff(id: staff.id)
|
||||
.industries(industries)
|
||||
.skills(skills)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class ExperiencePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = t.staff.onboarding.experience;
|
||||
final i18n = Translations.of(context).staff.onboarding.experience;
|
||||
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(
|
||||
@@ -64,7 +64,14 @@ class ExperiencePage extends StatelessWidget {
|
||||
Modular.to.pop();
|
||||
} else if (state.status == ExperienceStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'An error occurred')),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,7 +13,9 @@ import '../../domain/repositories/personal_info_repository_interface.dart';
|
||||
/// - Delegating all data access to the data_connect layer
|
||||
/// - Mapping between data_connect DTOs and domain entities
|
||||
/// - Containing no business logic
|
||||
class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
class PersonalInfoRepositoryImpl
|
||||
with DataErrorHandler
|
||||
implements PersonalInfoRepositoryInterface {
|
||||
|
||||
/// Creates a [PersonalInfoRepositoryImpl].
|
||||
///
|
||||
@@ -28,58 +30,63 @@ class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
|
||||
@override
|
||||
Future<Staff> getStaffProfile() async {
|
||||
final firebase_auth.User? user = _firebaseAuth.currentUser;
|
||||
if (user == null) {
|
||||
throw Exception('User not authenticated');
|
||||
}
|
||||
return executeProtected(() async {
|
||||
final firebase_auth.User? user = _firebaseAuth.currentUser;
|
||||
if (user == null) {
|
||||
throw NotAuthenticatedException(
|
||||
technicalMessage: 'User not authenticated');
|
||||
}
|
||||
|
||||
// Query staff data from Firebase Data Connect
|
||||
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables> result =
|
||||
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||
// Query staff data from Firebase Data Connect
|
||||
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables> result =
|
||||
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||
|
||||
if (result.data.staffs.isEmpty) {
|
||||
throw Exception('Staff profile not found for User ID: ${user.uid}');
|
||||
}
|
||||
if (result.data.staffs.isEmpty) {
|
||||
throw const ServerException(technicalMessage: 'Staff profile not found');
|
||||
}
|
||||
|
||||
final GetStaffByUserIdStaffs rawStaff = result.data.staffs.first;
|
||||
final GetStaffByUserIdStaffs rawStaff = result.data.staffs.first;
|
||||
|
||||
// Map from data_connect DTO to domain entity
|
||||
return _mapToStaffEntity(rawStaff);
|
||||
// Map from data_connect DTO to domain entity
|
||||
return _mapToStaffEntity(rawStaff);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Staff> updateStaffProfile({required String staffId, required Map<String, dynamic> data}) async {
|
||||
// Start building the update mutation
|
||||
UpdateStaffVariablesBuilder updateBuilder = _dataConnect.updateStaff(id: staffId);
|
||||
return executeProtected(() async {
|
||||
// Start building the update mutation
|
||||
UpdateStaffVariablesBuilder updateBuilder = _dataConnect.updateStaff(id: staffId);
|
||||
|
||||
// Apply updates from map if present
|
||||
if (data.containsKey('name')) {
|
||||
updateBuilder = updateBuilder.fullName(data['name'] as String);
|
||||
}
|
||||
if (data.containsKey('email')) {
|
||||
updateBuilder = updateBuilder.email(data['email'] as String);
|
||||
}
|
||||
if (data.containsKey('phone')) {
|
||||
updateBuilder = updateBuilder.phone(data['phone'] as String?);
|
||||
}
|
||||
if (data.containsKey('avatar')) {
|
||||
updateBuilder = updateBuilder.photoUrl(data['avatar'] as String?);
|
||||
}
|
||||
if (data.containsKey('preferredLocations')) {
|
||||
// After schema update and SDK regeneration, preferredLocations accepts List<String>
|
||||
updateBuilder = updateBuilder.preferredLocations(data['preferredLocations'] as List<String>);
|
||||
}
|
||||
// Apply updates from map if present
|
||||
if (data.containsKey('name')) {
|
||||
updateBuilder = updateBuilder.fullName(data['name'] as String);
|
||||
}
|
||||
if (data.containsKey('email')) {
|
||||
updateBuilder = updateBuilder.email(data['email'] as String);
|
||||
}
|
||||
if (data.containsKey('phone')) {
|
||||
updateBuilder = updateBuilder.phone(data['phone'] as String?);
|
||||
}
|
||||
if (data.containsKey('avatar')) {
|
||||
updateBuilder = updateBuilder.photoUrl(data['avatar'] as String?);
|
||||
}
|
||||
if (data.containsKey('preferredLocations')) {
|
||||
// After schema update and SDK regeneration, preferredLocations accepts List<String>
|
||||
updateBuilder = updateBuilder.preferredLocations(data['preferredLocations'] as List<String>);
|
||||
}
|
||||
|
||||
// Execute the update
|
||||
final OperationResult<UpdateStaffData, UpdateStaffVariables> result =
|
||||
await updateBuilder.execute();
|
||||
// Execute the update
|
||||
final OperationResult<UpdateStaffData, UpdateStaffVariables> result =
|
||||
await updateBuilder.execute();
|
||||
|
||||
if (result.data.staff_update == null) {
|
||||
throw Exception('Failed to update staff profile');
|
||||
}
|
||||
if (result.data.staff_update == null) {
|
||||
throw const ServerException(technicalMessage: 'Failed to update staff profile');
|
||||
}
|
||||
|
||||
// Fetch the updated staff profile to return complete entity
|
||||
return getStaffProfile();
|
||||
// Fetch the updated staff profile to return complete entity
|
||||
return getStaffProfile();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -23,7 +23,7 @@ class PersonalInfoPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
|
||||
final i18n = Translations.of(context).staff.onboarding.personal_info;
|
||||
return BlocProvider<PersonalInfoBloc>(
|
||||
create: (BuildContext context) => Modular.get<PersonalInfoBloc>(),
|
||||
child: BlocListener<PersonalInfoBloc, PersonalInfoState>(
|
||||
@@ -39,8 +39,12 @@ class PersonalInfoPage extends StatelessWidget {
|
||||
} else if (state.status == PersonalInfoStatus.error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'An error occurred'),
|
||||
backgroundColor: UiColors.destructive,
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user