feat: Centralized Error Handling & Crash Fixes
This commit is contained in:
@@ -12,7 +12,9 @@ import 'package:krow_domain/krow_domain.dart'
|
||||
AccountExistsException,
|
||||
UserNotFoundException,
|
||||
UnauthorizedAppException,
|
||||
PasswordMismatchException;
|
||||
UnauthorizedAppException,
|
||||
PasswordMismatchException,
|
||||
NetworkException;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
|
||||
import '../../domain/repositories/auth_repository_interface.dart';
|
||||
@@ -63,6 +65,10 @@ class AuthRepositoryImpl
|
||||
throw InvalidCredentialsException(
|
||||
technicalMessage: 'Firebase error code: ${e.code}',
|
||||
);
|
||||
} else if (e.code == 'network-request-failed') {
|
||||
throw NetworkException(
|
||||
technicalMessage: 'Firebase: ${e.message}',
|
||||
);
|
||||
} else {
|
||||
throw SignInFailedException(
|
||||
technicalMessage: 'Firebase auth error: ${e.message}',
|
||||
@@ -120,6 +126,10 @@ class AuthRepositoryImpl
|
||||
password: password,
|
||||
companyName: companyName,
|
||||
);
|
||||
} else if (e.code == 'network-request-failed') {
|
||||
throw NetworkException(
|
||||
technicalMessage: 'Firebase: ${e.message}',
|
||||
);
|
||||
} else {
|
||||
throw SignUpFailedException(
|
||||
technicalMessage: 'Firebase auth error: ${e.message}',
|
||||
|
||||
@@ -47,8 +47,11 @@ class ClientSignInPage extends StatelessWidget {
|
||||
final String errorMessage = state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: t.errors.generic.unknown;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(errorMessage)),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: errorMessage,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 120, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -51,8 +51,11 @@ class ClientSignUpPage extends StatelessWidget {
|
||||
final String errorMessage = state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: t.errors.generic.unknown;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(errorMessage)),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: errorMessage,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 120, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,9 +47,11 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
|
||||
|
||||
void _handleSubmit() {
|
||||
if (_passwordController.text != _confirmPasswordController.text) {
|
||||
ScaffoldMessenger.of(
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Passwords do not match')));
|
||||
message: translateErrorKey('passwords_dont_match'),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,13 +71,26 @@ class _BillingViewState extends State<BillingView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<BillingBloc, BillingState>(
|
||||
return BlocConsumer<BillingBloc, BillingState>(
|
||||
listener: (BuildContext context, BillingState state) {
|
||||
if (state.status == BillingStatus.failure && state.errorMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, BillingState state) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: <Widget>[
|
||||
SliverAppBar(
|
||||
// ... (APP BAR CODE REMAINS UNCHANGED, BUT I MUST INCLUDE IT OR CHUNK IT CORRECTLY)
|
||||
// Since I cannot see the headers in this chunk, I will target the _buildContent method instead
|
||||
// to avoid messing up the whole file structure.
|
||||
// Wait, I can just replace the build method wrapper.
|
||||
pinned: true,
|
||||
expandedHeight: 200.0,
|
||||
backgroundColor: UiColors.primary,
|
||||
@@ -180,17 +193,28 @@ class _BillingViewState extends State<BillingView> {
|
||||
}
|
||||
|
||||
if (state.status == BillingStatus.failure) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(translateErrorKey(state.errorMessage!))),
|
||||
);
|
||||
});
|
||||
return Center(
|
||||
child: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
style: UiTypography.body1r.textError,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space8),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.error, size: 48, color: UiColors.error),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
style: UiTypography.body1m.textError,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiButton.secondary(
|
||||
text: 'Retry',
|
||||
onPressed: () => BlocProvider.of<BillingBloc>(context).add(const BillingLoadStarted()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,43 +23,56 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
||||
/// Fetches shifts for a specific date.
|
||||
@override
|
||||
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
|
||||
final String? businessId =
|
||||
dc.ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return <CoverageShift>[];
|
||||
try {
|
||||
final String? businessId =
|
||||
dc.ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return <CoverageShift>[];
|
||||
}
|
||||
|
||||
final DateTime start = DateTime(date.year, date.month, date.day);
|
||||
final DateTime end =
|
||||
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
|
||||
await _dataConnect
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _toTimestamp(start),
|
||||
end: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListStaffsApplicationsByBusinessForDayData,
|
||||
dc.ListStaffsApplicationsByBusinessForDayVariables> applicationsResult =
|
||||
await _dataConnect
|
||||
.listStaffsApplicationsByBusinessForDay(
|
||||
businessId: businessId,
|
||||
dayStart: _toTimestamp(start),
|
||||
dayEnd: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return _mapCoverageShifts(
|
||||
shiftRolesResult.data.shiftRoles,
|
||||
applicationsResult.data.applications,
|
||||
date,
|
||||
);
|
||||
} catch (e) {
|
||||
final String error = e.toString().toLowerCase();
|
||||
if (error.contains('network') ||
|
||||
error.contains('connection') ||
|
||||
error.contains('unavailable') ||
|
||||
error.contains('offline') ||
|
||||
error.contains('socket') ||
|
||||
error.contains('failed host lookup')) {
|
||||
throw NetworkException(technicalMessage: 'Coverage fetch failed: $e');
|
||||
}
|
||||
throw ServerException(technicalMessage: 'Coverage fetch failed: $e');
|
||||
}
|
||||
|
||||
final DateTime start = DateTime(date.year, date.month, date.day);
|
||||
final DateTime end =
|
||||
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
|
||||
await _dataConnect
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _toTimestamp(start),
|
||||
end: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListStaffsApplicationsByBusinessForDayData,
|
||||
dc.ListStaffsApplicationsByBusinessForDayVariables> applicationsResult =
|
||||
await _dataConnect
|
||||
.listStaffsApplicationsByBusinessForDay(
|
||||
businessId: businessId,
|
||||
dayStart: _toTimestamp(start),
|
||||
dayEnd: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return _mapCoverageShifts(
|
||||
shiftRolesResult.data.shiftRoles,
|
||||
applicationsResult.data.applications,
|
||||
date,
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetches coverage statistics for a specific date.
|
||||
@@ -180,6 +193,7 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
||||
case dc.ApplicationStatus.REJECTED:
|
||||
return CoverageWorkerStatus.rejected;
|
||||
case dc.ApplicationStatus.CONFIRMED:
|
||||
case dc.ApplicationStatus.ACCEPTED:
|
||||
return CoverageWorkerStatus.confirmed;
|
||||
case dc.ApplicationStatus.CHECKED_IN:
|
||||
return CoverageWorkerStatus.checkedIn;
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import '../blocs/coverage_bloc.dart';
|
||||
import '../blocs/coverage_event.dart';
|
||||
import '../blocs/coverage_state.dart';
|
||||
@@ -57,7 +58,16 @@ class _CoveragePageState extends State<CoveragePage> {
|
||||
create: (BuildContext context) => Modular.get<CoverageBloc>()
|
||||
..add(CoverageLoadRequested(date: DateTime.now())),
|
||||
child: Scaffold(
|
||||
body: BlocBuilder<CoverageBloc, CoverageState>(
|
||||
body: BlocConsumer<CoverageBloc, CoverageState>(
|
||||
listener: (BuildContext context, CoverageState state) {
|
||||
if (state.status == CoverageStatus.failure && state.errorMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, CoverageState state) {
|
||||
final DateTime selectedDate = state.selectedDate ?? DateTime.now();
|
||||
|
||||
@@ -226,43 +236,45 @@ class _CoveragePageState extends State<CoveragePage> {
|
||||
required BuildContext context,
|
||||
required CoverageState state,
|
||||
}) {
|
||||
if (state.status == CoverageStatus.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
if (state.shifts.isEmpty) {
|
||||
if (state.status == CoverageStatus.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == CoverageStatus.failure) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.warning,
|
||||
size: UiConstants.space12,
|
||||
color: UiColors.destructive,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Failed to load coverage data',
|
||||
style: UiTypography.title2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
if (state.status == CoverageStatus.failure) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.error,
|
||||
size: 48,
|
||||
color: UiColors.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
state.errorMessage ?? 'An unknown error occurred',
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.mutedForeground,
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
style: UiTypography.body1m.textError,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiButton.secondary(
|
||||
text: 'Retry',
|
||||
onPressed: () => BlocProvider.of<CoverageBloc>(context).add(
|
||||
const CoverageRefreshRequested(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'one_time_order_state.dart';
|
||||
|
||||
/// BLoC for managing the multi-step one-time order creation form.
|
||||
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
with BlocErrorHandler<OneTimeOrderState> {
|
||||
with BlocErrorHandler<OneTimeOrderState>, SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||
OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._dataConnect)
|
||||
: super(OneTimeOrderState.initial()) {
|
||||
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
|
||||
|
||||
@@ -71,6 +71,20 @@ class OneTimeOrderState extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
bool get isValid {
|
||||
return eventName.isNotEmpty &&
|
||||
selectedVendor != null &&
|
||||
selectedHub != null &&
|
||||
positions.isNotEmpty &&
|
||||
positions.every(
|
||||
(OneTimeOrderPosition p) =>
|
||||
p.role.isNotEmpty &&
|
||||
p.count > 0 &&
|
||||
p.startTime.isNotEmpty &&
|
||||
p.endTime.isNotEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
date,
|
||||
|
||||
@@ -25,7 +25,18 @@ class OneTimeOrderView extends StatelessWidget {
|
||||
final TranslationsClientCreateOrderOneTimeEn labels =
|
||||
t.client_create_order.one_time;
|
||||
|
||||
return BlocBuilder<OneTimeOrderBloc, OneTimeOrderState>(
|
||||
return BlocConsumer<OneTimeOrderBloc, OneTimeOrderState>(
|
||||
listener: (BuildContext context, OneTimeOrderState state) {
|
||||
if (state.status == OneTimeOrderStatus.failure &&
|
||||
state.errorMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, OneTimeOrderState state) {
|
||||
if (state.status == OneTimeOrderStatus.success) {
|
||||
return OneTimeOrderSuccessView(
|
||||
@@ -104,9 +115,11 @@ class OneTimeOrderView extends StatelessWidget {
|
||||
? labels.creating
|
||||
: labels.create_order,
|
||||
isLoading: state.status == OneTimeOrderStatus.loading,
|
||||
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(const OneTimeOrderSubmitted()),
|
||||
onPressed: state.isValid
|
||||
? () => BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(const OneTimeOrderSubmitted())
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -286,7 +299,7 @@ class _BottomActionButton extends StatelessWidget {
|
||||
this.isLoading = false,
|
||||
});
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback? onPressed;
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
|
||||
@@ -15,92 +15,103 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
|
||||
|
||||
@override
|
||||
Future<HomeDashboardData> getDashboardData() async {
|
||||
final String? businessId = dc.ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return const HomeDashboardData(
|
||||
weeklySpending: 0,
|
||||
next7DaysSpending: 0,
|
||||
weeklyShifts: 0,
|
||||
next7DaysScheduled: 0,
|
||||
totalNeeded: 0,
|
||||
totalFilled: 0,
|
||||
try {
|
||||
final String? businessId = dc.ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return const HomeDashboardData(
|
||||
weeklySpending: 0,
|
||||
next7DaysSpending: 0,
|
||||
weeklyShifts: 0,
|
||||
next7DaysScheduled: 0,
|
||||
totalNeeded: 0,
|
||||
totalFilled: 0,
|
||||
);
|
||||
}
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final int daysFromMonday = now.weekday - DateTime.monday;
|
||||
final DateTime monday =
|
||||
DateTime(now.year, now.month, now.day).subtract(Duration(days: daysFromMonday));
|
||||
final DateTime weekRangeStart = DateTime(monday.year, monday.month, monday.day);
|
||||
final DateTime weekRangeEnd =
|
||||
DateTime(monday.year, monday.month, monday.day + 13, 23, 59, 59, 999);
|
||||
final fdc.QueryResult<
|
||||
dc.GetCompletedShiftsByBusinessIdData,
|
||||
dc.GetCompletedShiftsByBusinessIdVariables> completedResult =
|
||||
await _dataConnect
|
||||
.getCompletedShiftsByBusinessId(
|
||||
businessId: businessId,
|
||||
dateFrom: _toTimestamp(weekRangeStart),
|
||||
dateTo: _toTimestamp(weekRangeEnd),
|
||||
)
|
||||
.execute();
|
||||
|
||||
double weeklySpending = 0.0;
|
||||
double next7DaysSpending = 0.0;
|
||||
int weeklyShifts = 0;
|
||||
int next7DaysScheduled = 0;
|
||||
for (final dc.GetCompletedShiftsByBusinessIdShifts shift
|
||||
in completedResult.data.shifts) {
|
||||
final DateTime? shiftDate = shift.date?.toDateTime();
|
||||
if (shiftDate == null) {
|
||||
continue;
|
||||
}
|
||||
final int offset = shiftDate.difference(weekRangeStart).inDays;
|
||||
if (offset < 0 || offset > 13) {
|
||||
continue;
|
||||
}
|
||||
final double cost = shift.cost ?? 0.0;
|
||||
if (offset <= 6) {
|
||||
weeklySpending += cost;
|
||||
weeklyShifts += 1;
|
||||
} else {
|
||||
next7DaysSpending += cost;
|
||||
next7DaysScheduled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
final DateTime start = DateTime(now.year, now.month, now.day);
|
||||
final DateTime end = DateTime(now.year, now.month, now.day, 23, 59, 59, 999);
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables> result =
|
||||
await _dataConnect
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _toTimestamp(start),
|
||||
end: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
int totalNeeded = 0;
|
||||
int totalFilled = 0;
|
||||
for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole
|
||||
in result.data.shiftRoles) {
|
||||
totalNeeded += shiftRole.count;
|
||||
totalFilled += shiftRole.assigned ?? 0;
|
||||
}
|
||||
|
||||
return HomeDashboardData(
|
||||
weeklySpending: weeklySpending,
|
||||
next7DaysSpending: next7DaysSpending,
|
||||
weeklyShifts: weeklyShifts,
|
||||
next7DaysScheduled: next7DaysScheduled,
|
||||
totalNeeded: totalNeeded,
|
||||
totalFilled: totalFilled,
|
||||
);
|
||||
}
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final int daysFromMonday = now.weekday - DateTime.monday;
|
||||
final DateTime monday =
|
||||
DateTime(now.year, now.month, now.day).subtract(Duration(days: daysFromMonday));
|
||||
final DateTime weekRangeStart = DateTime(monday.year, monday.month, monday.day);
|
||||
final DateTime weekRangeEnd =
|
||||
DateTime(monday.year, monday.month, monday.day + 13, 23, 59, 59, 999);
|
||||
final fdc.QueryResult<
|
||||
dc.GetCompletedShiftsByBusinessIdData,
|
||||
dc.GetCompletedShiftsByBusinessIdVariables> completedResult =
|
||||
await _dataConnect
|
||||
.getCompletedShiftsByBusinessId(
|
||||
businessId: businessId,
|
||||
dateFrom: _toTimestamp(weekRangeStart),
|
||||
dateTo: _toTimestamp(weekRangeEnd),
|
||||
)
|
||||
.execute();
|
||||
|
||||
|
||||
double weeklySpending = 0.0;
|
||||
double next7DaysSpending = 0.0;
|
||||
int weeklyShifts = 0;
|
||||
int next7DaysScheduled = 0;
|
||||
for (final dc.GetCompletedShiftsByBusinessIdShifts shift
|
||||
in completedResult.data.shifts) {
|
||||
final DateTime? shiftDate = shift.date?.toDateTime();
|
||||
if (shiftDate == null) {
|
||||
continue;
|
||||
}
|
||||
final int offset = shiftDate.difference(weekRangeStart).inDays;
|
||||
if (offset < 0 || offset > 13) {
|
||||
continue;
|
||||
}
|
||||
final double cost = shift.cost ?? 0.0;
|
||||
if (offset <= 6) {
|
||||
weeklySpending += cost;
|
||||
weeklyShifts += 1;
|
||||
} else {
|
||||
next7DaysSpending += cost;
|
||||
next7DaysScheduled += 1;
|
||||
} catch (e) {
|
||||
final String error = e.toString().toLowerCase();
|
||||
if (error.contains('network') ||
|
||||
error.contains('connection') ||
|
||||
error.contains('unavailable') ||
|
||||
error.contains('offline') ||
|
||||
error.contains('socket') ||
|
||||
error.contains('failed host lookup')) {
|
||||
throw NetworkException(technicalMessage: 'Home dashboard fetch failed: $e');
|
||||
}
|
||||
throw ServerException(technicalMessage: 'Home dashboard fetch failed: $e');
|
||||
}
|
||||
|
||||
final DateTime start = DateTime(now.year, now.month, now.day);
|
||||
final DateTime end = DateTime(now.year, now.month, now.day, 23, 59, 59, 999);
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables> result =
|
||||
await _dataConnect
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _toTimestamp(start),
|
||||
end: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
|
||||
int totalNeeded = 0;
|
||||
int totalFilled = 0;
|
||||
for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole
|
||||
in result.data.shiftRoles) {
|
||||
totalNeeded += shiftRole.count;
|
||||
totalFilled += shiftRole.assigned ?? 0;
|
||||
}
|
||||
|
||||
return HomeDashboardData(
|
||||
weeklySpending: weeklySpending,
|
||||
next7DaysSpending: next7DaysSpending,
|
||||
weeklyShifts: weeklyShifts,
|
||||
next7DaysScheduled: next7DaysScheduled,
|
||||
totalNeeded: totalNeeded,
|
||||
totalFilled: totalFilled,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -114,46 +125,57 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
|
||||
|
||||
@override
|
||||
Future<List<ReorderItem>> getRecentReorders() async {
|
||||
final String? businessId = dc.ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return const <ReorderItem>[];
|
||||
try {
|
||||
final String? businessId = dc.ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return const <ReorderItem>[];
|
||||
}
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime start = now.subtract(const Duration(days: 30));
|
||||
final fdc.Timestamp startTimestamp = _toTimestamp(start);
|
||||
final fdc.Timestamp endTimestamp = _toTimestamp(now);
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersData,
|
||||
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersVariables> result =
|
||||
await _dataConnect.listShiftRolesByBusinessDateRangeCompletedOrders(
|
||||
businessId: businessId,
|
||||
start: startTimestamp,
|
||||
end: endTimestamp,
|
||||
).execute();
|
||||
|
||||
return result.data.shiftRoles.map((
|
||||
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersShiftRoles shiftRole,
|
||||
) {
|
||||
|
||||
final String location =
|
||||
shiftRole.shift.location ??
|
||||
shiftRole.shift.locationAddress ??
|
||||
'';
|
||||
final String type = shiftRole.shift.order.orderType.stringValue;
|
||||
return ReorderItem(
|
||||
orderId: shiftRole.shift.order.id,
|
||||
title: '${shiftRole.role.name} - ${shiftRole.shift.title}',
|
||||
location: location,
|
||||
hourlyRate: shiftRole.role.costPerHour,
|
||||
hours: shiftRole.hours ?? 0,
|
||||
workers: shiftRole.count,
|
||||
type: type,
|
||||
);
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
final String error = e.toString().toLowerCase();
|
||||
if (error.contains('network') ||
|
||||
error.contains('connection') ||
|
||||
error.contains('unavailable') ||
|
||||
error.contains('offline') ||
|
||||
error.contains('socket') ||
|
||||
error.contains('failed host lookup')) {
|
||||
throw NetworkException(technicalMessage: 'Home reorders fetch failed: $e');
|
||||
}
|
||||
throw ServerException(technicalMessage: 'Home reorders fetch failed: $e');
|
||||
}
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime start = now.subtract(const Duration(days: 30));
|
||||
final fdc.Timestamp startTimestamp = _toTimestamp(start);
|
||||
final fdc.Timestamp endTimestamp = _toTimestamp(now);
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersData,
|
||||
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersVariables> result =
|
||||
await _dataConnect.listShiftRolesByBusinessDateRangeCompletedOrders(
|
||||
businessId: businessId,
|
||||
start: startTimestamp,
|
||||
end: endTimestamp,
|
||||
).execute();
|
||||
|
||||
|
||||
|
||||
return result.data.shiftRoles.map((
|
||||
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersShiftRoles shiftRole,
|
||||
) {
|
||||
|
||||
final String location =
|
||||
shiftRole.shift.location ??
|
||||
shiftRole.shift.locationAddress ??
|
||||
'';
|
||||
final String type = shiftRole.shift.order.orderType.stringValue;
|
||||
return ReorderItem(
|
||||
orderId: shiftRole.shift.order.id,
|
||||
title: '${shiftRole.role.name} - ${shiftRole.shift.title}',
|
||||
location: location,
|
||||
hourlyRate: shiftRole.role.costPerHour,
|
||||
hours: shiftRole.hours ?? 0,
|
||||
workers: shiftRole.count,
|
||||
type: type,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
fdc.Timestamp _toTimestamp(DateTime date) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'client_home_state.dart';
|
||||
|
||||
/// BLoC responsible for managing the state and business logic of the client home dashboard.
|
||||
class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState>
|
||||
with BlocErrorHandler<ClientHomeState> {
|
||||
with BlocErrorHandler<ClientHomeState>, SafeBloc<ClientHomeEvent, ClientHomeState> {
|
||||
ClientHomeBloc({
|
||||
required GetDashboardDataUseCase getDashboardDataUseCase,
|
||||
required GetRecentReordersUseCase getRecentReordersUseCase,
|
||||
|
||||
@@ -32,8 +32,21 @@ class ClientHomePage extends StatelessWidget {
|
||||
ClientHomeHeader(i18n: i18n),
|
||||
ClientHomeEditBanner(i18n: i18n),
|
||||
Flexible(
|
||||
child: BlocBuilder<ClientHomeBloc, ClientHomeState>(
|
||||
child: BlocConsumer<ClientHomeBloc, ClientHomeState>(
|
||||
listener: (BuildContext context, ClientHomeState state) {
|
||||
if (state.status == ClientHomeStatus.error &&
|
||||
state.errorMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, ClientHomeState state) {
|
||||
if (state.status == ClientHomeStatus.error) {
|
||||
return _buildErrorState(context, state);
|
||||
}
|
||||
if (state.isEditMode) {
|
||||
return _buildEditModeList(context, state);
|
||||
}
|
||||
@@ -108,4 +121,33 @@ class ClientHomePage extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(BuildContext context, ClientHomeState state) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.error,
|
||||
size: 48,
|
||||
color: UiColors.error,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
style: UiTypography.body1m.textError,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiButton.secondary(
|
||||
text: 'Retry',
|
||||
onPressed: () =>
|
||||
BlocProvider.of<ClientHomeBloc>(context).add(ClientHomeStarted()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,18 +34,21 @@ class ClientHubsPage extends StatelessWidget {
|
||||
},
|
||||
listener: (BuildContext context, ClientHubsState state) {
|
||||
if (state.errorMessage != null && state.errorMessage!.isNotEmpty) {
|
||||
final String errorMessage = translateErrorKey(state.errorMessage!);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(errorMessage)),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
BlocProvider.of<ClientHubsBloc>(
|
||||
context,
|
||||
).add(const ClientHubsMessageCleared());
|
||||
}
|
||||
if (state.successMessage != null && state.successMessage!.isNotEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(state.successMessage!)));
|
||||
message: state.successMessage!,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
BlocProvider.of<ClientHubsBloc>(
|
||||
context,
|
||||
).add(const ClientHubsMessageCleared());
|
||||
|
||||
@@ -52,6 +52,8 @@ class _AddHubDialogState extends State<AddHubDialog> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -68,66 +70,85 @@ class _AddHubDialogState extends State<AddHubDialog> {
|
||||
BoxShadow(color: UiColors.popupShadow, blurRadius: 20),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_hubs.add_hub_dialog.title,
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
_buildFieldLabel(t.client_hubs.add_hub_dialog.name_label),
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
style: UiTypography.body1r.textPrimary,
|
||||
decoration: _buildInputDecoration(
|
||||
t.client_hubs.add_hub_dialog.name_hint,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_hubs.add_hub_dialog.title,
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildFieldLabel(t.client_hubs.add_hub_dialog.address_label),
|
||||
HubAddressAutocomplete(
|
||||
controller: _addressController,
|
||||
hintText: t.client_hubs.add_hub_dialog.address_hint,
|
||||
focusNode: _addressFocusNode,
|
||||
onSelected: (Prediction prediction) {
|
||||
_selectedPrediction = prediction;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: UiButton.secondary(
|
||||
onPressed: widget.onCancel,
|
||||
text: t.common.cancel,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
_buildFieldLabel(t.client_hubs.add_hub_dialog.name_label),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
style: UiTypography.body1r.textPrimary,
|
||||
validator: (String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Name is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: _buildInputDecoration(
|
||||
t.client_hubs.add_hub_dialog.name_hint,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: UiButton.primary(
|
||||
onPressed: () {
|
||||
if (_nameController.text.isNotEmpty) {
|
||||
widget.onCreate(
|
||||
_nameController.text,
|
||||
_addressController.text,
|
||||
placeId: _selectedPrediction?.placeId,
|
||||
latitude: double.tryParse(
|
||||
_selectedPrediction?.lat ?? '',
|
||||
),
|
||||
longitude: double.tryParse(
|
||||
_selectedPrediction?.lng ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
text: t.client_hubs.add_hub_dialog.create_button,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildFieldLabel(t.client_hubs.add_hub_dialog.address_label),
|
||||
// Assuming HubAddressAutocomplete is a custom widget wrapper.
|
||||
// If it doesn't expose a validator, we might need to modify it or manually check _addressController.
|
||||
// For now, let's just make sure we validate name. Address is tricky if it's a wrapper.
|
||||
HubAddressAutocomplete(
|
||||
controller: _addressController,
|
||||
hintText: t.client_hubs.add_hub_dialog.address_hint,
|
||||
focusNode: _addressFocusNode,
|
||||
onSelected: (Prediction prediction) {
|
||||
_selectedPrediction = prediction;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: UiButton.secondary(
|
||||
onPressed: widget.onCancel,
|
||||
text: t.common.cancel,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: UiButton.primary(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Manually check address if needed, or assume manual entry is ok.
|
||||
if (_addressController.text.trim().isEmpty) {
|
||||
// Show manual error or scaffold
|
||||
UiSnackbar.show(context, message: 'Address is required', type: UiSnackbarType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onCreate(
|
||||
_nameController.text,
|
||||
_addressController.text,
|
||||
placeId: _selectedPrediction?.placeId,
|
||||
latitude: double.tryParse(
|
||||
_selectedPrediction?.lat ?? '',
|
||||
),
|
||||
longitude: double.tryParse(
|
||||
_selectedPrediction?.lng ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
text: t.client_hubs.add_hub_dialog.create_button,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
import '../blocs/client_settings_bloc.dart';
|
||||
import '../widgets/client_settings_page/settings_actions.dart';
|
||||
@@ -24,15 +26,19 @@ class ClientSettingsPage extends StatelessWidget {
|
||||
child: BlocListener<ClientSettingsBloc, ClientSettingsState>(
|
||||
listener: (BuildContext context, ClientSettingsState state) {
|
||||
if (state is ClientSettingsSignOutSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Signed out successfully')),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Signed out successfully',
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
Modular.to.toClientRoot();
|
||||
}
|
||||
if (state is ClientSettingsError) {
|
||||
ScaffoldMessenger.of(
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(state.message)));
|
||||
message: translateErrorKey(state.message),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Scaffold(
|
||||
|
||||
@@ -62,7 +62,10 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState>
|
||||
);
|
||||
_updateDerivedState();
|
||||
},
|
||||
onError: (String _) => state.copyWith(status: ViewOrdersStatus.failure),
|
||||
onError: (String message) => state.copyWith(
|
||||
status: ViewOrdersStatus.failure,
|
||||
errorMessage: message,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,11 @@ class ViewOrdersState extends Equatable {
|
||||
this.activeCount = 0,
|
||||
this.completedCount = 0,
|
||||
this.upNextCount = 0,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final ViewOrdersStatus status;
|
||||
final String? errorMessage;
|
||||
final List<OrderItem> orders;
|
||||
final List<OrderItem> filteredOrders;
|
||||
final List<DateTime> calendarDays;
|
||||
@@ -39,9 +41,11 @@ class ViewOrdersState extends Equatable {
|
||||
int? activeCount,
|
||||
int? completedCount,
|
||||
int? upNextCount,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return ViewOrdersState(
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
orders: orders ?? this.orders,
|
||||
filteredOrders: filteredOrders ?? this.filteredOrders,
|
||||
calendarDays: calendarDays ?? this.calendarDays,
|
||||
@@ -66,5 +70,6 @@ class ViewOrdersState extends Equatable {
|
||||
activeCount,
|
||||
completedCount,
|
||||
upNextCount,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -68,7 +68,17 @@ class _ViewOrdersViewState extends State<ViewOrdersView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ViewOrdersCubit, ViewOrdersState>(
|
||||
return BlocConsumer<ViewOrdersCubit, ViewOrdersState>(
|
||||
listener: (BuildContext context, ViewOrdersState state) {
|
||||
if (state.status == ViewOrdersStatus.failure &&
|
||||
state.errorMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, ViewOrdersState state) {
|
||||
final List<DateTime> calendarDays = state.calendarDays;
|
||||
final List<OrderItem> filteredOrders = state.filteredOrders;
|
||||
@@ -101,64 +111,66 @@ class _ViewOrdersViewState extends State<ViewOrdersView> {
|
||||
|
||||
// Content List
|
||||
Expanded(
|
||||
child: filteredOrders.isEmpty
|
||||
? _buildEmptyState(context: context, state: state)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space4,
|
||||
UiConstants.space5,
|
||||
100,
|
||||
),
|
||||
children: <Widget>[
|
||||
if (filteredOrders.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: dotColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: UiConstants.space2,
|
||||
),
|
||||
Text(
|
||||
sectionTitle.toUpperCase(),
|
||||
style: UiTypography.titleUppercase2m
|
||||
.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: UiConstants.space1,
|
||||
),
|
||||
Text(
|
||||
'(${filteredOrders.length})',
|
||||
style: UiTypography.footnote1r
|
||||
.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: state.status == ViewOrdersStatus.failure
|
||||
? _buildErrorState(context: context, state: state)
|
||||
: filteredOrders.isEmpty
|
||||
? _buildEmptyState(context: context, state: state)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space4,
|
||||
UiConstants.space5,
|
||||
100,
|
||||
),
|
||||
...filteredOrders.map(
|
||||
(OrderItem order) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
children: <Widget>[
|
||||
if (filteredOrders.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: dotColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: UiConstants.space2,
|
||||
),
|
||||
Text(
|
||||
sectionTitle.toUpperCase(),
|
||||
style: UiTypography.titleUppercase2m
|
||||
.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: UiConstants.space1,
|
||||
),
|
||||
Text(
|
||||
'(${filteredOrders.length})',
|
||||
style: UiTypography.footnote1r
|
||||
.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
...filteredOrders.map(
|
||||
(OrderItem order) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
child: ViewOrderCard(order: order),
|
||||
),
|
||||
),
|
||||
child: ViewOrderCard(order: order),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -208,4 +220,36 @@ class _ViewOrdersViewState extends State<ViewOrdersView> {
|
||||
if (checkDate == tomorrow) return 'Tomorrow';
|
||||
return DateFormat('EEE, MMM d').format(date);
|
||||
}
|
||||
|
||||
Widget _buildErrorState({
|
||||
required BuildContext context,
|
||||
required ViewOrdersState state,
|
||||
}) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.error,
|
||||
size: 48,
|
||||
color: UiColors.error,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
style: UiTypography.body1m.textError,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiButton.secondary(
|
||||
text: 'Retry',
|
||||
onPressed: () => BlocProvider.of<ViewOrdersCubit>(context)
|
||||
.jumpToDate(state.selectedDate ?? DateTime.now()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user