From 015f1fbc1babd3c65d6755d6e20550337d2afcd8 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Mar 2026 03:06:28 -0500 Subject: [PATCH] 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. --- .../design_system/lib/src/ui_typography.dart | 10 + .../presentation/pages/certificates_page.dart | 5 +- .../widgets/certificates_header.dart | 32 +- .../presentation/pages/bank_account_page.dart | 192 +++--------- .../presentation/widgets/account_card.dart | 97 ++++++ .../presentation/widgets/security_notice.dart | 20 ++ .../presentation/pages/experience_page.dart | 16 +- .../widgets/experience_section_title.dart | 26 +- .../pages/personal_info_page.dart | 6 +- .../widgets/personal_info_form.dart | 293 ------------------ .../personal_info_page/editable_field.dart | 63 ++++ .../personal_info_page/field_label.dart | 16 + .../personal_info_page/language_selector.dart | 54 ++++ .../personal_info_content.dart | 14 +- .../personal_info_form.dart | 99 ++++++ .../personal_info_page/read_only_field.dart | 28 ++ .../personal_info_page/tappable_row.dart | 75 +++++ .../src/presentation/widgets/save_button.dart | 46 +-- 18 files changed, 562 insertions(+), 530 deletions(-) create mode 100644 apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/account_card.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/security_notice.dart delete mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/editable_field.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/field_label.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/language_selector.dart rename apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/{ => personal_info_page}/personal_info_content.dart (88%) create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_form.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/read_only_field.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/tappable_row.dart diff --git a/apps/mobile/packages/design_system/lib/src/ui_typography.dart b/apps/mobile/packages/design_system/lib/src/ui_typography.dart index 8e1ce9bb..fde6263a 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_typography.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_typography.dart @@ -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, diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart index 2b0d95ee..21e2c4c7 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificates_page.dart @@ -54,7 +54,10 @@ class CertificatesPage extends StatelessWidget { final List documents = state.certificates; return Scaffold( - backgroundColor: UiColors.background, // Matches 0xFFF8FAFC + appBar: UiAppBar( + title: t.staff_certificates.title, + showBackButton: true, + ), body: SingleChildScrollView( child: Column( children: [ diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_header.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_header.dart index d270b5f4..925f415c 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_header.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificates_header.dart @@ -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: [ - UiColors.primary, UiColors.primary.withValues(alpha: 0.8), + UiColors.primary.withValues(alpha: 0.5), ], ), ), child: Column( children: [ - Row( - children: [ - 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: [ SizedBox( diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart index ca23581e..3f814544 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart @@ -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( bloc: cubit, @@ -88,18 +80,51 @@ class BankAccountPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSecurityNotice(strings), + SecurityNotice(strings: strings), const SizedBox(height: UiConstants.space6), - Text( - strings.linked_accounts, - style: UiTypography.headline4m.copyWith( - color: UiColors.textPrimary, + if (state.accounts.isEmpty) + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space10, + ), + child: Column( + children: [ + 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 ...[ + Text( + strings.linked_accounts, + style: UiTypography.headline4m.copyWith( + color: UiColors.textPrimary, + ), ), - ), - const SizedBox(height: UiConstants.space3), - ...state.accounts.map( - (StaffBankAccount a) => _buildAccountCard(a, strings), - ), // Added type + const SizedBox(height: UiConstants.space3), + ...state.accounts.map( + (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: [ - const Icon(UiIcons.shield, color: UiColors.primary, size: 20), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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: [ - Row( - children: [ - 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: [ - 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: [ - const Icon( - UiIcons.check, - size: UiConstants.iconXs, - color: primaryColor, - ), - const SizedBox(width: UiConstants.space1), - Text(strings.primary, style: UiTypography.body3m.primary), - ], - ), - ), - ], - ), - ); - } } diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/account_card.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/account_card.dart new file mode 100644 index 00000000..bf9356c9 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/account_card.dart @@ -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: [ + Row( + children: [ + 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: [ + 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: [ + const Icon( + UiIcons.check, + size: UiConstants.iconXs, + color: primaryColor, + ), + const SizedBox(width: UiConstants.space1), + Text(strings.primary, style: UiTypography.body3m.primary), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/security_notice.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/security_notice.dart new file mode 100644 index 00000000..b0739b2f --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/security_notice.dart @@ -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, + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/pages/experience_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/pages/experience_page.dart index 7b42e3d0..e33628af 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/pages/experience_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/pages/experience_page.dart @@ -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( diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/widgets/experience_section_title.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/widgets/experience_section_title.dart index 7a588933..28cfc255 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/widgets/experience_section_title.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/experience/lib/src/presentation/widgets/experience_section_title.dart @@ -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( - title, - style: UiTypography.title2m.copyWith( - color: UiColors.textPrimary, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: UiTypography.title2m, + ), + if (subtitle != null) ...[ + Text( + subtitle!, + style: UiTypography.body2r.textSecondary, + ), + ], + ], ), ); } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart index 239f5bce..a7cbf5cc 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart @@ -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. /// diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart deleted file mode 100644 index 5a50f6ef..00000000 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart +++ /dev/null @@ -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 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: [ - _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 [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: [ - 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: [ - 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? 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, - ), - ); - } -} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/editable_field.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/editable_field.dart new file mode 100644 index 00000000..97010bc3 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/editable_field.dart @@ -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? 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, + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/field_label.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/field_label.dart new file mode 100644 index 00000000..59c49bde --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/field_label.dart @@ -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); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/language_selector.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/language_selector.dart new file mode 100644 index 00000000..30bdff3d --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/language_selector.dart @@ -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: [ + 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, + ), + ], + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_content.dart similarity index 88% rename from apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart rename to apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_content.dart index 944f5297..9481bac6 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_content.dart @@ -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. /// diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_form.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_form.dart new file mode 100644 index 00000000..38c774b7 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_form.dart @@ -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 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: [ + 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 [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), + ], + ); + } +} \ No newline at end of file diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/read_only_field.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/read_only_field.dart new file mode 100644 index 00000000..2bd956a0 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/read_only_field.dart @@ -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), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/tappable_row.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/tappable_row.dart new file mode 100644 index 00000000..6d92aa9e --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/tappable_row.dart @@ -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: [ + 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, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart index ea03339b..e13c0681 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart @@ -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( - 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( - UiColors.bgPopup, - ), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(UiIcons.check, color: UiColors.bgPopup, size: 20), - const SizedBox(width: UiConstants.space2), - Text( - label, - style: UiTypography.body1m.copyWith( - color: UiColors.bgPopup, - ), - ), - ], - ), - ), + child: UiButton.primary( + fullWidth: true, + onPressed: onPressed, + text: label, + isLoading: isLoading, ), ), );