feat: Refactor onboarding experience and personal info pages

- Updated ExperiencePage to include subtitles in ExperienceSectionTitle.
- Modified ExperienceSectionTitle widget to accept an optional subtitle parameter.
- Refactored PersonalInfoPage to improve imports and structure.
- Removed unused PersonalInfoContent and PersonalInfoForm widgets.
- Introduced new widgets: EditableField, FieldLabel, ReadOnlyField, TappableRow, and LanguageSelector for better modularity.
- Added AccountCard and SecurityNotice widgets for bank account section.
- Enhanced SaveButton to utilize UiButton for consistency.
This commit is contained in:
Achintha Isuru
2026-03-01 03:06:28 -05:00
parent ea6b3fcc76
commit 015f1fbc1b
18 changed files with 562 additions and 530 deletions

View File

@@ -264,6 +264,16 @@ class UiTypography {
color: UiColors.textPrimary,
);
/// Title Uppercase 2 Bold - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: 0.7 (#121826)
/// Used for section headers and important labels.
static final TextStyle titleUppercase2b = _primaryBase.copyWith(
fontWeight: FontWeight.w700,
fontSize: 14,
height: 1.5,
letterSpacing: 0.4,
color: UiColors.textPrimary,
);
/// Title Uppercase 3 Medium - Font: Instrument Sans, Size: 12, Height: 1.5, Spacing: 1.5 (#121826)
static final TextStyle titleUppercase3m = _primaryBase.copyWith(
fontWeight: FontWeight.w500,

View File

@@ -54,7 +54,10 @@ class CertificatesPage extends StatelessWidget {
final List<StaffCertificate> documents = state.certificates;
return Scaffold(
backgroundColor: UiColors.background, // Matches 0xFFF8FAFC
appBar: UiAppBar(
title: t.staff_certificates.title,
showBackButton: true,
),
body: SingleChildScrollView(
child: Column(
children: <Widget>[

View File

@@ -1,8 +1,6 @@
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';
import 'package:krow_core/core.dart';
import 'package:core_localization/core_localization.dart';
class CertificatesHeader extends StatelessWidget {
const CertificatesHeader({
@@ -36,39 +34,13 @@ class CertificatesHeader extends StatelessWidget {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[
UiColors.primary,
UiColors.primary.withValues(alpha: 0.8),
UiColors.primary.withValues(alpha: 0.5),
],
),
),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
GestureDetector(
onTap: () => Modular.to.popSafe(),
child: Container(
width: UiConstants.space10,
height: UiConstants.space10,
decoration: BoxDecoration(
color: UiColors.white.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
UiIcons.chevronLeft,
color: UiColors.white,
size: UiConstants.iconMd,
),
),
),
const SizedBox(width: UiConstants.space3),
Text(
t.staff_certificates.title,
style: UiTypography.headline3m.white,
),
],
),
const SizedBox(height: UiConstants.space8),
Row(
children: <Widget>[
SizedBox(

View File

@@ -1,14 +1,16 @@
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:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/bank_account_cubit.dart';
import '../blocs/bank_account_state.dart';
import '../widgets/account_card.dart';
import '../widgets/add_account_form.dart';
import '../widgets/security_notice.dart';
class BankAccountPage extends StatelessWidget {
const BankAccountPage({super.key});
@@ -26,19 +28,9 @@ class BankAccountPage extends StatelessWidget {
final dynamic strings = t.staff.profile.bank_account_page;
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
backgroundColor: UiColors.background, // Was surface
elevation: 0,
leading: IconButton(
icon: const Icon(UiIcons.arrowLeft, color: UiColors.textSecondary),
onPressed: () => Modular.to.popSafe(),
),
title: Text(strings.title, style: UiTypography.headline3m.textPrimary),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
appBar: UiAppBar(
title: strings.title,
showBackButton: true,
),
body: BlocConsumer<BankAccountCubit, BankAccountState>(
bloc: cubit,
@@ -88,8 +80,37 @@ class BankAccountPage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildSecurityNotice(strings),
SecurityNotice(strings: strings),
const SizedBox(height: UiConstants.space6),
if (state.accounts.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: UiConstants.space10,
),
child: Column(
children: <Widget>[
const Icon(
UiIcons.building,
size: 48,
color: UiColors.iconSecondary,
),
const SizedBox(height: UiConstants.space4),
Text(
'No accounts yet',
style: UiTypography.headline4m,
textAlign: TextAlign.center,
),
Text(
'Add your first bank account to get started',
style: UiTypography.body2m.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
)
else ...<Widget>[
Text(
strings.linked_accounts,
style: UiTypography.headline4m.copyWith(
@@ -97,9 +118,13 @@ class BankAccountPage extends StatelessWidget {
),
),
const SizedBox(height: UiConstants.space3),
...state.accounts.map(
(StaffBankAccount a) => _buildAccountCard(a, strings),
), // Added type
...state.accounts.map<Widget>(
(StaffBankAccount account) => AccountCard(
account: account,
strings: strings,
),
),
],
// Add extra padding at bottom
const SizedBox(height: UiConstants.space20),
],
@@ -157,119 +182,4 @@ class BankAccountPage extends StatelessWidget {
),
);
}
Widget _buildSecurityNotice(dynamic strings) {
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Icon(UiIcons.shield, color: UiColors.primary, size: 20),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
strings.secure_title,
style: UiTypography.body2m.textPrimary,
),
const SizedBox(height: UiConstants.space1 - 2), // 2px
Text(
strings.secure_subtitle,
style: UiTypography.body3r.textSecondary,
),
],
),
),
],
),
);
}
Widget _buildAccountCard(StaffBankAccount account, dynamic strings) {
final bool isPrimary = account.isPrimary;
const Color primaryColor = UiColors.primary;
return Container(
margin: const EdgeInsets.only(bottom: UiConstants.space3),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.bgPopup, // Was surface, using bgPopup (white) for card
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(
color: isPrimary ? primaryColor : UiColors.border,
width: isPrimary ? 2 : 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: const Center(
child: Icon(
UiIcons.building,
color: primaryColor,
size: UiConstants.iconLg,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
account.bankName,
style: UiTypography.body2m.textPrimary,
),
Text(
strings.account_ending(
last4: account.last4?.isNotEmpty == true
? account.last4!
: '----',
),
style: UiTypography.body2r.textSecondary,
),
],
),
],
),
if (isPrimary)
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: UiConstants.space1,
),
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: 0.15),
borderRadius: UiConstants.radiusFull,
),
child: Row(
children: <Widget>[
const Icon(
UiIcons.check,
size: UiConstants.iconXs,
color: primaryColor,
),
const SizedBox(width: UiConstants.space1),
Text(strings.primary, style: UiTypography.body3m.primary),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,97 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
class AccountCard extends StatelessWidget {
final StaffBankAccount account;
final dynamic strings;
const AccountCard({
super.key,
required this.account,
required this.strings,
});
@override
Widget build(BuildContext context) {
final bool isPrimary = account.isPrimary;
const Color primaryColor = UiColors.primary;
return Container(
margin: const EdgeInsets.only(bottom: UiConstants.space3),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(
color: isPrimary ? primaryColor : UiColors.border,
width: isPrimary ? 2 : 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: const Center(
child: Icon(
UiIcons.building,
color: primaryColor,
size: UiConstants.iconLg,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
account.bankName,
style: UiTypography.body2m.textPrimary,
),
Text(
strings.account_ending(
last4: account.last4?.isNotEmpty == true
? account.last4!
: '----',
),
style: UiTypography.body2r.textSecondary,
),
],
),
],
),
if (isPrimary)
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: UiConstants.space1,
),
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: 0.15),
borderRadius: UiConstants.radiusFull,
),
child: Row(
children: <Widget>[
const Icon(
UiIcons.check,
size: UiConstants.iconXs,
color: primaryColor,
),
const SizedBox(width: UiConstants.space1),
Text(strings.primary, style: UiTypography.body3m.primary),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
class SecurityNotice extends StatelessWidget {
final dynamic strings;
const SecurityNotice({
super.key,
required this.strings,
});
@override
Widget build(BuildContext context) {
return UiNoticeBanner(
icon: UiIcons.shield,
title: strings.secure_title,
description: strings.secure_subtitle,
);
}
}

View File

@@ -116,10 +116,9 @@ class ExperiencePage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ExperienceSectionTitle(title: i18n.industries_title),
Text(
i18n.industries_subtitle,
style: UiTypography.body2m.textSecondary,
ExperienceSectionTitle(
title: i18n.industries_title,
subtitle: i18n.industries_subtitle,
),
const SizedBox(height: UiConstants.space3),
Wrap(
@@ -142,11 +141,10 @@ class ExperiencePage extends StatelessWidget {
)
.toList(),
),
const SizedBox(height: UiConstants.space6),
ExperienceSectionTitle(title: i18n.skills_title),
Text(
i18n.skills_subtitle,
style: UiTypography.body2m.textSecondary,
const SizedBox(height: UiConstants.space10),
ExperienceSectionTitle(
title: i18n.skills_title,
subtitle: i18n.skills_subtitle,
),
const SizedBox(height: UiConstants.space3),
Wrap(

View File

@@ -3,17 +3,31 @@ import 'package:flutter/material.dart';
class ExperienceSectionTitle extends StatelessWidget {
final String title;
const ExperienceSectionTitle({super.key, required this.title});
final String? subtitle;
const ExperienceSectionTitle({
super.key,
required this.title,
this.subtitle,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: UiConstants.space2),
child: Text(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: UiTypography.title2m.copyWith(
color: UiColors.textPrimary,
style: UiTypography.title2m,
),
if (subtitle != null) ...[
Text(
subtitle!,
style: UiTypography.body2r.textSecondary,
),
],
],
),
);
}

View File

@@ -4,10 +4,10 @@ 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:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart';
import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/personal_info_content.dart';
import '../blocs/personal_info_bloc.dart';
import '../blocs/personal_info_state.dart';
import '../widgets/personal_info_content.dart';
/// The Personal Info page for staff onboarding.
///

View File

@@ -1,293 +0,0 @@
import 'package:flutter/material.dart';
import 'package:core_localization/core_localization.dart';
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 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 {
/// Creates a [PersonalInfoForm].
const PersonalInfoForm({
super.key,
required this.fullName,
required this.email,
required this.emailController,
required this.phoneController,
required this.currentLocations,
this.enabled = true,
});
/// The staff member's full name (read-only).
final String fullName;
/// The staff member's email (read-only).
final String email;
/// Controller for the email field.
final TextEditingController emailController;
/// Controller for the phone number field.
final TextEditingController phoneController;
/// 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;
@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,
children: <Widget>[
_FieldLabel(text: i18n.full_name_label),
const SizedBox(height: UiConstants.space2),
_ReadOnlyField(value: fullName),
const SizedBox(height: UiConstants.space4),
_FieldLabel(text: i18n.email_label),
const SizedBox(height: UiConstants.space2),
_EditableField(
controller: emailController,
hint: i18n.email_label,
enabled: enabled,
keyboardType: TextInputType.emailAddress,
autofillHints: const <String>[AutofillHints.email],
),
const SizedBox(height: UiConstants.space4),
_FieldLabel(text: i18n.phone_label),
const SizedBox(height: UiConstants.space2),
_EditableField(
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),
// Uber-style tappable row → navigates to PreferredLocationsPage
_TappableRow(
value: locationSummary,
hint: i18n.locations_hint,
icon: UiIcons.mapPin,
enabled: enabled,
onTap: enabled ? () => Modular.to.toPreferredLocations() : null,
),
const SizedBox(height: UiConstants.space4),
const _FieldLabel(text: 'Language'),
const SizedBox(height: UiConstants.space2),
_LanguageSelector(enabled: enabled),
],
);
}
}
/// 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)
const 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({this.enabled = true});
final bool enabled;
@override
Widget build(BuildContext context) {
final String currentLocale = Localizations.localeOf(context).languageCode;
final String languageName = currentLocale == 'es' ? 'Español' : 'English';
return GestureDetector(
onTap: enabled ? () => Modular.to.toLanguageSelection() : null,
child: Container(
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>[
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,
),
],
),
),
);
}
}
class _FieldLabel extends StatelessWidget {
const _FieldLabel({required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(text, style: UiTypography.titleUppercase3m.textSecondary);
}
}
class _ReadOnlyField extends StatelessWidget {
const _ReadOnlyField({required this.value});
final String value;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: UiColors.bgSecondary,
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
border: Border.all(color: UiColors.border),
),
child: Text(value, style: UiTypography.body2r.textInactive),
);
}
}
class _EditableField extends StatelessWidget {
const _EditableField({
required this.controller,
required this.hint,
this.enabled = true,
this.keyboardType,
this.autofillHints,
});
final TextEditingController controller;
final String hint;
final bool enabled;
final TextInputType? keyboardType;
final Iterable<String>? autofillHints;
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
enabled: enabled,
keyboardType: keyboardType,
autofillHints: autofillHints,
style: UiTypography.body2r.textPrimary,
decoration: InputDecoration(
hintText: hint,
hintStyle: UiTypography.body2r.textSecondary,
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),
),
fillColor: enabled ? UiColors.bgPopup : UiColors.bgSecondary,
filled: true,
),
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// An editable text field widget.
class EditableField extends StatelessWidget {
/// Creates an [EditableField].
const EditableField({
super.key,
required this.controller,
required this.hint,
this.enabled = true,
this.keyboardType,
this.autofillHints,
});
/// The text editing controller.
final TextEditingController controller;
/// The hint text to display when empty.
final String hint;
/// Whether the field is enabled for editing.
final bool enabled;
/// The keyboard type for the field.
final TextInputType? keyboardType;
/// Autofill hints for the field.
final Iterable<String>? autofillHints;
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
enabled: enabled,
keyboardType: keyboardType,
autofillHints: autofillHints,
style: UiTypography.body2r.textPrimary,
decoration: InputDecoration(
hintText: hint,
hintStyle: UiTypography.body2r.textSecondary,
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),
),
fillColor: enabled ? UiColors.bgPopup : UiColors.bgSecondary,
filled: true,
),
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A label widget for form fields.
class FieldLabel extends StatelessWidget {
/// Creates a [FieldLabel].
const FieldLabel({super.key, required this.text});
/// The label text to display.
final String text;
@override
Widget build(BuildContext context) {
return Text(text, style: UiTypography.titleUppercase2b.textSecondary);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/field_label.dart';
/// A language selector widget that displays the current language and navigates to language selection page.
class LanguageSelector extends StatelessWidget {
/// Creates a [LanguageSelector].
const LanguageSelector({super.key, this.enabled = true});
/// Whether the selector is enabled for interaction.
final bool enabled;
@override
Widget build(BuildContext context) {
final String currentLocale = Localizations.localeOf(context).languageCode;
final String languageName = currentLocale == 'es' ? 'Español' : 'English';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space3,
children: [
const FieldLabel(text: 'Language'),
GestureDetector(
onTap: enabled ? () => Modular.to.toLanguageSelection() : null,
child: Row(
children: <Widget>[
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: 16,
color: UiColors.iconSecondary,
),
],
),
),
],
);
}
}

View File

@@ -3,14 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/personal_info_bloc.dart';
import '../blocs/personal_info_event.dart';
import '../blocs/personal_info_state.dart';
import 'profile_photo_widget.dart';
import 'personal_info_form.dart';
import 'save_button.dart';
import 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart';
import 'package:staff_profile_info/src/presentation/blocs/personal_info_event.dart';
import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/personal_info_form.dart';
import 'package:staff_profile_info/src/presentation/widgets/profile_photo_widget.dart';
import 'package:staff_profile_info/src/presentation/widgets/save_button.dart';
/// Content widget that displays and manages the staff profile form.
///

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/editable_field.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/field_label.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/language_selector.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/read_only_field.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/tappable_row.dart';
/// A form widget containing all personal information fields.
///
/// 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 {
/// Creates a [PersonalInfoForm].
const PersonalInfoForm({
super.key,
required this.fullName,
required this.email,
required this.emailController,
required this.phoneController,
required this.currentLocations,
this.enabled = true,
});
/// The staff member's full name (read-only).
final String fullName;
/// The staff member's email (read-only).
final String email;
/// Controller for the email field.
final TextEditingController emailController;
/// Controller for the phone number field.
final TextEditingController phoneController;
/// 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;
@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,
children: <Widget>[
FieldLabel(text: i18n.full_name_label),
const SizedBox(height: UiConstants.space2),
ReadOnlyField(value: fullName),
const SizedBox(height: UiConstants.space4),
FieldLabel(text: i18n.email_label),
const SizedBox(height: UiConstants.space2),
EditableField(
controller: emailController,
hint: i18n.email_label,
enabled: enabled,
keyboardType: TextInputType.emailAddress,
autofillHints: const <String>[AutofillHints.email],
),
const SizedBox(height: UiConstants.space4),
FieldLabel(text: i18n.phone_label),
const SizedBox(height: UiConstants.space2),
EditableField(
controller: phoneController,
hint: i18n.phone_hint,
enabled: enabled,
keyboardType: TextInputType.phone,
),
const SizedBox(height: UiConstants.space4),
TappableRow(
value: locationSummary,
hint: i18n.locations_hint,
icon: UiIcons.mapPin,
enabled: enabled,
onTap: enabled ? () => Modular.to.toPreferredLocations() : null,
),
const SizedBox(height: UiConstants.space6),
const Divider(),
const SizedBox(height: UiConstants.space6),
LanguageSelector(enabled: enabled),
],
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A read-only text field widget.
class ReadOnlyField extends StatelessWidget {
/// Creates a [ReadOnlyField].
const ReadOnlyField({super.key, required this.value});
/// The value to display.
final String value;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: UiColors.bgSecondary,
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
border: Border.all(color: UiColors.border),
),
child: Text(value, style: UiTypography.body2r.textInactive),
);
}
}

View File

@@ -0,0 +1,75 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/field_label.dart';
/// 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 {
/// Creates a [TappableRow].
const TappableRow({
super.key,
required this.value,
required this.hint,
required this.icon,
this.onTap,
this.enabled = true,
});
/// The current value to display.
final String value;
/// The hint text to display when value is empty.
final String hint;
/// The icon to display on the left.
final IconData icon;
/// Callback when the row is tapped.
final VoidCallback? onTap;
/// Whether the row is enabled for tapping.
final bool enabled;
@override
Widget build(BuildContext context) {
final bool hasValue = value.isNotEmpty;
final TranslationsStaffOnboardingPersonalInfoEn i18n =
t.staff.onboarding.personal_info;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space3,
children: [
FieldLabel(text: i18n.locations_label),
GestureDetector(
onTap: enabled ? onTap : null,
child: Container(
width: double.infinity,
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)
const Icon(
UiIcons.chevronRight,
size: 16,
color: UiColors.iconSecondary,
),
],
),
),
),
],
);
}
}

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A save button widget for the bottom of the personal info page.
@@ -31,47 +31,15 @@ class SaveButton extends StatelessWidget {
decoration: const BoxDecoration(
color: UiColors.bgPopup,
border: Border(
top: BorderSide(color: UiColors.border),
top: BorderSide(color: UiColors.border, width: 0.5),
),
),
child: SafeArea(
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
child: UiButton.primary(
fullWidth: true,
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
),
elevation: 0,
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
UiColors.bgPopup,
),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(UiIcons.check, color: UiColors.bgPopup, size: 20),
const SizedBox(width: UiConstants.space2),
Text(
label,
style: UiTypography.body1m.copyWith(
color: UiColors.bgPopup,
),
),
],
),
),
text: label,
isLoading: isLoading,
),
),
);