feat: architecture overhaul, launchpad-style reports, and uber-style locations

- Strengthened Buffer Layer architecture to decouple Data Connect from Domain
- Rewired Coverage, Performance, and Forecast reports to match Launchpad logic
- Implemented Uber-style Preferred Locations search using Google Places API
- Added session recovery logic to prevent crashes on app restart
- Synchronized backend schemas & SDK for ShiftStatus enums
- Fixed various build/compilation errors and localization duplicates
This commit is contained in:
2026-02-20 17:20:06 +05:30
parent e6c4b51e84
commit 8849bf2273
60 changed files with 3804 additions and 2397 deletions

View File

@@ -29,6 +29,8 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
on<PersonalInfoFieldChanged>(_onFieldChanged);
on<PersonalInfoAddressSelected>(_onAddressSelected);
on<PersonalInfoFormSubmitted>(_onSubmitted);
on<PersonalInfoLocationAdded>(_onLocationAdded);
on<PersonalInfoLocationRemoved>(_onLocationRemoved);
add(const PersonalInfoLoadRequested());
}
@@ -133,11 +135,48 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
PersonalInfoAddressSelected event,
Emitter<PersonalInfoState> emit,
) {
// TODO: Implement Google Places logic if needed
// Legacy address selected no-op; use PersonalInfoLocationAdded instead.
}
/// With _onPhotoUploadRequested and _onSaveRequested removed or renamed,
/// there are no errors pointing to them here.
/// Adds a location to the preferredLocations list (max 5, no duplicates).
void _onLocationAdded(
PersonalInfoLocationAdded event,
Emitter<PersonalInfoState> emit,
) {
final dynamic raw = state.formValues['preferredLocations'];
final List<String> current = _toStringList(raw);
if (current.length >= 5) return; // max guard
if (current.contains(event.location)) return; // no duplicates
final List<String> updated = List<String>.from(current)..add(event.location);
final Map<String, dynamic> updatedValues = Map<String, dynamic>.from(state.formValues)
..['preferredLocations'] = updated;
emit(state.copyWith(formValues: updatedValues));
}
/// Removes a location from the preferredLocations list.
void _onLocationRemoved(
PersonalInfoLocationRemoved event,
Emitter<PersonalInfoState> emit,
) {
final dynamic raw = state.formValues['preferredLocations'];
final List<String> current = _toStringList(raw);
final List<String> updated = List<String>.from(current)
..remove(event.location);
final Map<String, dynamic> updatedValues = Map<String, dynamic>.from(state.formValues)
..['preferredLocations'] = updated;
emit(state.copyWith(formValues: updatedValues));
}
List<String> _toStringList(dynamic raw) {
if (raw is List<String>) return raw;
if (raw is List) return raw.map((dynamic e) => e.toString()).toList();
return <String>[];
}
@override
void dispose() {

View File

@@ -40,3 +40,21 @@ class PersonalInfoAddressSelected extends PersonalInfoEvent {
@override
List<Object?> get props => <Object?>[address];
}
/// Event to add a preferred location.
class PersonalInfoLocationAdded extends PersonalInfoEvent {
const PersonalInfoLocationAdded({required this.location});
final String location;
@override
List<Object?> get props => <Object?>[location];
}
/// Event to remove a preferred location.
class PersonalInfoLocationRemoved extends PersonalInfoEvent {
const PersonalInfoLocationRemoved({required this.location});
final String location;
@override
List<Object?> get props => <Object?>[location];
}

View File

@@ -0,0 +1,513 @@
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';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:google_places_flutter/google_places_flutter.dart';
import 'package:google_places_flutter/model/prediction.dart';
import 'package:krow_core/core.dart';
import '../blocs/personal_info_bloc.dart';
import '../blocs/personal_info_event.dart';
import '../blocs/personal_info_state.dart';
/// The maximum number of preferred locations a staff member can add.
const int _kMaxLocations = 5;
/// Uber-style Preferred Locations editing page.
///
/// Allows staff to search for US locations using the Google Places API,
/// add them as chips (max 5), and save back to their profile.
class PreferredLocationsPage extends StatefulWidget {
/// Creates a [PreferredLocationsPage].
const PreferredLocationsPage({super.key});
@override
State<PreferredLocationsPage> createState() => _PreferredLocationsPageState();
}
class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
late final TextEditingController _searchController;
late final FocusNode _searchFocusNode;
@override
void initState() {
super.initState();
_searchController = TextEditingController();
_searchFocusNode = FocusNode();
}
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
void _onLocationSelected(Prediction prediction, PersonalInfoBloc bloc) {
final String description = prediction.description ?? '';
if (description.isEmpty) return;
bloc.add(PersonalInfoLocationAdded(location: description));
// Clear search field after selection
_searchController.clear();
_searchFocusNode.unfocus();
}
void _removeLocation(String location, PersonalInfoBloc bloc) {
bloc.add(PersonalInfoLocationRemoved(location: location));
}
void _save(BuildContext context, PersonalInfoBloc bloc, PersonalInfoState state) {
bloc.add(const PersonalInfoFormSubmitted());
}
@override
Widget build(BuildContext context) {
final i18n = t.staff.onboarding.personal_info;
// Access the same PersonalInfoBloc singleton managed by the module.
final PersonalInfoBloc bloc = Modular.get<PersonalInfoBloc>();
return BlocProvider<PersonalInfoBloc>.value(
value: bloc,
child: BlocConsumer<PersonalInfoBloc, PersonalInfoState>(
listener: (BuildContext context, PersonalInfoState state) {
if (state.status == PersonalInfoStatus.saved) {
UiSnackbar.show(
context,
message: i18n.preferred_locations.save_success,
type: UiSnackbarType.success,
);
Navigator.of(context).pop();
} else if (state.status == PersonalInfoStatus.error) {
UiSnackbar.show(
context,
message: state.errorMessage != null
? translateErrorKey(state.errorMessage!)
: 'An error occurred',
type: UiSnackbarType.error,
);
}
},
builder: (BuildContext context, PersonalInfoState state) {
final List<String> locations = _currentLocations(state);
final bool atMax = locations.length >= _kMaxLocations;
final bool isSaving = state.status == PersonalInfoStatus.saving;
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
backgroundColor: UiColors.bgPopup,
elevation: 0,
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
onPressed: () => Navigator.of(context).pop(),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
),
title: Text(
i18n.preferred_locations.title,
style: UiTypography.title1m.textPrimary,
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// ── Description
Padding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space5,
UiConstants.space5,
UiConstants.space3,
),
child: Text(
i18n.preferred_locations.description,
style: UiTypography.body2r.textSecondary,
),
),
// ── Search autocomplete field
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
child: _PlacesSearchField(
controller: _searchController,
focusNode: _searchFocusNode,
hint: i18n.preferred_locations.search_hint,
enabled: !atMax && !isSaving,
onSelected: (Prediction p) => _onLocationSelected(p, bloc),
),
),
// ── "Max reached" banner
if (atMax)
Padding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space2,
UiConstants.space5,
0,
),
child: Row(
children: <Widget>[
const Icon(
UiIcons.info,
size: 14,
color: UiColors.textWarning,
),
const SizedBox(width: UiConstants.space1),
Text(
i18n.preferred_locations.max_reached,
style: UiTypography.footnote1r.textWarning,
),
],
),
),
const SizedBox(height: UiConstants.space5),
// ── Section label
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
child: Text(
i18n.preferred_locations.added_label,
style: UiTypography.titleUppercase3m.textSecondary,
),
),
const SizedBox(height: UiConstants.space3),
// ── Locations list / empty state
Expanded(
child: locations.isEmpty
? _EmptyLocationsState(message: i18n.preferred_locations.empty_state)
: _LocationsList(
locations: locations,
isSaving: isSaving,
removeTooltip: i18n.preferred_locations.remove_tooltip,
onRemove: (String loc) => _removeLocation(loc, bloc),
),
),
// ── Save button
Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: UiButton.primary(
text: i18n.preferred_locations.save_button,
fullWidth: true,
onPressed: isSaving ? null : () => _save(context, bloc, state),
),
),
],
),
),
);
},
),
);
}
List<String> _currentLocations(PersonalInfoState state) {
final dynamic raw = state.formValues['preferredLocations'];
if (raw is List<String>) return raw;
if (raw is List) return raw.map((dynamic e) => e.toString()).toList();
return <String>[];
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Subwidgets
// ─────────────────────────────────────────────────────────────────────────────
/// Google Places autocomplete search field, locked to US results.
class _PlacesSearchField extends StatelessWidget {
const _PlacesSearchField({
required this.controller,
required this.focusNode,
required this.hint,
required this.onSelected,
this.enabled = true,
});
final TextEditingController controller;
final FocusNode focusNode;
final String hint;
final bool enabled;
final void Function(Prediction) onSelected;
@override
Widget build(BuildContext context) {
return GooglePlaceAutoCompleteTextField(
textEditingController: controller,
focusNode: focusNode,
googleAPIKey: AppConfig.googleMapsApiKey,
debounceTime: 400,
countries: const <String>['us'],
isLatLngRequired: false,
getPlaceDetailWithLatLng: onSelected,
itemClick: (Prediction prediction) {
controller.text = prediction.description ?? '';
controller.selection = TextSelection.fromPosition(
TextPosition(offset: controller.text.length),
);
onSelected(prediction);
},
inputDecoration: InputDecoration(
hintText: hint,
hintStyle: UiTypography.body2r.textSecondary,
prefixIcon: const Icon(UiIcons.search, color: UiColors.iconSecondary, size: 20),
suffixIcon: controller.text.isNotEmpty
? IconButton(
icon: const Icon(UiIcons.close, size: 18, color: UiColors.iconSecondary),
onPressed: controller.clear,
)
: null,
contentPadding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: UiConstants.space3,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
borderSide: const BorderSide(color: UiColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
borderSide: const BorderSide(color: UiColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
borderSide: const BorderSide(color: UiColors.primary, width: 1.5),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
borderSide: BorderSide(color: UiColors.border.withValues(alpha: 0.5)),
),
fillColor: enabled ? UiColors.bgPopup : UiColors.bgSecondary,
filled: true,
),
textStyle: UiTypography.body2r.textPrimary,
itemBuilder: (BuildContext context, int index, Prediction prediction) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: UiConstants.space2,
),
child: Row(
children: <Widget>[
Container(
padding: const EdgeInsets.all(UiConstants.space2),
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(4.0),
),
child: const Icon(UiIcons.mapPin, size: 16, color: UiColors.primary),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
_mainText(prediction.description ?? ''),
style: UiTypography.body2m.textPrimary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (_subText(prediction.description ?? '').isNotEmpty)
Text(
_subText(prediction.description ?? ''),
style: UiTypography.footnote1r.textSecondary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
},
);
}
/// Extracts text before first comma as the primary line.
String _mainText(String description) {
final int commaIndex = description.indexOf(',');
return commaIndex > 0 ? description.substring(0, commaIndex) : description;
}
/// Extracts text after first comma as the secondary line.
String _subText(String description) {
final int commaIndex = description.indexOf(',');
return commaIndex > 0 ? description.substring(commaIndex + 1).trim() : '';
}
}
/// The scrollable list of location chips.
class _LocationsList extends StatelessWidget {
const _LocationsList({
required this.locations,
required this.isSaving,
required this.removeTooltip,
required this.onRemove,
});
final List<String> locations;
final bool isSaving;
final String removeTooltip;
final void Function(String) onRemove;
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
itemCount: locations.length,
separatorBuilder: (_, __) => const SizedBox(height: UiConstants.space2),
itemBuilder: (BuildContext context, int index) {
final String location = locations[index];
return _LocationChip(
label: location,
index: index + 1,
total: locations.length,
isSaving: isSaving,
removeTooltip: removeTooltip,
onRemove: () => onRemove(location),
);
},
);
}
}
/// A single location row with pin icon, label, and remove button.
class _LocationChip extends StatelessWidget {
const _LocationChip({
required this.label,
required this.index,
required this.total,
required this.isSaving,
required this.removeTooltip,
required this.onRemove,
});
final String label;
final int index;
final int total;
final bool isSaving;
final String removeTooltip;
final VoidCallback onRemove;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
border: Border.all(color: UiColors.border),
),
child: Row(
children: <Widget>[
// Index badge
Container(
width: 28,
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Text(
'$index',
style: UiTypography.footnote1m.copyWith(color: UiColors.primary),
),
),
const SizedBox(width: UiConstants.space3),
// Pin icon
const Icon(UiIcons.mapPin, size: 16, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space2),
// Location text
Expanded(
child: Text(
label,
style: UiTypography.body2m.textPrimary,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// Remove button
if (!isSaving)
Tooltip(
message: removeTooltip,
child: GestureDetector(
onTap: onRemove,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.all(UiConstants.space1),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: UiColors.bgSecondary,
shape: BoxShape.circle,
),
child: const Icon(UiIcons.close, size: 14, color: UiColors.iconSecondary),
),
),
),
),
],
),
);
}
}
/// Shows when no locations have been added yet.
class _EmptyLocationsState extends StatelessWidget {
const _EmptyLocationsState({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UiConstants.space8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.08),
shape: BoxShape.circle,
),
child: const Icon(UiIcons.mapPin, size: 28, color: UiColors.primary),
),
const SizedBox(height: UiConstants.space4),
Text(
message,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary,
),
],
),
),
);
}
}

View File

@@ -34,26 +34,22 @@ class PersonalInfoContent extends StatefulWidget {
class _PersonalInfoContentState extends State<PersonalInfoContent> {
late final TextEditingController _emailController;
late final TextEditingController _phoneController;
late final TextEditingController _locationsController;
@override
void initState() {
super.initState();
_emailController = TextEditingController(text: widget.staff.email);
_phoneController = TextEditingController(text: widget.staff.phone ?? '');
_locationsController = TextEditingController(text: widget.staff.preferredLocations?.join(', ')?? '');
// Listen to changes and update BLoC
_emailController.addListener(_onEmailChanged);
_phoneController.addListener(_onPhoneChanged);
_locationsController.addListener(_onAddressChanged);
}
@override
void dispose() {
_emailController.dispose();
_phoneController.dispose();
_locationsController.dispose();
super.dispose();
}
@@ -76,23 +72,6 @@ class _PersonalInfoContentState extends State<PersonalInfoContent> {
);
}
void _onAddressChanged() {
// Split the comma-separated string into a list for storage
// The backend expects List<AnyValue> (JSON/List) for preferredLocations
final List<String> locations = _locationsController.text
.split(',')
.map((String e) => e.trim())
.where((String e) => e.isNotEmpty)
.toList();
context.read<PersonalInfoBloc>().add(
PersonalInfoFieldChanged(
field: 'preferredLocations',
value: locations,
),
);
}
void _handleSave() {
context.read<PersonalInfoBloc>().add(const PersonalInfoFormSubmitted());
}
@@ -129,7 +108,7 @@ class _PersonalInfoContentState extends State<PersonalInfoContent> {
email: widget.staff.email,
emailController: _emailController,
phoneController: _phoneController,
locationsController: _locationsController,
currentLocations: _toStringList(state.formValues['preferredLocations']),
enabled: !isSaving,
),
const SizedBox(height: UiConstants.space16), // Space for bottom button
@@ -147,4 +126,10 @@ class _PersonalInfoContentState extends State<PersonalInfoContent> {
},
);
}
}
List<String> _toStringList(dynamic raw) {
if (raw is List<String>) return raw;
if (raw is List) return raw.map((dynamic e) => e.toString()).toList();
return <String>[];
}
}

View File

@@ -4,11 +4,11 @@ import 'package:design_system/design_system.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
/// A form widget containing all personal information fields.
///
/// Includes read-only fields for full name and email,
/// and editable fields for phone and address.
/// Includes read-only fields for full name,
/// and editable fields for email and phone.
/// The Preferred Locations row navigates to a dedicated Uber-style page.
/// Uses only design system tokens for colors, typography, and spacing.
class PersonalInfoForm extends StatelessWidget {
@@ -19,7 +19,7 @@ class PersonalInfoForm extends StatelessWidget {
required this.email,
required this.emailController,
required this.phoneController,
required this.locationsController,
required this.currentLocations,
this.enabled = true,
});
/// The staff member's full name (read-only).
@@ -34,8 +34,8 @@ class PersonalInfoForm extends StatelessWidget {
/// Controller for the phone number field.
final TextEditingController phoneController;
/// Controller for the address field.
final TextEditingController locationsController;
/// Current preferred locations list to show in the summary row.
final List<String> currentLocations;
/// Whether the form fields are enabled for editing.
final bool enabled;
@@ -43,6 +43,9 @@ class PersonalInfoForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
final String locationSummary = currentLocations.isEmpty
? i18n.locations_summary_none
: currentLocations.join(', ');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -69,15 +72,21 @@ class PersonalInfoForm extends StatelessWidget {
controller: phoneController,
hint: i18n.phone_hint,
enabled: enabled,
keyboardType: TextInputType.phone,
),
const SizedBox(height: UiConstants.space4),
_FieldLabel(text: i18n.locations_label),
const SizedBox(height: UiConstants.space2),
_EditableField(
controller: locationsController,
// Uber-style tappable row → navigates to PreferredLocationsPage
_TappableRow(
value: locationSummary,
hint: i18n.locations_hint,
icon: UiIcons.mapPin,
enabled: enabled,
onTap: enabled
? () => Modular.to.pushNamed(StaffPaths.preferredLocations)
: null,
),
const SizedBox(height: UiConstants.space4),
@@ -91,6 +100,68 @@ class PersonalInfoForm extends StatelessWidget {
}
}
/// An Uber-style tappable row for navigating to a sub-page editor.
/// Displays the current value (or hint if empty) and a chevron arrow.
class _TappableRow extends StatelessWidget {
const _TappableRow({
required this.value,
required this.hint,
required this.icon,
this.onTap,
this.enabled = true,
});
final String value;
final String hint;
final IconData icon;
final VoidCallback? onTap;
final bool enabled;
@override
Widget build(BuildContext context) {
final bool hasValue = value.isNotEmpty;
return GestureDetector(
onTap: enabled ? onTap : null,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: enabled ? UiColors.bgPopup : UiColors.bgSecondary,
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
border: Border.all(
color: enabled ? UiColors.border : UiColors.border.withValues(alpha: 0.5),
),
),
child: Row(
children: <Widget>[
Icon(icon, size: 18, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space2),
Expanded(
child: Text(
hasValue ? value : hint,
style: hasValue
? UiTypography.body2r.textPrimary
: UiTypography.body2r.textSecondary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (enabled)
Icon(
UiIcons.chevronRight,
size: 18,
color: UiColors.iconSecondary,
),
],
),
),
);
}
}
/// A language selector widget that displays the current language and navigates to language selection page.
class _LanguageSelector extends StatelessWidget {
const _LanguageSelector({
@@ -99,46 +170,43 @@ class _LanguageSelector extends StatelessWidget {
final bool enabled;
String _getLanguageLabel(AppLocale locale) {
switch (locale) {
case AppLocale.en:
return 'English';
case AppLocale.es:
return 'Español';
}
}
@override
Widget build(BuildContext context) {
final AppLocale currentLocale = LocaleSettings.currentLocale;
final String currentLanguage = _getLanguageLabel(currentLocale);
final String currentLocale = Localizations.localeOf(context).languageCode;
final String languageName = currentLocale == 'es' ? 'Español' : 'English';
return GestureDetector(
onTap: enabled
? () => Modular.to.pushNamed(StaffPaths.languageSelection)
: null,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: UiColors.bgPopup,
color: enabled ? UiColors.bgPopup : UiColors.bgSecondary,
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
border: Border.all(color: UiColors.border),
border: Border.all(
color: enabled ? UiColors.border : UiColors.border.withValues(alpha: 0.5),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
currentLanguage,
style: UiTypography.body2r.textPrimary,
),
Icon(
UiIcons.chevronRight,
color: UiColors.textSecondary,
const Icon(UiIcons.settings, size: 18, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Text(
languageName,
style: UiTypography.body2r.textPrimary,
),
),
if (enabled)
const Icon(
UiIcons.chevronRight,
size: 18,
color: UiColors.iconSecondary,
),
],
),
),
@@ -146,10 +214,7 @@ class _LanguageSelector extends StatelessWidget {
}
}
/// A label widget for form fields.
/// A label widget for form fields.
class _FieldLabel extends StatelessWidget {
const _FieldLabel({required this.text});
final String text;
@@ -157,13 +222,11 @@ class _FieldLabel extends StatelessWidget {
Widget build(BuildContext context) {
return Text(
text,
style: UiTypography.body2m.textPrimary,
style: UiTypography.titleUppercase3m.textSecondary,
);
}
}
/// A read-only field widget for displaying non-editable information.
/// A read-only field widget for displaying non-editable information.
class _ReadOnlyField extends StatelessWidget {
const _ReadOnlyField({required this.value});
final String value;
@@ -183,14 +246,12 @@ class _ReadOnlyField extends StatelessWidget {
),
child: Text(
value,
style: UiTypography.body2r.textPrimary,
style: UiTypography.body2r.textInactive,
),
);
}
}
/// An editable text field widget.
/// An editable text field widget.
class _EditableField extends StatelessWidget {
const _EditableField({
required this.controller,
@@ -232,7 +293,7 @@ class _EditableField extends StatelessWidget {
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
borderSide: const BorderSide(color: UiColors.primary),
),
fillColor: UiColors.bgPopup,
fillColor: enabled ? UiColors.bgPopup : UiColors.bgSecondary,
filled: true,
),
);

View File

@@ -9,6 +9,7 @@ import 'domain/usecases/update_personal_info_usecase.dart';
import 'presentation/blocs/personal_info_bloc.dart';
import 'presentation/pages/personal_info_page.dart';
import 'presentation/pages/language_selection_page.dart';
import 'presentation/pages/preferred_locations_page.dart';
/// The entry module for the Staff Profile Info feature.
///
@@ -61,5 +62,12 @@ class StaffProfileInfoModule extends Module {
),
child: (BuildContext context) => const LanguageSelectionPage(),
);
r.child(
StaffPaths.childRoute(
StaffPaths.onboardingPersonalInfo,
StaffPaths.preferredLocations,
),
child: (BuildContext context) => const PreferredLocationsPage(),
);
}
}

View File

@@ -30,6 +30,8 @@ dependencies:
firebase_auth: any
firebase_data_connect: any
google_places_flutter: ^2.1.1
http: ^1.2.2
dev_dependencies:
flutter_test:
sdk: flutter