diff --git a/frontend/pshared/lib/models/account/base.dart b/frontend/pshared/lib/models/account/base.dart index fd5722e..5275587 100644 --- a/frontend/pshared/lib/models/account/base.dart +++ b/frontend/pshared/lib/models/account/base.dart @@ -20,6 +20,14 @@ class AccountBase implements StorableDescribable { DateTime get updatedAt => storable.updatedAt; @override String get name => describable.name; + String get fullName { + final first = describable.name.trim(); + final last = lastName.trim(); + + if (last.isEmpty) return first; + if (first.isEmpty) return last; + return '$first $last'; + } @override String? get description => describable.description; @@ -32,7 +40,7 @@ class AccountBase implements StorableDescribable { required this.lastName, }); - String get nameInitials => getNameInitials(describable.name); + String get nameInitials => getNameInitials(fullName); AccountBase copyWith({ Describable? describable, diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart index 7376394..576ac0f 100644 --- a/frontend/pshared/lib/provider/account.dart +++ b/frontend/pshared/lib/provider/account.dart @@ -203,6 +203,7 @@ class AccountProvider extends ChangeNotifier { Future update({ Describable? describable, + String? lastName, String? locale, String? avatarUrl, String? notificationFrequency, @@ -213,6 +214,7 @@ class AccountProvider extends ChangeNotifier { final updated = await AccountService.update( account!.copyWith( describable: describable, + lastName: lastName, avatarUrl: () => avatarUrl ?? account!.avatarUrl, locale: locale ?? account!.locale, ), @@ -250,10 +252,11 @@ class AccountProvider extends ChangeNotifier { } } - Future resetUsername(String userName) async { + Future resetUsername(String userName, {String? lastName}) async { if (account == null) throw ErrorUnauthorized(); return update( describable: account!.describable.copyWith(name: userName), + lastName: lastName ?? account!.lastName, ); } diff --git a/frontend/pweb/lib/pages/settings/profile/account/name/name.dart b/frontend/pweb/lib/pages/settings/profile/account/name/name.dart index 4539e98..f0e8288 100644 --- a/frontend/pweb/lib/pages/settings/profile/account/name/name.dart +++ b/frontend/pweb/lib/pages/settings/profile/account/name/name.dart @@ -17,16 +17,20 @@ class _AccountNameConstants { } class AccountName extends StatelessWidget { - final String name; + final String firstName; + final String lastName; final String title; final String hintText; + final String lastNameHint; final String errorText; const AccountName({ super.key, - required this.name, + required this.firstName, + required this.lastName, required this.title, required this.hintText, + required this.lastNameHint, required this.errorText, }); @@ -34,12 +38,14 @@ class AccountName extends StatelessWidget { Widget build(BuildContext context) { return ChangeNotifierProvider( create: (ctx) => AccountNameState( - initialName: name, + initialFirstName: firstName, + initialLastName: lastName, errorMessage: errorText, accountProvider: ctx.read(), ), child: _AccountNameBody( hintText: hintText, + lastNameHint: lastNameHint, ), ); } @@ -48,9 +54,11 @@ class AccountName extends StatelessWidget { class _AccountNameBody extends StatelessWidget { const _AccountNameBody({ required this.hintText, + required this.lastNameHint, }); final String hintText; + final String lastNameHint; @override Widget build(BuildContext context) { @@ -58,8 +66,9 @@ class _AccountNameBody extends StatelessWidget { final provider = context.watch(); final theme = Theme.of(context); - final currentName = provider.account?.name ?? state.initialName; - state.syncName(currentName); + final currentFirstName = provider.account?.name ?? state.initialFirstName; + final currentLastName = provider.account?.lastName ?? state.initialLastName; + state.syncNames(currentFirstName, currentLastName); return Column( mainAxisSize: MainAxisSize.min, @@ -69,6 +78,7 @@ class _AccountNameBody extends StatelessWidget { children: [ AccountNameText( hintText: hintText, + lastNameHint: lastNameHint, inputWidth: _AccountNameConstants.inputWidth, borderWidth: _AccountNameConstants.borderWidth, ), diff --git a/frontend/pweb/lib/pages/settings/profile/account/name/text.dart b/frontend/pweb/lib/pages/settings/profile/account/name/text.dart index d077d4d..566c959 100644 --- a/frontend/pweb/lib/pages/settings/profile/account/name/text.dart +++ b/frontend/pweb/lib/pages/settings/profile/account/name/text.dart @@ -9,11 +9,13 @@ class AccountNameText extends StatelessWidget { const AccountNameText({ super.key, required this.hintText, + required this.lastNameHint, required this.inputWidth, required this.borderWidth, }); final String hintText; + final String lastNameHint; final double inputWidth; final double borderWidth; @@ -25,29 +27,55 @@ class AccountNameText extends StatelessWidget { if (state.isEditing) { return SizedBox( width: inputWidth, - child: TextFormField( - controller: state.controller, - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - autofocus: true, - enabled: !state.isBusy, - decoration: InputDecoration( - hintText: hintText, - isDense: true, - border: UnderlineInputBorder( - borderSide: BorderSide( - color: theme.colorScheme.primary, - width: borderWidth, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: state.firstNameController, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + autofocus: true, + enabled: !state.isBusy, + decoration: InputDecoration( + hintText: hintText, + labelText: hintText, + isDense: true, + border: UnderlineInputBorder( + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: borderWidth, + ), + ), ), ), - ), + const SizedBox(height: 8), + TextFormField( + controller: state.lastNameController, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + enabled: !state.isBusy, + decoration: InputDecoration( + hintText: lastNameHint, + labelText: lastNameHint, + isDense: true, + border: UnderlineInputBorder( + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: borderWidth, + ), + ), + ), + ), + ], ), ); } + final displayName = state.currentFullName.isNotEmpty ? state.currentFullName : hintText; return Text( - state.currentName, + displayName, style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, ), diff --git a/frontend/pweb/lib/pages/settings/profile/page.dart b/frontend/pweb/lib/pages/settings/profile/page.dart index f628ee9..3329d69 100644 --- a/frontend/pweb/lib/pages/settings/profile/page.dart +++ b/frontend/pweb/lib/pages/settings/profile/page.dart @@ -23,8 +23,11 @@ class ProfileSettingsPage extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final theme = Theme.of(context); - final accountName = context.select( - (provider) => provider.account?.describable.name, + final accountFirstName = context.select( + (provider) => provider.account?.name, + ); + final accountLastName = context.select( + (provider) => provider.account?.lastName, ); final accountAvatarUrl = context.select( (provider) => provider.account?.avatarUrl, @@ -49,9 +52,11 @@ class ProfileSettingsPage extends StatelessWidget { errorText: loc.avatarUpdateError, ), AccountName( - name: accountName ?? loc.userNamePlaceholder, + firstName: accountFirstName ?? '', + lastName: accountLastName ?? '', title: loc.accountName, hintText: loc.accountNameHint, + lastNameHint: loc.lastName, errorText: loc.accountNameUpdateError, ), AccountPassword( diff --git a/frontend/pweb/lib/providers/account_name.dart b/frontend/pweb/lib/providers/account_name.dart index 4084f6c..22b7d10 100644 --- a/frontend/pweb/lib/providers/account_name.dart +++ b/frontend/pweb/lib/providers/account_name.dart @@ -7,50 +7,70 @@ import 'package:pweb/models/edit_state.dart'; class AccountNameState extends ChangeNotifier { AccountNameState({ - required this.initialName, + required this.initialFirstName, + required this.initialLastName, required this.errorMessage, required AccountProvider accountProvider, }) : _accountProvider = accountProvider { - _controller = TextEditingController(text: initialName); + _firstNameController = TextEditingController(text: initialFirstName); + _lastNameController = TextEditingController(text: initialLastName); } final AccountProvider _accountProvider; - final String initialName; + final String initialFirstName; + final String initialLastName; final String errorMessage; - late final TextEditingController _controller; + late final TextEditingController _firstNameController; + late final TextEditingController _lastNameController; EditState _editState = EditState.view; String _errorText = ''; bool _disposed = false; - TextEditingController get controller => _controller; + TextEditingController get firstNameController => _firstNameController; + TextEditingController get lastNameController => _lastNameController; EditState get editState => _editState; String get errorText => _errorText; bool get isEditing => _editState != EditState.view; bool get isSaving => _editState == EditState.saving; bool get isBusy => _accountProvider.isLoading || isSaving; - String get currentName => _accountProvider.account?.name ?? initialName; + String get currentFirstName => _accountProvider.account?.name ?? initialFirstName; + String get currentLastName => _accountProvider.account?.lastName ?? initialLastName; + String get currentFullName { + final first = currentFirstName.trim(); + final last = currentLastName.trim(); + if (first.isEmpty && last.isEmpty) return ''; + if (first.isEmpty) return last; + if (last.isEmpty) return first; + return '$first $last'; + } void startEditing() => _setState(EditState.edit); void cancelEditing() { - _controller.text = currentName; + _firstNameController.text = currentFirstName; + _lastNameController.text = currentLastName; _setError(''); _setState(EditState.view); } - void syncName(String latestName) { + void syncNames(String latestFirstName, String latestLastName) { if (isEditing) return; - if (_controller.text != latestName) { - _controller.text = latestName; + if (_firstNameController.text != latestFirstName) { + _firstNameController.text = latestFirstName; + } + if (_lastNameController.text != latestLastName) { + _lastNameController.text = latestLastName; } } Future save() async { - final newName = _controller.text.trim(); - final current = currentName; + final newFirstName = _firstNameController.text.trim(); + final newLastName = _lastNameController.text.trim(); + final currentFirst = currentFirstName; + final currentLast = currentLastName; - if (newName.isEmpty || newName == current) { + if (newFirstName.isEmpty || (newFirstName == currentFirst && newLastName == currentLast)) { cancelEditing(); return false; } @@ -59,7 +79,7 @@ class AccountNameState extends ChangeNotifier { _setState(EditState.saving); try { - await _accountProvider.resetUsername(newName); + await _accountProvider.resetUsername(newFirstName, lastName: newLastName); _setState(EditState.view); return true; } catch (_) { @@ -88,7 +108,8 @@ class AccountNameState extends ChangeNotifier { @override void dispose() { _disposed = true; - _controller.dispose(); + _firstNameController.dispose(); + _lastNameController.dispose(); super.dispose(); } } diff --git a/frontend/pweb/lib/widgets/drawer/avatar.dart b/frontend/pweb/lib/widgets/drawer/avatar.dart index 7cb65ef..1064826 100644 --- a/frontend/pweb/lib/widgets/drawer/avatar.dart +++ b/frontend/pweb/lib/widgets/drawer/avatar.dart @@ -50,7 +50,7 @@ class AccountAvatar extends StatelessWidget { final loc = AppLocalizations.of(context)!; return UserAccountsDrawerHeader( - accountName: Text(provider.account?.describable.name ?? loc.userNamePlaceholder), + accountName: Text(provider.account?.fullName ?? loc.userNamePlaceholder), accountEmail: Text(provider.account?.login ?? loc.usernameHint), currentAccountPicture: avatar, ); diff --git a/frontend/pweb/lib/widgets/sidebar/sidebar.dart b/frontend/pweb/lib/widgets/sidebar/sidebar.dart index 60cdefd..8bcb435 100644 --- a/frontend/pweb/lib/widgets/sidebar/sidebar.dart +++ b/frontend/pweb/lib/widgets/sidebar/sidebar.dart @@ -32,7 +32,7 @@ class PayoutSidebar extends StatelessWidget { @override Widget build(BuildContext context) { final accountName = context.select( - (provider) => provider.account?.describable.name, + (provider) => provider.account?.fullName, ); final accountAvatar = context.select( (provider) => provider.account?.avatarUrl,