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:
@@ -264,6 +264,16 @@ class UiTypography {
|
|||||||
color: UiColors.textPrimary,
|
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)
|
/// Title Uppercase 3 Medium - Font: Instrument Sans, Size: 12, Height: 1.5, Spacing: 1.5 (#121826)
|
||||||
static final TextStyle titleUppercase3m = _primaryBase.copyWith(
|
static final TextStyle titleUppercase3m = _primaryBase.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ class CertificatesPage extends StatelessWidget {
|
|||||||
final List<StaffCertificate> documents = state.certificates;
|
final List<StaffCertificate> documents = state.certificates;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.background, // Matches 0xFFF8FAFC
|
appBar: UiAppBar(
|
||||||
|
title: t.staff_certificates.title,
|
||||||
|
showBackButton: true,
|
||||||
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.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 {
|
class CertificatesHeader extends StatelessWidget {
|
||||||
const CertificatesHeader({
|
const CertificatesHeader({
|
||||||
@@ -36,39 +34,13 @@ class CertificatesHeader extends StatelessWidget {
|
|||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: <Color>[
|
colors: <Color>[
|
||||||
UiColors.primary,
|
|
||||||
UiColors.primary.withValues(alpha: 0.8),
|
UiColors.primary.withValues(alpha: 0.8),
|
||||||
|
UiColors.primary.withValues(alpha: 0.5),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
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(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|||||||
@@ -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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.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_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import '../blocs/bank_account_cubit.dart';
|
import '../blocs/bank_account_cubit.dart';
|
||||||
import '../blocs/bank_account_state.dart';
|
import '../blocs/bank_account_state.dart';
|
||||||
|
import '../widgets/account_card.dart';
|
||||||
import '../widgets/add_account_form.dart';
|
import '../widgets/add_account_form.dart';
|
||||||
|
import '../widgets/security_notice.dart';
|
||||||
|
|
||||||
class BankAccountPage extends StatelessWidget {
|
class BankAccountPage extends StatelessWidget {
|
||||||
const BankAccountPage({super.key});
|
const BankAccountPage({super.key});
|
||||||
@@ -26,19 +28,9 @@ class BankAccountPage extends StatelessWidget {
|
|||||||
final dynamic strings = t.staff.profile.bank_account_page;
|
final dynamic strings = t.staff.profile.bank_account_page;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.background,
|
appBar: UiAppBar(
|
||||||
appBar: AppBar(
|
title: strings.title,
|
||||||
backgroundColor: UiColors.background, // Was surface
|
showBackButton: true,
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: BlocConsumer<BankAccountCubit, BankAccountState>(
|
body: BlocConsumer<BankAccountCubit, BankAccountState>(
|
||||||
bloc: cubit,
|
bloc: cubit,
|
||||||
@@ -88,8 +80,37 @@ class BankAccountPage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildSecurityNotice(strings),
|
SecurityNotice(strings: strings),
|
||||||
const SizedBox(height: UiConstants.space6),
|
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(
|
Text(
|
||||||
strings.linked_accounts,
|
strings.linked_accounts,
|
||||||
style: UiTypography.headline4m.copyWith(
|
style: UiTypography.headline4m.copyWith(
|
||||||
@@ -97,9 +118,13 @@ class BankAccountPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
...state.accounts.map(
|
...state.accounts.map<Widget>(
|
||||||
(StaffBankAccount a) => _buildAccountCard(a, strings),
|
(StaffBankAccount account) => AccountCard(
|
||||||
), // Added type
|
account: account,
|
||||||
|
strings: strings,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
// Add extra padding at bottom
|
// Add extra padding at bottom
|
||||||
const SizedBox(height: UiConstants.space20),
|
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,10 +116,9 @@ class ExperiencePage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ExperienceSectionTitle(title: i18n.industries_title),
|
ExperienceSectionTitle(
|
||||||
Text(
|
title: i18n.industries_title,
|
||||||
i18n.industries_subtitle,
|
subtitle: i18n.industries_subtitle,
|
||||||
style: UiTypography.body2m.textSecondary,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
Wrap(
|
Wrap(
|
||||||
@@ -142,11 +141,10 @@ class ExperiencePage extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space10),
|
||||||
ExperienceSectionTitle(title: i18n.skills_title),
|
ExperienceSectionTitle(
|
||||||
Text(
|
title: i18n.skills_title,
|
||||||
i18n.skills_subtitle,
|
subtitle: i18n.skills_subtitle,
|
||||||
style: UiTypography.body2m.textSecondary,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
Wrap(
|
Wrap(
|
||||||
|
|||||||
@@ -3,17 +3,31 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class ExperienceSectionTitle extends StatelessWidget {
|
class ExperienceSectionTitle extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
const ExperienceSectionTitle({super.key, required this.title});
|
final String? subtitle;
|
||||||
|
const ExperienceSectionTitle({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: UiConstants.space2),
|
padding: EdgeInsets.only(bottom: UiConstants.space2),
|
||||||
child: Text(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
title,
|
title,
|
||||||
style: UiTypography.title2m.copyWith(
|
style: UiTypography.title2m,
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
),
|
||||||
|
if (subtitle != null) ...[
|
||||||
|
Text(
|
||||||
|
subtitle!,
|
||||||
|
style: UiTypography.body2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.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.
|
/// The Personal Info page for staff onboarding.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart';
|
||||||
import '../blocs/personal_info_bloc.dart';
|
import 'package:staff_profile_info/src/presentation/blocs/personal_info_event.dart';
|
||||||
import '../blocs/personal_info_event.dart';
|
import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart';
|
||||||
import '../blocs/personal_info_state.dart';
|
import 'package:staff_profile_info/src/presentation/widgets/personal_info_page/personal_info_form.dart';
|
||||||
import 'profile_photo_widget.dart';
|
import 'package:staff_profile_info/src/presentation/widgets/profile_photo_widget.dart';
|
||||||
import 'personal_info_form.dart';
|
import 'package:staff_profile_info/src/presentation/widgets/save_button.dart';
|
||||||
import 'save_button.dart';
|
|
||||||
|
|
||||||
|
|
||||||
/// Content widget that displays and manages the staff profile form.
|
/// Content widget that displays and manages the staff profile form.
|
||||||
///
|
///
|
||||||
@@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:design_system/design_system.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.
|
/// A save button widget for the bottom of the personal info page.
|
||||||
@@ -31,47 +31,15 @@ class SaveButton extends StatelessWidget {
|
|||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: UiColors.bgPopup,
|
color: UiColors.bgPopup,
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(color: UiColors.border),
|
top: BorderSide(color: UiColors.border, width: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: SizedBox(
|
child: UiButton.primary(
|
||||||
width: double.infinity,
|
fullWidth: true,
|
||||||
height: 48,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
style: ElevatedButton.styleFrom(
|
text: label,
|
||||||
backgroundColor: UiColors.primary,
|
isLoading: isLoading,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user