feat: complete centralized error handling system with documentation
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user