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()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -57,12 +58,11 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
||||
AuthSignInRequested(phoneNumber: '+1$normalized', mode: widget.mode),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
t.staff_authentication.phone_verification_page.validation_error,
|
||||
),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.staff_authentication.phone_verification_page.validation_error,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 180, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -110,14 +110,11 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
||||
final String messageKey = state.errorMessage ?? '';
|
||||
// Handle specific business logic errors for signup
|
||||
if (messageKey == 'errors.auth.account_exists') {
|
||||
final ScaffoldMessengerState messenger =
|
||||
ScaffoldMessenger.of(context);
|
||||
messenger.hideCurrentSnackBar();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(translateErrorKey(messageKey)),
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(messageKey),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 180, left: 16, right: 16),
|
||||
);
|
||||
Future<void>.delayed(const Duration(seconds: 5), () {
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -96,13 +96,11 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||
if (state.status == ProfileSetupStatus.success) {
|
||||
Modular.to.toStaffHome();
|
||||
} else if (state.status == ProfileSetupStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage ??
|
||||
t.staff_authentication.profile_setup_page.error_occurred,
|
||||
),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage ?? t.staff_authentication.profile_setup_page.error_occurred),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ class AvailabilityRepositoryImpl
|
||||
implements AvailabilityRepository {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final firebase.FirebaseAuth _firebaseAuth;
|
||||
String? _cachedStaffId;
|
||||
|
||||
AvailabilityRepositoryImpl({
|
||||
required dc.ExampleConnector dataConnect,
|
||||
@@ -22,6 +23,8 @@ class AvailabilityRepositoryImpl
|
||||
_firebaseAuth = firebaseAuth;
|
||||
|
||||
Future<String> _getStaffId() async {
|
||||
if (_cachedStaffId != null) return _cachedStaffId!;
|
||||
|
||||
final firebase.User? user = _firebaseAuth.currentUser;
|
||||
if (user == null) {
|
||||
throw NotAuthenticatedException(
|
||||
@@ -33,7 +36,8 @@ class AvailabilityRepositoryImpl
|
||||
if (result.data.staffs.isEmpty) {
|
||||
throw const ServerException(technicalMessage: 'Staff profile not found');
|
||||
}
|
||||
return result.data.staffs.first.id;
|
||||
_cachedStaffId = result.data.staffs.first.id;
|
||||
return _cachedStaffId!;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -149,48 +153,51 @@ class AvailabilityRepositoryImpl
|
||||
final Set<dc.DayOfWeek> processedDays = {};
|
||||
final List<DayAvailability> resultDays = [];
|
||||
|
||||
final List<Future<void>> futures = [];
|
||||
|
||||
for (int i = 0; i <= dayCount; i++) {
|
||||
final DateTime date = start.add(Duration(days: i));
|
||||
final dc.DayOfWeek dow = _toBackendDay(date.weekday);
|
||||
|
||||
// Logic to determine if enabled based on type
|
||||
bool enableDay = false;
|
||||
if (type == 'all') enableDay = true;
|
||||
else if (type == 'clear') enableDay = false;
|
||||
else if (type == 'weekdays') {
|
||||
enableDay = (dow != dc.DayOfWeek.SATURDAY && dow != dc.DayOfWeek.SUNDAY);
|
||||
} else if (type == 'weekends') {
|
||||
enableDay = (dow == dc.DayOfWeek.SATURDAY || dow == dc.DayOfWeek.SUNDAY);
|
||||
}
|
||||
final DateTime date = start.add(Duration(days: i));
|
||||
final dc.DayOfWeek dow = _toBackendDay(date.weekday);
|
||||
|
||||
// Only update backend once per DayOfWeek (since it's recurring)
|
||||
// to avoid redundant calls if range > 1 week.
|
||||
if (!processedDays.contains(dow)) {
|
||||
processedDays.add(dow);
|
||||
|
||||
final dc.AvailabilityStatus status = _boolToStatus(enableDay);
|
||||
|
||||
await Future.wait([
|
||||
_upsertSlot(staffId, dow, dc.AvailabilitySlot.MORNING, status),
|
||||
_upsertSlot(staffId, dow, dc.AvailabilitySlot.AFTERNOON, status),
|
||||
_upsertSlot(staffId, dow, dc.AvailabilitySlot.EVENING, status),
|
||||
]);
|
||||
}
|
||||
|
||||
// Prepare return object
|
||||
final slots = [
|
||||
AvailabilityAdapter.fromPrimitive('MORNING', isAvailable: enableDay),
|
||||
AvailabilityAdapter.fromPrimitive('AFTERNOON', isAvailable: enableDay),
|
||||
AvailabilityAdapter.fromPrimitive('EVENING', isAvailable: enableDay),
|
||||
];
|
||||
// Logic to determine if enabled based on type
|
||||
bool enableDay = false;
|
||||
if (type == 'all') {
|
||||
enableDay = true;
|
||||
} else if (type == 'clear') {
|
||||
enableDay = false;
|
||||
} else if (type == 'weekdays') {
|
||||
enableDay = (dow != dc.DayOfWeek.SATURDAY && dow != dc.DayOfWeek.SUNDAY);
|
||||
} else if (type == 'weekends') {
|
||||
enableDay = (dow == dc.DayOfWeek.SATURDAY || dow == dc.DayOfWeek.SUNDAY);
|
||||
}
|
||||
|
||||
resultDays.add(DayAvailability(
|
||||
date: date,
|
||||
isAvailable: enableDay,
|
||||
slots: slots,
|
||||
));
|
||||
// Only update backend once per DayOfWeek (since it's recurring)
|
||||
if (!processedDays.contains(dow)) {
|
||||
processedDays.add(dow);
|
||||
final dc.AvailabilityStatus status = _boolToStatus(enableDay);
|
||||
|
||||
futures.add(_upsertSlot(staffId, dow, dc.AvailabilitySlot.MORNING, status));
|
||||
futures.add(_upsertSlot(staffId, dow, dc.AvailabilitySlot.AFTERNOON, status));
|
||||
futures.add(_upsertSlot(staffId, dow, dc.AvailabilitySlot.EVENING, status));
|
||||
}
|
||||
|
||||
// Prepare return object
|
||||
final slots = [
|
||||
AvailabilityAdapter.fromPrimitive('MORNING', isAvailable: enableDay),
|
||||
AvailabilityAdapter.fromPrimitive('AFTERNOON', isAvailable: enableDay),
|
||||
AvailabilityAdapter.fromPrimitive('EVENING', isAvailable: enableDay),
|
||||
];
|
||||
|
||||
resultDays.add(DayAvailability(
|
||||
date: date,
|
||||
isAvailable: enableDay,
|
||||
slots: slots,
|
||||
));
|
||||
}
|
||||
|
||||
// Execute all updates in parallel
|
||||
await Future.wait(futures);
|
||||
|
||||
return resultDays;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,12 +42,12 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Translations.of(context);
|
||||
final i18n = Translations.of(context).staff.availability;
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: Scaffold(
|
||||
appBar: UiAppBar(
|
||||
title: 'My Availability',
|
||||
title: i18n.title,
|
||||
centerTitle: false,
|
||||
showBackButton: true,
|
||||
),
|
||||
@@ -64,17 +64,9 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
if (state is AvailabilityError) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.message,
|
||||
message: translateErrorKey(state.message),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
} else if (state is AvailabilityError) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(translateErrorKey(state.message)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<AvailabilityBloc, AvailabilityState>(
|
||||
@@ -110,7 +102,12 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
),
|
||||
),
|
||||
if (state.isActionInProgress)
|
||||
const UiLoadingPage(), // Show loading overlay during actions
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: UiColors.white.withValues(alpha: 0.5),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (state is AvailabilityError) {
|
||||
@@ -128,7 +125,6 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
@@ -140,6 +136,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
}
|
||||
|
||||
Widget _buildQuickSet(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff.availability;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -150,26 +147,28 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Quick Set Availability',
|
||||
i18n.quick_set_title,
|
||||
style: UiTypography.body2b,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildQuickSetButton(context, 'All Week', 'all')),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(context, 'Weekdays', 'weekdays'),
|
||||
child: _buildQuickSetButton(context, i18n.all_week, 'all'),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(context, 'Weekends', 'weekends'),
|
||||
child: _buildQuickSetButton(context, i18n.weekdays, 'weekdays'),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(context, i18n.weekends, 'weekends'),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(
|
||||
context,
|
||||
'Clear All',
|
||||
i18n.clear_all,
|
||||
'clear',
|
||||
isDestructive: true,
|
||||
),
|
||||
@@ -388,7 +387,15 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
style: UiTypography.title2b,
|
||||
),
|
||||
Text(
|
||||
isAvailable ? 'You are available' : 'Not available',
|
||||
isAvailable
|
||||
? Translations.of(context)
|
||||
.staff
|
||||
.availability
|
||||
.available_status
|
||||
: Translations.of(context)
|
||||
.staff
|
||||
.availability
|
||||
.not_available_status,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
@@ -560,6 +567,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
}
|
||||
|
||||
Widget _buildInfoCard() {
|
||||
final i18n = Translations.of(context).staff.availability;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -577,11 +585,11 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
spacing: UiConstants.space1,
|
||||
children: [
|
||||
Text(
|
||||
'Auto-Match uses your availability',
|
||||
i18n.auto_match_title,
|
||||
style: UiTypography.body2m,
|
||||
),
|
||||
Text(
|
||||
"When enabled, you'll only be matched with shifts during your available times.",
|
||||
i18n.auto_match_description,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -32,6 +32,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff.clock_in;
|
||||
return BlocProvider<ClockInBloc>.value(
|
||||
value: _bloc,
|
||||
child: BlocConsumer<ClockInBloc, ClockInState>(
|
||||
@@ -68,7 +69,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(
|
||||
titleWidget: Text(
|
||||
'Clock In to your Shift',
|
||||
i18n.title,
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
showBackButton: false,
|
||||
@@ -115,7 +116,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
|
||||
// Your Activity Header
|
||||
Text(
|
||||
"Your Activity",
|
||||
i18n.your_activity,
|
||||
textAlign: TextAlign.start,
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
@@ -161,21 +162,23 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
shift.id ==
|
||||
selectedShift?.id
|
||||
? "SELECTED SHIFT"
|
||||
: "TODAY'S SHIFT",
|
||||
style: UiTypography
|
||||
.titleUppercase4b
|
||||
.copyWith(
|
||||
color: shift.id ==
|
||||
Text(
|
||||
shift.id ==
|
||||
selectedShift?.id
|
||||
? UiColors.primary
|
||||
: UiColors
|
||||
.textSecondary,
|
||||
? i18n
|
||||
.selected_shift_badge
|
||||
: i18n
|
||||
.today_shift_badge,
|
||||
style: UiTypography
|
||||
.titleUppercase4b
|
||||
.copyWith(
|
||||
color: shift.id ==
|
||||
selectedShift?.id
|
||||
? UiColors.primary
|
||||
: UiColors
|
||||
.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
shift.title,
|
||||
@@ -237,12 +240,16 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
"You're early!",
|
||||
i18n.early_title,
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
"Check-in available at ${_getCheckInAvailabilityTime(selectedShift)}",
|
||||
i18n.check_in_at(
|
||||
time: _getCheckInAvailabilityTime(
|
||||
selectedShift,
|
||||
),
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -316,12 +323,12 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Text(
|
||||
"Shift Completed!",
|
||||
i18n.shift_completed,
|
||||
style: UiTypography.body1b.textSuccess,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
"Great work today",
|
||||
i18n.great_work,
|
||||
style: UiTypography.body2r.textSuccess,
|
||||
),
|
||||
],
|
||||
@@ -339,13 +346,13 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"No confirmed shifts for today",
|
||||
i18n.no_shifts_today,
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
"Accept a shift to clock in",
|
||||
i18n.accept_shift_cta,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -377,7 +384,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Checked in at",
|
||||
i18n.checked_in_at_label,
|
||||
style: UiTypography.body3m.textSuccess,
|
||||
),
|
||||
Text(
|
||||
@@ -472,6 +479,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
}
|
||||
|
||||
Future<void> _showNFCDialog(BuildContext context) async {
|
||||
final i18n = Translations.of(context).staff.clock_in;
|
||||
bool scanned = false;
|
||||
|
||||
// Using a local navigator context since we are in a dialog
|
||||
@@ -482,7 +490,11 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(scanned ? 'Tag Scanned!' : 'Scan NFC Tag'),
|
||||
title: Text(
|
||||
scanned
|
||||
? i18n.nfc_dialog.scanned_title
|
||||
: i18n.nfc_dialog.scan_title,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
@@ -503,14 +515,16 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
scanned ? 'Processing check-in...' : 'Ready to scan',
|
||||
scanned
|
||||
? i18n.nfc_dialog.processing
|
||||
: i18n.nfc_dialog.ready_to_scan,
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
scanned
|
||||
? 'Please wait...'
|
||||
: 'Hold your phone near the NFC tag at the clock-in station',
|
||||
? i18n.nfc_dialog.please_wait
|
||||
: i18n.nfc_dialog.scan_instruction,
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
@@ -538,7 +552,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
},
|
||||
icon: const Icon(UiIcons.nfc, size: 24),
|
||||
label: Text(
|
||||
'Tap to Scan',
|
||||
i18n.nfc_dialog.tap_to_scan,
|
||||
style: UiTypography.headline4m.white,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -608,7 +622,8 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
final DateTime windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||
return DateFormat('h:mm a').format(windowStart);
|
||||
} catch (e) {
|
||||
return 'soon';
|
||||
final i18n = Translations.of(context).staff.clock_in;
|
||||
return i18n.soon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:krow_domain/krow_domain.dart';
|
||||
@@ -131,6 +132,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final CommuteMode mode = _getAppMode();
|
||||
final i18n = Translations.of(context).staff.clock_in.commute;
|
||||
|
||||
// Notify parent of mode change
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -142,20 +144,20 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
return const SizedBox.shrink();
|
||||
|
||||
case CommuteMode.needsConsent:
|
||||
return _buildConsentCard();
|
||||
return _buildConsentCard(i18n);
|
||||
|
||||
case CommuteMode.preShiftCommuteAllowed:
|
||||
return _buildPreShiftCard();
|
||||
return _buildPreShiftCard(i18n);
|
||||
|
||||
case CommuteMode.commuteModeActive:
|
||||
return _buildActiveCommuteScreen();
|
||||
return _buildActiveCommuteScreen(i18n);
|
||||
|
||||
case CommuteMode.arrivedCanClockIn:
|
||||
return _buildArrivedCard();
|
||||
return _buildArrivedCard(i18n);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildConsentCard() {
|
||||
Widget _buildConsentCard(TranslationsStaffClockInCommuteEn i18n) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space5),
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
@@ -202,12 +204,12 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Enable Commute Tracking?',
|
||||
i18n.enable_title,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'Share location 1hr before shift so your manager can see you\'re on the way.',
|
||||
i18n.enable_desc,
|
||||
style: UiTypography.body4r.textSecondary,
|
||||
),
|
||||
],
|
||||
@@ -229,7 +231,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
child: Text('Not Now', style: UiTypography.footnote1m),
|
||||
child: Text(i18n.not_now, style: UiTypography.footnote1m),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
@@ -245,7 +247,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Enable',
|
||||
i18n.enable,
|
||||
style: UiTypography.footnote1m.white,
|
||||
),
|
||||
),
|
||||
@@ -257,7 +259,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreShiftCard() {
|
||||
Widget _buildPreShiftCard(TranslationsStaffClockInCommuteEn i18n) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space5),
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
@@ -295,7 +297,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'On My Way',
|
||||
i18n.on_my_way,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
@@ -308,7 +310,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'Shift starts in ${_getMinutesUntilShift()} min',
|
||||
i18n.starts_in(min: _getMinutesUntilShift().toString()),
|
||||
style: UiTypography.titleUppercase4m.textSecondary,
|
||||
),
|
||||
],
|
||||
@@ -316,7 +318,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'Track arrival',
|
||||
i18n.track_arrival,
|
||||
style: UiTypography.titleUppercase4m.textSecondary,
|
||||
),
|
||||
],
|
||||
@@ -335,7 +337,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActiveCommuteScreen() {
|
||||
Widget _buildActiveCommuteScreen(TranslationsStaffClockInCommuteEn i18n) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
decoration: const BoxDecoration(
|
||||
@@ -353,11 +355,11 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
TweenAnimationBuilder(
|
||||
tween: Tween<double>(begin: 1.0, end: 1.1),
|
||||
duration: const Duration(seconds: 1),
|
||||
@@ -387,12 +389,12 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
'On My Way',
|
||||
i18n.on_my_way,
|
||||
style: UiTypography.displayMb.white,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'Your manager can see you\'re heading to the site',
|
||||
i18n.heading_to_site,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
@@ -414,7 +416,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Distance to Site',
|
||||
i18n.distance_to_site,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
@@ -443,14 +445,14 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Estimated Arrival',
|
||||
i18n.estimated_arrival,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'${widget.etaMinutes} min',
|
||||
i18n.eta_label(min: widget.etaMinutes.toString()),
|
||||
style: UiTypography.headline1m.white,
|
||||
),
|
||||
],
|
||||
@@ -460,7 +462,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
],
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
Text(
|
||||
'Most app features are locked while commute mode is on. You\'ll be able to clock in once you arrive.',
|
||||
i18n.locked_desc,
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
@@ -485,7 +487,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: Text('Turn Off Commute Mode', style: UiTypography.buttonL),
|
||||
child: Text(i18n.turn_off, style: UiTypography.buttonL),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -494,7 +496,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildArrivedCard() {
|
||||
Widget _buildArrivedCard(TranslationsStaffClockInCommuteEn i18n) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space5),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
@@ -533,12 +535,12 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'You\'ve Arrived! 🎉',
|
||||
i18n.arrived_title,
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'You\'re at the shift location. Ready to clock in?',
|
||||
i18n.arrived_desc,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -22,13 +23,6 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
String _additionalNotes = '';
|
||||
|
||||
final List<String> _timeOptions = _generateTimeOptions();
|
||||
final List<String> _noLunchReasons = <String>[
|
||||
'Unpredictable Workflows',
|
||||
'Poor Time Management',
|
||||
'Lack of coverage or short Staff',
|
||||
'No Lunch Area',
|
||||
'Other (Please specify)',
|
||||
];
|
||||
|
||||
static List<String> _generateTimeOptions() {
|
||||
final List<String> options = <String>[];
|
||||
@@ -45,6 +39,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff.clock_in.lunch_break;
|
||||
return Dialog(
|
||||
backgroundColor: UiColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -52,29 +47,29 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _buildCurrentStep(),
|
||||
child: _buildCurrentStep(i18n),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentStep() {
|
||||
Widget _buildCurrentStep(TranslationsStaffClockInLunchBreakEn i18n) {
|
||||
switch (_step) {
|
||||
case 1:
|
||||
return _buildStep1();
|
||||
return _buildStep1(i18n);
|
||||
case 2:
|
||||
return _buildStep2();
|
||||
return _buildStep2(i18n);
|
||||
case 102: // 2b: No lunch reason
|
||||
return _buildStep2b();
|
||||
return _buildStep2b(i18n);
|
||||
case 3:
|
||||
return _buildStep3();
|
||||
return _buildStep3(i18n);
|
||||
case 4:
|
||||
return _buildStep4();
|
||||
return _buildStep4(i18n);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStep1() {
|
||||
Widget _buildStep1(TranslationsStaffClockInLunchBreakEn i18n) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
@@ -95,7 +90,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
"Did You Take\na Lunch?",
|
||||
i18n.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.headline1m.textPrimary,
|
||||
),
|
||||
@@ -121,7 +116,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"No",
|
||||
i18n.no,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
),
|
||||
@@ -146,7 +141,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Yes",
|
||||
i18n.yes,
|
||||
style: UiTypography.body1m.white,
|
||||
),
|
||||
),
|
||||
@@ -158,7 +153,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep2() {
|
||||
Widget _buildStep2(TranslationsStaffClockInLunchBreakEn i18n) {
|
||||
// Time input
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
@@ -166,7 +161,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"When did you take lunch?",
|
||||
i18n.when_title,
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
@@ -186,9 +181,9 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (String? v) => setState(() => _breakStart = v),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Start',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
decoration: InputDecoration(
|
||||
labelText: i18n.start,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
@@ -209,9 +204,9 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (String? v) => setState(() => _breakEnd = v),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'End',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
decoration: InputDecoration(
|
||||
labelText: i18n.end,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
@@ -230,14 +225,14 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
backgroundColor: UiColors.primary,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: Text("Next", style: UiTypography.body1m.white),
|
||||
child: Text(i18n.next, style: UiTypography.body1m.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep2b() {
|
||||
Widget _buildStep2b(TranslationsStaffClockInLunchBreakEn i18n) {
|
||||
// No lunch reason
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
@@ -246,11 +241,11 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Why didn't you take lunch?",
|
||||
i18n.why_no_lunch,
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
..._noLunchReasons.map(
|
||||
...i18n.reasons.map(
|
||||
(String reason) => RadioListTile<String>(
|
||||
title: Text(reason, style: UiTypography.body2r),
|
||||
value: reason,
|
||||
@@ -269,14 +264,14 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
backgroundColor: UiColors.primary,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: Text("Next", style: UiTypography.body1m.white),
|
||||
child: Text(i18n.next, style: UiTypography.body1m.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep3() {
|
||||
Widget _buildStep3(TranslationsStaffClockInLunchBreakEn i18n) {
|
||||
// Additional Notes
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
@@ -284,16 +279,16 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Additional Notes",
|
||||
i18n.additional_notes,
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
TextField(
|
||||
onChanged: (String v) => _additionalNotes = v,
|
||||
style: UiTypography.body2r,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Add any details...',
|
||||
border: OutlineInputBorder(),
|
||||
decoration: InputDecoration(
|
||||
hintText: i18n.notes_placeholder,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
@@ -307,14 +302,14 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
backgroundColor: UiColors.primary,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: Text("Submit", style: UiTypography.body1m.white),
|
||||
child: Text(i18n.submit, style: UiTypography.body1m.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep4() {
|
||||
Widget _buildStep4(TranslationsStaffClockInLunchBreakEn i18n) {
|
||||
// Success
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
@@ -324,7 +319,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
const Icon(UiIcons.checkCircle, size: 64, color: UiColors.success),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
"Break Logged!",
|
||||
i18n.success_title,
|
||||
style: UiTypography.headline1m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
@@ -334,7 +329,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
backgroundColor: UiColors.primary,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: Text("Close", style: UiTypography.body1m.white),
|
||||
child: Text(i18n.close, style: UiTypography.body1m.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -71,6 +72,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff.clock_in.swipe;
|
||||
final Color baseColor = widget.isCheckedIn
|
||||
? UiColors.success
|
||||
: UiColors.primary;
|
||||
@@ -110,9 +112,9 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
Text(
|
||||
widget.isLoading
|
||||
? (widget.isCheckedIn
|
||||
? "Checking out..."
|
||||
: "Checking in...")
|
||||
: (widget.isCheckedIn ? "NFC Check Out" : "NFC Check In"),
|
||||
? i18n.checking_out
|
||||
: i18n.checking_in)
|
||||
: (widget.isCheckedIn ? i18n.nfc_checkout : i18n.nfc_checkin),
|
||||
style: UiTypography.body1b.white,
|
||||
),
|
||||
],
|
||||
@@ -157,8 +159,8 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
opacity: 1.0 - progress,
|
||||
child: Text(
|
||||
widget.isCheckedIn
|
||||
? "Swipe to Check Out"
|
||||
: "Swipe to Check In",
|
||||
? i18n.swipe_checkout
|
||||
: i18n.swipe_checkin,
|
||||
style: UiTypography.body1b,
|
||||
),
|
||||
),
|
||||
@@ -166,7 +168,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
if (_isComplete)
|
||||
Center(
|
||||
child: Text(
|
||||
widget.isCheckedIn ? "Check Out!" : "Check In!",
|
||||
widget.isCheckedIn ? i18n.checkout_complete : i18n.checkin_complete,
|
||||
style: UiTypography.body1b,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -38,14 +38,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
backgroundColor: UiColors.background,
|
||||
body: BlocConsumer<PaymentsBloc, PaymentsState>(
|
||||
listener: (context, state) {
|
||||
if (state is PaymentsError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(translateErrorKey(state.message)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Error is already shown on the page itself (lines 53-63), no need for snackbar
|
||||
},
|
||||
builder: (BuildContext context, PaymentsState state) {
|
||||
if (state is PaymentsLoading) {
|
||||
@@ -67,14 +60,6 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (state is PaymentsLoaded) {
|
||||
return _buildContent(context, state);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -66,11 +66,10 @@ class StaffProfilePage extends StatelessWidget {
|
||||
Modular.to.toGetStarted();
|
||||
} else if (state.status == ProfileStatus.error &&
|
||||
state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(translateErrorKey(state.errorMessage!)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -74,11 +74,10 @@ class CertificatesPage extends StatelessWidget {
|
||||
onEditExpiry: () => _showEditExpiryDialog(context, doc),
|
||||
onRemove: () => _showRemoveConfirmation(context, doc),
|
||||
onView: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.staff_certificates.card.opened_snackbar),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.staff_certificates.card.opened_snackbar,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -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_modular/flutter_modular.dart' hide ModularWatchExtension;
|
||||
@@ -76,6 +77,15 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.i9;
|
||||
|
||||
final List<Map<String, String>> steps = <Map<String, String>>[
|
||||
<String, String>{'title': i18n.steps.personal, 'subtitle': i18n.steps.personal_sub},
|
||||
<String, String>{'title': i18n.steps.address, 'subtitle': i18n.steps.address_sub},
|
||||
<String, String>{'title': i18n.steps.citizenship, 'subtitle': i18n.steps.citizenship_sub},
|
||||
<String, String>{'title': i18n.steps.review, 'subtitle': i18n.steps.review_sub},
|
||||
];
|
||||
|
||||
return BlocProvider<FormI9Cubit>.value(
|
||||
value: Modular.get<FormI9Cubit>(),
|
||||
child: BlocConsumer<FormI9Cubit, FormI9State>(
|
||||
@@ -83,34 +93,32 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
if (state.status == FormI9Status.success) {
|
||||
// Success view is handled by state check in build or we can navigate
|
||||
} else if (state.status == FormI9Status.failure) {
|
||||
final ScaffoldMessengerState messenger =
|
||||
ScaffoldMessenger.of(context);
|
||||
messenger.hideCurrentSnackBar();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'An error occurred'),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage ?? 'An error occurred'),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 100),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, FormI9State state) {
|
||||
if (state.status == FormI9Status.success) return _buildSuccessView();
|
||||
if (state.status == FormI9Status.success) return _buildSuccessView(i18n);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
_buildHeader(context, state),
|
||||
_buildHeader(context, state, steps, i18n),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: _buildCurrentStep(context, state),
|
||||
child: _buildCurrentStep(context, state, i18n),
|
||||
),
|
||||
),
|
||||
_buildFooter(context, state),
|
||||
_buildFooter(context, state, steps),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -119,7 +127,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccessView() {
|
||||
Widget _buildSuccessView(TranslationsStaffComplianceTaxFormsI9En i18n) {
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
body: Center(
|
||||
@@ -150,12 +158,12 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Form I-9 Submitted!',
|
||||
i18n.submitted_title,
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'Your employment eligibility verification has been submitted.',
|
||||
i18n.submitted_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
@@ -175,7 +183,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('Back to Documents'),
|
||||
child: Text(Translations.of(context).staff_compliance.tax_forms.w4.back_to_docs),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -186,7 +194,12 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context, FormI9State state) {
|
||||
Widget _buildHeader(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
List<Map<String, String>> steps,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Container(
|
||||
color: UiColors.primary,
|
||||
padding: const EdgeInsets.only(
|
||||
@@ -213,11 +226,11 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Form I-9',
|
||||
i18n.title,
|
||||
style: UiTypography.headline4m.white,
|
||||
),
|
||||
Text(
|
||||
'Employment Eligibility Verification',
|
||||
i18n.subtitle,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
@@ -228,12 +241,12 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Row(
|
||||
children: _steps
|
||||
children: steps
|
||||
.asMap()
|
||||
.entries
|
||||
.map((MapEntry<int, Map<String, String>> entry) {
|
||||
final int idx = entry.key;
|
||||
final bool isLast = idx == _steps.length - 1;
|
||||
final bool isLast = idx == steps.length - 1;
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@@ -259,14 +272,17 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Step ${state.currentStep + 1} of ${_steps.length}',
|
||||
i18n.step_label(
|
||||
current: (state.currentStep + 1).toString(),
|
||||
total: steps.length.toString(),
|
||||
),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_steps[state.currentStep]['title']!,
|
||||
steps[state.currentStep]['title']!,
|
||||
textAlign: TextAlign.end,
|
||||
style: UiTypography.body3m.white.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -280,16 +296,20 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentStep(BuildContext context, FormI9State state) {
|
||||
Widget _buildCurrentStep(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
switch (state.currentStep) {
|
||||
case 0:
|
||||
return _buildStep1(context, state);
|
||||
return _buildStep1(context, state, i18n);
|
||||
case 1:
|
||||
return _buildStep2(context, state);
|
||||
return _buildStep2(context, state, i18n);
|
||||
case 2:
|
||||
return _buildStep3(context, state);
|
||||
return _buildStep3(context, state, i18n);
|
||||
case 3:
|
||||
return _buildStep4(context, state);
|
||||
return _buildStep4(context, state, i18n);
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
@@ -347,26 +367,30 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep1(BuildContext context, FormI9State state) {
|
||||
Widget _buildStep1(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'First Name *',
|
||||
i18n.fields.first_name,
|
||||
value: state.firstName,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().firstNameChanged(val),
|
||||
placeholder: 'John',
|
||||
placeholder: i18n.fields.hints.first_name,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'Last Name *',
|
||||
i18n.fields.last_name,
|
||||
value: state.lastName,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().lastNameChanged(val),
|
||||
placeholder: 'Smith',
|
||||
placeholder: i18n.fields.hints.last_name,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -376,37 +400,37 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'Middle Initial',
|
||||
i18n.fields.middle_initial,
|
||||
value: state.middleInitial,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().middleInitialChanged(val),
|
||||
placeholder: 'A',
|
||||
placeholder: i18n.fields.hints.middle_initial,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
'Other Last Names',
|
||||
i18n.fields.other_last_names,
|
||||
value: state.otherLastNames,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().otherLastNamesChanged(val),
|
||||
placeholder: 'Maiden name (if any)',
|
||||
placeholder: i18n.fields.maiden_name,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Date of Birth *',
|
||||
i18n.fields.dob,
|
||||
value: state.dob,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().dobChanged(val),
|
||||
placeholder: 'MM/DD/YYYY',
|
||||
placeholder: i18n.fields.hints.dob,
|
||||
keyboardType: TextInputType.datetime,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Social Security Number *',
|
||||
i18n.fields.ssn,
|
||||
value: state.ssn,
|
||||
placeholder: 'XXX-XX-XXXX',
|
||||
placeholder: i18n.fields.hints.ssn,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (String val) {
|
||||
String text = val.replaceAll(RegExp(r'\D'), '');
|
||||
@@ -416,39 +440,43 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Email Address',
|
||||
i18n.fields.email,
|
||||
value: state.email,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().emailChanged(val),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
placeholder: 'john.smith@example.com',
|
||||
placeholder: i18n.fields.hints.email,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Phone Number',
|
||||
i18n.fields.phone,
|
||||
value: state.phone,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().phoneChanged(val),
|
||||
keyboardType: TextInputType.phone,
|
||||
placeholder: '(555) 555-5555',
|
||||
placeholder: i18n.fields.hints.phone,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep2(BuildContext context, FormI9State state) {
|
||||
Widget _buildStep2(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
_buildTextField(
|
||||
'Address (Street Number and Name) *',
|
||||
i18n.fields.address_long,
|
||||
value: state.address,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().addressChanged(val),
|
||||
placeholder: '123 Main Street',
|
||||
placeholder: i18n.fields.hints.address,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Apt. Number',
|
||||
i18n.fields.apt,
|
||||
value: state.aptNumber,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().aptNumberChanged(val),
|
||||
placeholder: '4B',
|
||||
placeholder: i18n.fields.hints.apt,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
@@ -456,10 +484,10 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
'City or Town *',
|
||||
i18n.fields.city,
|
||||
value: state.city,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().cityChanged(val),
|
||||
placeholder: 'San Francisco',
|
||||
placeholder: i18n.fields.hints.city,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -468,7 +496,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'State *',
|
||||
i18n.fields.state,
|
||||
style: UiTypography.body3m.textSecondary.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@@ -507,22 +535,26 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'ZIP Code *',
|
||||
i18n.fields.zip,
|
||||
value: state.zipCode,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().zipCodeChanged(val),
|
||||
placeholder: '94103',
|
||||
placeholder: i18n.fields.hints.zip,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep3(BuildContext context, FormI9State state) {
|
||||
Widget _buildStep3(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'I attest, under penalty of perjury, that I am (check one of the following boxes):',
|
||||
i18n.fields.attestation,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
@@ -530,29 +562,29 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
context,
|
||||
state,
|
||||
'CITIZEN',
|
||||
'1. A citizen of the United States',
|
||||
i18n.fields.citizen,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildRadioOption(
|
||||
context,
|
||||
state,
|
||||
'NONCITIZEN',
|
||||
'2. A noncitizen national of the United States',
|
||||
i18n.fields.noncitizen,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildRadioOption(
|
||||
context,
|
||||
state,
|
||||
'PERMANENT_RESIDENT',
|
||||
'3. A lawful permanent resident',
|
||||
i18n.fields.permanent_resident,
|
||||
child: state.citizenshipStatus == 'PERMANENT_RESIDENT'
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: _buildTextField(
|
||||
'USCIS Number',
|
||||
i18n.fields.uscis_number_label,
|
||||
value: state.uscisNumber,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().uscisNumberChanged(val),
|
||||
placeholder: 'A-123456789',
|
||||
placeholder: i18n.fields.hints.uscis,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
@@ -562,26 +594,26 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
context,
|
||||
state,
|
||||
'ALIEN',
|
||||
'4. An alien authorized to work',
|
||||
i18n.fields.alien,
|
||||
child: state.citizenshipStatus == 'ALIEN'
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
_buildTextField(
|
||||
'USCIS/Admission Number',
|
||||
i18n.fields.admission_number,
|
||||
value: state.admissionNumber,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().admissionNumberChanged(val),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
'Foreign Passport Number',
|
||||
i18n.fields.passport,
|
||||
value: state.passportNumber,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().passportNumberChanged(val),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
'Country of Issuance',
|
||||
i18n.fields.country,
|
||||
value: state.countryIssuance,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().countryIssuanceChanged(val),
|
||||
),
|
||||
@@ -645,7 +677,11 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep4(BuildContext context, FormI9State state) {
|
||||
Widget _buildStep4(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -660,18 +696,18 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Summary',
|
||||
i18n.fields.summary_title,
|
||||
style: UiTypography.headline4m.copyWith(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
_buildSummaryRow('Name', '${state.firstName} ${state.lastName}'),
|
||||
_buildSummaryRow('Address', '${state.address}, ${state.city}'),
|
||||
_buildSummaryRow(i18n.fields.summary_name, '${state.firstName} ${state.lastName}'),
|
||||
_buildSummaryRow(i18n.fields.summary_address, '${state.address}, ${state.city}'),
|
||||
_buildSummaryRow(
|
||||
'SSN',
|
||||
i18n.fields.summary_ssn,
|
||||
'***-**-${state.ssn.length >= 4 ? state.ssn.substring(state.ssn.length - 4) : '****'}',
|
||||
),
|
||||
_buildSummaryRow(
|
||||
'Citizenship',
|
||||
i18n.fields.summary_citizenship,
|
||||
_getReadableCitizenship(state.citizenshipStatus),
|
||||
),
|
||||
],
|
||||
@@ -685,7 +721,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
'I used a preparer or translator',
|
||||
i18n.fields.preparer,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@@ -699,13 +735,13 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Text(
|
||||
'I am aware that federal law provides for imprisonment and/or fines for false statements or use of false documents in connection with the completion of this form.',
|
||||
i18n.fields.warning,
|
||||
style: UiTypography.body3r.textWarning.copyWith(fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
'Signature (type your full name) *',
|
||||
i18n.fields.signature_label,
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -717,7 +753,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
onChanged: (String val) =>
|
||||
context.read<FormI9Cubit>().signatureChanged(val),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your full name',
|
||||
hintText: i18n.fields.signature_hint,
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
@@ -741,7 +777,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Date',
|
||||
i18n.fields.date_label,
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -788,21 +824,28 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
}
|
||||
|
||||
String _getReadableCitizenship(String status) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.i9.fields;
|
||||
switch (status) {
|
||||
case 'CITIZEN':
|
||||
return 'US Citizen';
|
||||
return i18n.status_us_citizen;
|
||||
case 'NONCITIZEN':
|
||||
return 'Noncitizen National';
|
||||
return i18n.status_noncitizen;
|
||||
case 'PERMANENT_RESIDENT':
|
||||
return 'Permanent Resident';
|
||||
return i18n.status_permanent_resident;
|
||||
case 'ALIEN':
|
||||
return 'Alien Authorized to Work';
|
||||
return i18n.status_alien;
|
||||
default:
|
||||
return 'Unknown';
|
||||
return i18n.status_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, FormI9State state) {
|
||||
Widget _buildFooter(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
List<Map<String, String>> steps,
|
||||
) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.i9;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
@@ -837,7 +880,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Back',
|
||||
i18n.back,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
],
|
||||
@@ -878,11 +921,11 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.currentStep == _steps.length - 1
|
||||
? 'Sign & Submit'
|
||||
: 'Continue',
|
||||
state.currentStep == steps.length - 1
|
||||
? i18n.submit
|
||||
: i18n.kContinue,
|
||||
),
|
||||
if (state.currentStep < _steps.length - 1) ...<Widget>[
|
||||
if (state.currentStep < steps.length - 1) ...<Widget>[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.white),
|
||||
],
|
||||
|
||||
@@ -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_modular/flutter_modular.dart' hide ModularWatchExtension;
|
||||
@@ -122,6 +123,17 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.w4;
|
||||
|
||||
final List<Map<String, String>> steps = <Map<String, String>>[
|
||||
<String, String>{'title': i18n.steps.personal, 'subtitle': i18n.step_label(current: '1', total: '5')},
|
||||
<String, String>{'title': i18n.steps.filing, 'subtitle': i18n.step_label(current: '1c', total: '5')},
|
||||
<String, String>{'title': i18n.steps.multiple_jobs, 'subtitle': i18n.step_label(current: '2', total: '5')},
|
||||
<String, String>{'title': i18n.steps.dependents, 'subtitle': i18n.step_label(current: '3', total: '5')},
|
||||
<String, String>{'title': i18n.steps.adjustments, 'subtitle': i18n.step_label(current: '4', total: '5')},
|
||||
<String, String>{'title': i18n.steps.review, 'subtitle': i18n.step_label(current: '5', total: '5')},
|
||||
];
|
||||
|
||||
return BlocProvider<FormW4Cubit>.value(
|
||||
value: Modular.get<FormW4Cubit>(),
|
||||
child: BlocConsumer<FormW4Cubit, FormW4State>(
|
||||
@@ -129,31 +141,32 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
if (state.status == FormW4Status.success) {
|
||||
// Handled in builder
|
||||
} else if (state.status == FormW4Status.failure) {
|
||||
final ScaffoldMessengerState messenger = ScaffoldMessenger.of(context);
|
||||
messenger.hideCurrentSnackBar();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'An error occurred')),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage ?? 'An error occurred'),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 100),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, FormW4State state) {
|
||||
if (state.status == FormW4Status.success) return _buildSuccessView();
|
||||
if (state.status == FormW4Status.success) return _buildSuccessView(i18n);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
_buildHeader(context, state),
|
||||
_buildHeader(context, state, steps, i18n),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: _buildCurrentStep(context, state),
|
||||
child: _buildCurrentStep(context, state, i18n),
|
||||
),
|
||||
),
|
||||
_buildFooter(context, state),
|
||||
_buildFooter(context, state, steps),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -162,7 +175,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccessView() {
|
||||
Widget _buildSuccessView(TranslationsStaffComplianceTaxFormsW4En i18n) {
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
body: Center(
|
||||
@@ -193,12 +206,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Form W-4 Submitted!',
|
||||
i18n.submitted_title,
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'Your withholding certificate has been submitted to your employer.',
|
||||
i18n.submitted_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
@@ -218,7 +231,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('Back to Documents'),
|
||||
child: Text(i18n.back_to_docs),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -229,7 +242,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context, FormW4State state) {
|
||||
Widget _buildHeader(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
List<Map<String, String>> steps,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Container(
|
||||
color: UiColors.primary,
|
||||
padding: const EdgeInsets.only(
|
||||
@@ -256,11 +274,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Form W-4',
|
||||
i18n.title,
|
||||
style: UiTypography.headline4m.white,
|
||||
),
|
||||
Text(
|
||||
'Employee\'s Withholding Certificate',
|
||||
i18n.subtitle,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
@@ -271,12 +289,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Row(
|
||||
children: _steps
|
||||
children: steps
|
||||
.asMap()
|
||||
.entries
|
||||
.map((MapEntry<int, Map<String, String>> entry) {
|
||||
final int idx = entry.key;
|
||||
final bool isLast = idx == _steps.length - 1;
|
||||
final bool isLast = idx == steps.length - 1;
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@@ -302,13 +320,16 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Step ${state.currentStep + 1} of ${_steps.length}',
|
||||
i18n.step_label(
|
||||
current: (state.currentStep + 1).toString(),
|
||||
total: steps.length.toString(),
|
||||
),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_steps[state.currentStep]['title']!,
|
||||
steps[state.currentStep]['title']!,
|
||||
style: UiTypography.body3m.white.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@@ -320,20 +341,24 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentStep(BuildContext context, FormW4State state) {
|
||||
Widget _buildCurrentStep(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
switch (state.currentStep) {
|
||||
case 0:
|
||||
return _buildStep1(context, state);
|
||||
return _buildStep1(context, state, i18n);
|
||||
case 1:
|
||||
return _buildStep2(context, state);
|
||||
return _buildStep2(context, state, i18n);
|
||||
case 2:
|
||||
return _buildStep3(context, state);
|
||||
return _buildStep3(context, state, i18n);
|
||||
case 3:
|
||||
return _buildStep4(context, state);
|
||||
return _buildStep4(context, state, i18n);
|
||||
case 4:
|
||||
return _buildStep5(context, state);
|
||||
return _buildStep5(context, state, i18n);
|
||||
case 5:
|
||||
return _buildStep6(context, state);
|
||||
return _buildStep6(context, state, i18n);
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
@@ -391,35 +416,39 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep1(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep1(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'First Name *',
|
||||
i18n.fields.first_name,
|
||||
value: state.firstName,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().firstNameChanged(val),
|
||||
placeholder: 'John',
|
||||
placeholder: i18n.fields.placeholder_john,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'Last Name *',
|
||||
i18n.fields.last_name,
|
||||
value: state.lastName,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().lastNameChanged(val),
|
||||
placeholder: 'Smith',
|
||||
placeholder: i18n.fields.placeholder_smith,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Social Security Number *',
|
||||
i18n.fields.ssn,
|
||||
value: state.ssn,
|
||||
placeholder: 'XXX-XX-XXXX',
|
||||
placeholder: i18n.fields.placeholder_ssn,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (String val) {
|
||||
String text = val.replaceAll(RegExp(r'\D'), '');
|
||||
@@ -429,23 +458,27 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Address *',
|
||||
i18n.fields.address,
|
||||
value: state.address,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().addressChanged(val),
|
||||
placeholder: '123 Main Street',
|
||||
placeholder: i18n.fields.placeholder_address,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'City, State, ZIP',
|
||||
i18n.fields.city_state_zip,
|
||||
value: state.cityStateZip,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().cityStateZipChanged(val),
|
||||
placeholder: 'San Francisco, CA 94102',
|
||||
placeholder: i18n.fields.placeholder_csz,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep2(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep2(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@@ -460,7 +493,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Your filing status determines your standard deduction and tax rates.',
|
||||
i18n.fields.filing_info,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
),
|
||||
@@ -472,7 +505,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
context,
|
||||
state,
|
||||
'SINGLE',
|
||||
'Single or Married filing separately',
|
||||
i18n.fields.single,
|
||||
null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -480,7 +513,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
context,
|
||||
state,
|
||||
'MARRIED',
|
||||
'Married filing jointly or Qualifying surviving spouse',
|
||||
i18n.fields.married,
|
||||
null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -488,8 +521,8 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
context,
|
||||
state,
|
||||
'HEAD',
|
||||
'Head of household',
|
||||
'Check only if you\'re unmarried and pay more than half the costs of keeping up a home',
|
||||
i18n.fields.head,
|
||||
i18n.fields.head_desc,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -555,7 +588,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep3(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep3(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@@ -578,12 +615,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'When to complete this step?',
|
||||
i18n.fields.multiple_jobs_title,
|
||||
style: UiTypography.body2m.accent,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Complete this step only if you hold more than one job at a time, or are married filing jointly and your spouse also works.',
|
||||
i18n.fields.multiple_jobs_desc,
|
||||
style: UiTypography.body3r.accent,
|
||||
),
|
||||
],
|
||||
@@ -632,12 +669,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'I have multiple jobs or my spouse works',
|
||||
i18n.fields.multiple_jobs_check,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Check this box if there are only two jobs total',
|
||||
i18n.fields.two_jobs_desc,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
@@ -649,7 +686,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'If this does not apply, you can continue to the next step',
|
||||
i18n.fields.multiple_jobs_not_apply,
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
@@ -657,7 +694,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep4(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep4(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@@ -672,7 +713,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'If your total income will be \$200,000 or less (\$400,000 if married filing jointly), you may claim credits for dependents.',
|
||||
i18n.fields.dependents_info,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
),
|
||||
@@ -692,8 +733,8 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
_buildCounter(
|
||||
context,
|
||||
state,
|
||||
'Qualifying children under age 17',
|
||||
'\$2,000 each',
|
||||
i18n.fields.children_under_17,
|
||||
i18n.fields.children_each,
|
||||
(FormW4State s) => s.qualifyingChildren,
|
||||
(int val) => context.read<FormW4Cubit>().qualifyingChildrenChanged(val),
|
||||
),
|
||||
@@ -704,8 +745,8 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
_buildCounter(
|
||||
context,
|
||||
state,
|
||||
'Other dependents',
|
||||
'\$500 each',
|
||||
i18n.fields.other_dependents,
|
||||
i18n.fields.other_each,
|
||||
(FormW4State s) => s.otherDependents,
|
||||
(int val) => context.read<FormW4Cubit>().otherDependentsChanged(val),
|
||||
),
|
||||
@@ -723,9 +764,9 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'Total credits (Step 3)',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
i18n.fields.total_credits,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF166534),
|
||||
),
|
||||
@@ -824,56 +865,60 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep5(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep5(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'These adjustments are optional. You can skip them if they don\'t apply.',
|
||||
i18n.fields.adjustments_info,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildTextField(
|
||||
'4(a) Other income (not from jobs)',
|
||||
i18n.fields.other_income,
|
||||
value: state.otherIncome,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().otherIncomeChanged(val),
|
||||
placeholder: '\$0',
|
||||
placeholder: i18n.fields.hints.zero,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 16),
|
||||
child: Text(
|
||||
'Include interest, dividends, retirement income',
|
||||
i18n.fields.other_income_desc,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
_buildTextField(
|
||||
'4(b) Deductions',
|
||||
i18n.fields.deductions,
|
||||
value: state.deductions,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().deductionsChanged(val),
|
||||
placeholder: '\$0',
|
||||
placeholder: i18n.fields.hints.zero,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 16),
|
||||
child: Text(
|
||||
'If you expect to claim deductions other than the standard deduction',
|
||||
i18n.fields.deductions_desc,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
_buildTextField(
|
||||
'4(c) Extra withholding',
|
||||
i18n.fields.extra_withholding,
|
||||
value: state.extraWithholding,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().extraWithholdingChanged(val),
|
||||
placeholder: '\$0',
|
||||
placeholder: i18n.fields.hints.zero,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 16),
|
||||
child: Text(
|
||||
'Any additional tax you want withheld each pay period',
|
||||
i18n.fields.extra_withholding_desc,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
),
|
||||
@@ -881,7 +926,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep6(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep6(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -896,25 +945,25 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Your W-4 Summary',
|
||||
i18n.fields.summary_title,
|
||||
style: UiTypography.headline4m.copyWith(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildSummaryRow(
|
||||
'Name',
|
||||
i18n.fields.summary_name,
|
||||
'${state.firstName} ${state.lastName}',
|
||||
),
|
||||
_buildSummaryRow(
|
||||
'SSN',
|
||||
i18n.fields.summary_ssn,
|
||||
'***-**-${state.ssn.length >= 4 ? state.ssn.substring(state.ssn.length - 4) : '****'}',
|
||||
),
|
||||
_buildSummaryRow(
|
||||
'Filing Status',
|
||||
i18n.fields.summary_filing,
|
||||
_getFilingStatusLabel(state.filingStatus),
|
||||
),
|
||||
if (_totalCredits(state) > 0)
|
||||
_buildSummaryRow(
|
||||
'Credits',
|
||||
i18n.fields.summary_credits,
|
||||
'\$${_totalCredits(state)}',
|
||||
valueColor: Colors.green[700],
|
||||
),
|
||||
@@ -929,13 +978,13 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Text(
|
||||
'Under penalties of perjury, I declare that this certificate, to the best of my knowledge and belief, is true, correct, and complete.',
|
||||
i18n.fields.perjury_declaration,
|
||||
style: UiTypography.body3r.textWarning.copyWith(fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
'Signature (type your full name) *',
|
||||
i18n.fields.signature_label,
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -947,7 +996,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
onChanged: (String val) =>
|
||||
context.read<FormW4Cubit>().signatureChanged(val),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your full name',
|
||||
hintText: i18n.fields.signature_hint,
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
@@ -971,7 +1020,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Date',
|
||||
i18n.fields.date_label,
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -1017,19 +1066,26 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
}
|
||||
|
||||
String _getFilingStatusLabel(String status) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.w4.fields;
|
||||
switch (status) {
|
||||
case 'single':
|
||||
return 'Single';
|
||||
case 'married':
|
||||
return 'Married';
|
||||
case 'head_of_household':
|
||||
return 'Head of Household';
|
||||
case 'SINGLE':
|
||||
return i18n.status_single;
|
||||
case 'MARRIED':
|
||||
return i18n.status_married;
|
||||
case 'HEAD':
|
||||
return i18n.status_head;
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, FormW4State state) {
|
||||
Widget _buildFooter(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
List<Map<String, String>> steps,
|
||||
) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.w4;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
@@ -1064,7 +1120,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Back',
|
||||
i18n.fields.back,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
],
|
||||
@@ -1105,11 +1161,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.currentStep == _steps.length - 1
|
||||
? 'Submit Form'
|
||||
: 'Continue',
|
||||
state.currentStep == steps.length - 1
|
||||
? i18n.fields.submit
|
||||
: i18n.fields.kContinue,
|
||||
),
|
||||
if (state.currentStep < _steps.length - 1) ...<Widget>[
|
||||
if (state.currentStep < steps.length - 1) ...<Widget>[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.white),
|
||||
],
|
||||
|
||||
@@ -48,29 +48,14 @@ class BankAccountPage extends StatelessWidget {
|
||||
bloc: cubit,
|
||||
listener: (BuildContext context, BankAccountState state) {
|
||||
if (state.status == BankAccountStatus.accountAdded) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
strings.account_added_success,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
backgroundColor: UiColors.tagSuccess,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
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,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: strings.account_added_success,
|
||||
type: UiSnackbarType.success,
|
||||
margin: const EdgeInsets.only(bottom: 120, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
// Error is already shown on the page itself (lines 73-85), no need for snackbar
|
||||
},
|
||||
builder: (BuildContext context, BankAccountState state) {
|
||||
if (state.status == BankAccountStatus.loading && state.accounts.isEmpty) {
|
||||
|
||||
@@ -19,6 +19,25 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
final TextEditingController _routingController = TextEditingController();
|
||||
final TextEditingController _accountController = TextEditingController();
|
||||
String _selectedType = 'CHECKING';
|
||||
bool _isFormValid = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bankNameController.addListener(_validateForm);
|
||||
_routingController.addListener(_validateForm);
|
||||
_accountController.addListener(_validateForm);
|
||||
}
|
||||
|
||||
void _validateForm() {
|
||||
setState(() {
|
||||
_isFormValid = _bankNameController.text.trim().isNotEmpty &&
|
||||
_routingController.text.trim().isNotEmpty &&
|
||||
_routingController.text.replaceAll(RegExp(r'\D'), '').length == 9 &&
|
||||
_accountController.text.trim().isNotEmpty &&
|
||||
_accountController.text.replaceAll(RegExp(r'\D'), '').length >= 4;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -96,14 +115,16 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
Expanded(
|
||||
child: UiButton.primary(
|
||||
text: widget.strings.save,
|
||||
onPressed: () {
|
||||
widget.onSubmit(
|
||||
_bankNameController.text,
|
||||
_routingController.text,
|
||||
_accountController.text,
|
||||
_selectedType,
|
||||
);
|
||||
},
|
||||
onPressed: _isFormValid
|
||||
? () {
|
||||
widget.onSubmit(
|
||||
_bankNameController.text.trim(),
|
||||
_routingController.text.trim(),
|
||||
_accountController.text.trim(),
|
||||
_selectedType,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -51,13 +51,10 @@ class _TimeCardPageState extends State<TimeCardPage> {
|
||||
body: BlocConsumer<TimeCardBloc, TimeCardState>(
|
||||
listener: (context, state) {
|
||||
if (state is TimeCardError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
translateErrorKey(state.message),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.message),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -42,8 +42,11 @@ class AttirePage extends StatelessWidget {
|
||||
body: BlocConsumer<AttireCubit, AttireState>(
|
||||
listener: (BuildContext context, AttireState state) {
|
||||
if (state.status == AttireStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'Error')),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage ?? 'Error'),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
if (state.status == AttireStatus.saved) {
|
||||
|
||||
@@ -42,15 +42,13 @@ class EmergencyContactScreen extends StatelessWidget {
|
||||
|
||||
listener: (context, state) {
|
||||
if (state.status == EmergencyContactStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,14 +16,11 @@ class EmergencyContactSaveButton extends StatelessWidget {
|
||||
listenWhen: (previous, current) => previous.status != current.status,
|
||||
listener: (context, state) {
|
||||
if (state.status == EmergencyContactStatus.saved) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Emergency contacts saved successfully',
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
backgroundColor: UiColors.iconSuccess,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Emergency contacts saved successfully',
|
||||
type: UiSnackbarType.success,
|
||||
margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -58,20 +58,21 @@ class ExperiencePage extends StatelessWidget {
|
||||
child: BlocConsumer<ExperienceBloc, ExperienceState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == ExperienceStatus.success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Experience saved successfully')),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Experience saved successfully',
|
||||
type: UiSnackbarType.success,
|
||||
margin: const EdgeInsets.only(bottom: 120, left: 16, right: 16),
|
||||
);
|
||||
Modular.to.pop();
|
||||
} else if (state.status == ExperienceStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 120, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ import 'personal_info_state.dart';
|
||||
/// during onboarding or profile editing. It delegates business logic to
|
||||
/// use cases following Clean Architecture principles.
|
||||
class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
with BlocErrorHandler<PersonalInfoState>
|
||||
with BlocErrorHandler<PersonalInfoState>, SafeBloc<PersonalInfoEvent, PersonalInfoState>
|
||||
implements Disposable {
|
||||
/// Creates a [PersonalInfoBloc].
|
||||
///
|
||||
@@ -54,8 +54,8 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
'phone': staff.phone,
|
||||
'preferredLocations':
|
||||
staff.address != null
|
||||
? <String?>[staff.address]
|
||||
: <dynamic>[], // TODO: Map correctly when Staff entity supports list
|
||||
? <String>[staff.address!]
|
||||
: <String>[], // TODO: Map correctly when Staff entity supports list
|
||||
'avatar': staff.avatar,
|
||||
};
|
||||
|
||||
@@ -109,8 +109,8 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
'phone': updatedStaff.phone,
|
||||
'preferredLocations':
|
||||
updatedStaff.address != null
|
||||
? <String?>[updatedStaff.address]
|
||||
: <dynamic>[],
|
||||
? <String>[updatedStaff.address!]
|
||||
: <String>[],
|
||||
'avatar': updatedStaff.avatar,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,24 +28,19 @@ class PersonalInfoPage extends StatelessWidget {
|
||||
child: BlocListener<PersonalInfoBloc, PersonalInfoState>(
|
||||
listener: (BuildContext context, PersonalInfoState state) {
|
||||
if (state.status == PersonalInfoStatus.saved) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(i18n.save_success),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: i18n.save_success,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
Modular.to.pop();
|
||||
} else if (state.status == PersonalInfoStatus.error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -136,20 +137,18 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
}
|
||||
if (state is ShiftActionSuccess) {
|
||||
_isApplying = false;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: UiColors.success,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.message,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
Modular.to.toShifts(selectedDate: state.shiftDate);
|
||||
} else if (state is ShiftDetailsError) {
|
||||
if (_isApplying || widget.shift == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: UiColors.destructive,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.message),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
_isApplying = false;
|
||||
@@ -170,9 +169,10 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
displayShift = widget.shift;
|
||||
}
|
||||
|
||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||
if (displayShift == null) {
|
||||
return const Scaffold(
|
||||
body: Center(child: Text("Shift not found")),
|
||||
return Scaffold(
|
||||
body: Center(child: Text(Translations.of(context).staff_shifts.list.no_shifts)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"VENDOR",
|
||||
i18n.vendor,
|
||||
style: UiTypography.titleUppercase4b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
@@ -245,7 +245,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"SHIFT DATE",
|
||||
i18n.shift_date,
|
||||
style: UiTypography.titleUppercase4b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
@@ -284,7 +284,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
"$openSlots slots remaining",
|
||||
i18n.slots_remaining(count: openSlots),
|
||||
style: UiTypography.footnote1m.textSuccess,
|
||||
),
|
||||
],
|
||||
@@ -298,14 +298,14 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildTimeBox(
|
||||
"START TIME",
|
||||
i18n.start_time,
|
||||
displayShift.startTime,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: _buildTimeBox(
|
||||
"END TIME",
|
||||
i18n.end_time,
|
||||
displayShift.endTime,
|
||||
),
|
||||
),
|
||||
@@ -320,15 +320,15 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
child: _buildStatCard(
|
||||
UiIcons.dollar,
|
||||
"\$${displayShift.hourlyRate.toStringAsFixed(0)}/hr",
|
||||
"Base Rate",
|
||||
i18n.base_rate,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
UiIcons.clock,
|
||||
"${duration.toInt()} hours",
|
||||
"Duration",
|
||||
i18n.hours_label(count: duration.toInt()),
|
||||
i18n.duration,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
@@ -336,7 +336,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
child: _buildStatCard(
|
||||
UiIcons.wallet,
|
||||
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||
"Est. Total",
|
||||
i18n.est_total,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -348,7 +348,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"LOCATION",
|
||||
i18n.location,
|
||||
style: UiTypography.titleUppercase4b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
@@ -396,7 +396,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
UiIcons.arrowRight,
|
||||
size: 16,
|
||||
),
|
||||
label: const Text("Open in Maps"),
|
||||
label: Text(i18n.open_in_maps),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.primary,
|
||||
padding: EdgeInsets.zero,
|
||||
@@ -412,7 +412,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
// Description / Instructions
|
||||
if ((displayShift.description ?? '').isNotEmpty) ...[
|
||||
Text(
|
||||
"JOB DESCRIPTION",
|
||||
i18n.job_description,
|
||||
style: UiTypography.titleUppercase4b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
@@ -460,15 +460,16 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
BuildContext context,
|
||||
Shift shift,
|
||||
) {
|
||||
final i18n = Translations.of(context).staff_shifts.shift_details.book_dialog;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Book Shift'),
|
||||
content: const Text('Do you want to instantly book this shift?'),
|
||||
title: Text(i18n.title),
|
||||
content: Text(i18n.message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Modular.to.pop(),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(Translations.of(context).common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -485,7 +486,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.success,
|
||||
),
|
||||
child: const Text('Book'),
|
||||
child: Text(Translations.of(context).staff_shifts.shift_details.apply_now),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -493,17 +494,16 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
}
|
||||
|
||||
void _declineShift(BuildContext context, String id) {
|
||||
final i18n = Translations.of(context).staff_shifts.shift_details.decline_dialog;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Decline Shift'),
|
||||
content: const Text(
|
||||
'Are you sure you want to decline this shift? It will be hidden from your available jobs.',
|
||||
),
|
||||
title: Text(i18n.title),
|
||||
content: Text(i18n.message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Modular.to.pop(),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(Translations.of(context).common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -514,7 +514,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
),
|
||||
child: const Text('Decline'),
|
||||
child: Text(Translations.of(context).staff_shifts.shift_details.decline),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -525,12 +525,13 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
if (_actionDialogOpen) return;
|
||||
_actionDialogOpen = true;
|
||||
_isApplying = true;
|
||||
final i18n = Translations.of(context).staff_shifts.shift_details.applying_dialog;
|
||||
showDialog(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Applying'),
|
||||
title: Text(i18n.title),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -576,6 +577,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
Widget _buildBottomButton(Shift shift, BuildContext context) {
|
||||
final String status = shift.status ?? 'open';
|
||||
|
||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||
if (status == 'confirmed') {
|
||||
return Row(
|
||||
children: [
|
||||
@@ -591,7 +593,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text("CANCEL SHIFT", style: UiTypography.body2b.white),
|
||||
child: Text(i18n.cancel_shift, style: UiTypography.body2b.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
@@ -607,7 +609,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text("CLOCK IN", style: UiTypography.body2b.white),
|
||||
child: Text(i18n.clock_in, style: UiTypography.body2b.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -628,7 +630,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
),
|
||||
child: Text("DECLINE", style: UiTypography.body2b.textError),
|
||||
child: Text(i18n.decline, style: UiTypography.body2b.textError),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
@@ -644,7 +646,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text("ACCEPT SHIFT", style: UiTypography.body2b.white),
|
||||
child: Text(i18n.accept_shift, style: UiTypography.body2b.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -665,7 +667,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
),
|
||||
child: Text("DECLINE", style: UiTypography.body2b.textSecondary),
|
||||
child: Text(i18n.decline, style: UiTypography.body2b.textSecondary),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
@@ -681,7 +683,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text("APPLY NOW", style: UiTypography.body2b.white),
|
||||
child: Text(i18n.apply_now, style: UiTypography.body2b.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -692,15 +694,16 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
}
|
||||
|
||||
void _openCancelDialog(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff_shifts.shift_details.cancel_dialog;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Cancel Shift'),
|
||||
content: const Text('Are you sure you want to cancel this shift?'),
|
||||
title: Text(i18n.title),
|
||||
content: Text(i18n.message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Modular.to.pop(),
|
||||
child: const Text('No'),
|
||||
child: Text(Translations.of(context).common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -712,7 +715,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
),
|
||||
child: const Text('Yes, cancel it'),
|
||||
child: Text(Translations.of(context).common.ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -72,11 +72,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
child: BlocConsumer<ShiftsBloc, ShiftsState>(
|
||||
listener: (context, state) {
|
||||
if (state is ShiftsError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(translateErrorKey(state.message)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.message),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../blocs/shifts/shifts_bloc.dart';
|
||||
import '../my_shift_card.dart';
|
||||
@@ -115,11 +116,10 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.read<ShiftsBloc>().add(AcceptShiftEvent(id));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Shift confirmed!'),
|
||||
backgroundColor: UiColors.success,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Shift confirmed!',
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
@@ -149,11 +149,10 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.read<ShiftsBloc>().add(DeclineShiftEvent(id));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Shift declined.'),
|
||||
backgroundColor: UiColors.destructive,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Shift declined.',
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
|
||||
Reference in New Issue
Block a user