feat: localization, file restriction banners, test credentials, edit icon fix
- #553: Audit and verify localizations (en/es), replace hardcoded strings - #549: Incomplete profile banner in Find Shifts (staff app) - #550: File restriction banner on document upload page - #551: File restriction banner on certificate upload page - #552: File restriction banner on attire upload page - #492: Hide edit icon for past/completed orders (client app) - #524: Display worker benefits in staff app - Add test credentials to seed: testclient@gmail.com, staff +1-555-555-1234 - Fix document upload validation (context arg in _validatePdfFile on submit) - Add PR_LOCALIZATION.md Made-with: Cursor
This commit is contained in:
@@ -317,7 +317,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
children: [
|
||||
const Icon(UiIcons.warning, color: UiColors.error),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(child: Text("Eligibility Requirements")),
|
||||
Expanded(child: Text(context.t.staff_shifts.shift_details.eligibility_requirements)),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
|
||||
@@ -252,7 +252,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
if (availableLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return FindShiftsTab(availableJobs: availableJobs);
|
||||
return FindShiftsTab(
|
||||
availableJobs: availableJobs,
|
||||
profileComplete: state.profileComplete ?? true,
|
||||
);
|
||||
case ShiftTabType.history:
|
||||
if (historyLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
|
||||
@@ -117,7 +117,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
statusColor = UiColors.textLink;
|
||||
statusBg = UiColors.primary;
|
||||
} else if (status == 'checked_in') {
|
||||
statusText = 'Checked in';
|
||||
statusText = context.t.staff_shifts.my_shift_card.checked_in;
|
||||
statusColor = UiColors.textSuccess;
|
||||
statusBg = UiColors.iconSuccess;
|
||||
} else if (status == 'pending' || status == 'open') {
|
||||
@@ -487,20 +487,22 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_isSubmitted ? 'SUBMITTED' : 'READY TO SUBMIT',
|
||||
_isSubmitted
|
||||
? context.t.staff_shifts.my_shift_card.submitted
|
||||
: context.t.staff_shifts.my_shift_card.ready_to_submit,
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: _isSubmitted ? UiColors.textSuccess : UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (!_isSubmitted)
|
||||
UiButton.secondary(
|
||||
text: 'Submit for Approval',
|
||||
text: context.t.staff_shifts.my_shift_card.submit_for_approval,
|
||||
size: UiButtonSize.small,
|
||||
onPressed: () {
|
||||
setState(() => _isSubmitted = true);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Timesheet submitted for client approval',
|
||||
message: context.t.staff_shifts.my_shift_card.timesheet_submitted,
|
||||
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:krow_domain/krow_domain.dart';
|
||||
@@ -124,7 +125,7 @@ class ShiftLocationSection extends StatelessWidget {
|
||||
if (context.mounted) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Could not open maps',
|
||||
message: context.t.staff_shifts.shift_location.could_not_open_maps,
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
@@ -12,7 +13,15 @@ import 'package:geolocator/geolocator.dart';
|
||||
class FindShiftsTab extends StatefulWidget {
|
||||
final List<Shift> availableJobs;
|
||||
|
||||
const FindShiftsTab({super.key, required this.availableJobs});
|
||||
/// Whether the worker's profile is complete. When false, shows incomplete
|
||||
/// profile banner and disables apply actions.
|
||||
final bool profileComplete;
|
||||
|
||||
const FindShiftsTab({
|
||||
super.key,
|
||||
required this.availableJobs,
|
||||
this.profileComplete = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FindShiftsTab> createState() => _FindShiftsTabState();
|
||||
@@ -305,6 +314,15 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Incomplete profile banner
|
||||
if (!widget.profileComplete) ...[
|
||||
_IncompleteProfileBanner(
|
||||
title: context.t.staff_shifts.find_shifts.incomplete_profile_banner_title,
|
||||
message: context.t.staff_shifts.find_shifts.incomplete_profile_banner_message,
|
||||
ctaText: context.t.staff_shifts.find_shifts.incomplete_profile_cta,
|
||||
onCtaPressed: () => Modular.to.toProfile(),
|
||||
),
|
||||
],
|
||||
// Search and Filters
|
||||
Container(
|
||||
color: UiColors.white,
|
||||
@@ -440,20 +458,22 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
),
|
||||
child: MyShiftCard(
|
||||
shift: shift,
|
||||
onAccept: () {
|
||||
context.read<ShiftsBloc>().add(
|
||||
AcceptShiftEvent(shift.id),
|
||||
);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context
|
||||
.t
|
||||
.staff_shifts
|
||||
.find_shifts
|
||||
.application_submitted,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
onAccept: widget.profileComplete
|
||||
? () {
|
||||
context.read<ShiftsBloc>().add(
|
||||
AcceptShiftEvent(shift.id),
|
||||
);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context
|
||||
.t
|
||||
.staff_shifts
|
||||
.find_shifts
|
||||
.application_submitted,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -466,3 +486,54 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Banner shown when the worker's profile is incomplete.
|
||||
class _IncompleteProfileBanner extends StatelessWidget {
|
||||
const _IncompleteProfileBanner({
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.ctaText,
|
||||
required this.onCtaPressed,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String message;
|
||||
final String ctaText;
|
||||
final VoidCallback onCtaPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.all(UiConstants.space5),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagPending,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(
|
||||
color: UiColors.textWarning.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
message,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiButton.primary(
|
||||
text: ctaText,
|
||||
onPressed: onCtaPressed,
|
||||
size: UiButtonSize.small,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,28 +104,28 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
void _confirmShift(String id) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.staff_shifts.my_shifts_tab.confirm_dialog.title),
|
||||
content: Text(t.staff_shifts.my_shifts_tab.confirm_dialog.message),
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(context.t.staff_shifts.my_shifts_tab.confirm_dialog.title),
|
||||
content: Text(context.t.staff_shifts.my_shifts_tab.confirm_dialog.message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text(context.t.common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<ShiftsBloc>().add(AcceptShiftEvent(id));
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.staff_shifts.my_shifts_tab.confirm_dialog.success,
|
||||
message: context.t.staff_shifts.my_shifts_tab.confirm_dialog.success,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.success,
|
||||
),
|
||||
child: Text(t.staff_shifts.shift_details.accept_shift),
|
||||
child: Text(context.t.staff_shifts.shift_details.accept_shift),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -135,30 +135,30 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
void _declineShift(String id) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.staff_shifts.my_shifts_tab.decline_dialog.title),
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(context.t.staff_shifts.my_shifts_tab.decline_dialog.title),
|
||||
content: Text(
|
||||
t.staff_shifts.my_shifts_tab.decline_dialog.message,
|
||||
context.t.staff_shifts.my_shifts_tab.decline_dialog.message,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text(context.t.common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<ShiftsBloc>().add(DeclineShiftEvent(id));
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.staff_shifts.my_shifts_tab.decline_dialog.success,
|
||||
message: context.t.staff_shifts.my_shifts_tab.decline_dialog.success,
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
),
|
||||
child: Text(t.staff_shifts.shift_details.decline),
|
||||
child: Text(context.t.staff_shifts.shift_details.decline),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -169,9 +169,9 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
try {
|
||||
final date = DateTime.parse(dateStr);
|
||||
final now = DateTime.now();
|
||||
if (_isSameDay(date, now)) return t.staff_shifts.my_shifts_tab.date.today;
|
||||
if (_isSameDay(date, now)) return context.t.staff_shifts.my_shifts_tab.date.today;
|
||||
final tomorrow = now.add(const Duration(days: 1));
|
||||
if (_isSameDay(date, tomorrow)) return t.staff_shifts.my_shifts_tab.date.tomorrow;
|
||||
if (_isSameDay(date, tomorrow)) return context.t.staff_shifts.my_shifts_tab.date.tomorrow;
|
||||
return DateFormat('EEE, MMM d').format(date);
|
||||
} catch (_) {
|
||||
return dateStr;
|
||||
@@ -338,7 +338,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
if (widget.pendingAssignments.isNotEmpty) ...[
|
||||
_buildSectionHeader(
|
||||
t.staff_shifts.my_shifts_tab.sections.awaiting,
|
||||
context.t.staff_shifts.my_shifts_tab.sections.awaiting,
|
||||
UiColors.textWarning,
|
||||
),
|
||||
...widget.pendingAssignments.map(
|
||||
@@ -356,7 +356,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
],
|
||||
|
||||
if (visibleCancelledShifts.isNotEmpty) ...[
|
||||
_buildSectionHeader(t.staff_shifts.my_shifts_tab.sections.cancelled, UiColors.textSecondary),
|
||||
_buildSectionHeader(context.t.staff_shifts.my_shifts_tab.sections.cancelled, UiColors.textSecondary),
|
||||
...visibleCancelledShifts.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
@@ -378,7 +378,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
|
||||
// Confirmed Shifts
|
||||
if (visibleMyShifts.isNotEmpty) ...[
|
||||
_buildSectionHeader(t.staff_shifts.my_shifts_tab.sections.confirmed, UiColors.textSecondary),
|
||||
_buildSectionHeader(context.t.staff_shifts.my_shifts_tab.sections.confirmed, UiColors.textSecondary),
|
||||
...visibleMyShifts.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
@@ -388,7 +388,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
onRequestSwap: () {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: "Swap functionality coming soon!", // Todo: Localization
|
||||
message: context.t.staff_shifts.my_shifts_tab.swap_coming_soon,
|
||||
type: UiSnackbarType.message,
|
||||
);
|
||||
},
|
||||
@@ -402,8 +402,8 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
widget.cancelledShifts.isEmpty)
|
||||
EmptyStateView(
|
||||
icon: UiIcons.calendar,
|
||||
title: t.staff_shifts.my_shifts_tab.empty.title,
|
||||
subtitle: t.staff_shifts.my_shifts_tab.empty.subtitle,
|
||||
title: context.t.staff_shifts.my_shifts_tab.empty.title,
|
||||
subtitle: context.t.staff_shifts.my_shifts_tab.empty.subtitle,
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space32),
|
||||
@@ -472,13 +472,13 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
t.staff_shifts.my_shifts_tab.card.cancelled,
|
||||
context.t.staff_shifts.my_shifts_tab.card.cancelled,
|
||||
style: UiTypography.footnote2b.textError,
|
||||
),
|
||||
if (isLastMinute) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
t.staff_shifts.my_shifts_tab.card.compensation,
|
||||
context.t.staff_shifts.my_shifts_tab.card.compensation,
|
||||
style: UiTypography.footnote2m.textSuccess,
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user