feat: Add optional agentTypeId field to RegisterRequest model.

This commit is contained in:
pradeepkumar
2026-03-16 14:23:31 +05:30
parent c4c6322507
commit 9512f5ef8d
8 changed files with 264 additions and 112 deletions

View File

@@ -624,42 +624,47 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
),
],
const SizedBox(height: 8),
// Location + Member Since (same line, matching web ProfileCard)
// Location (wrapping) + Member Since (separate line)
Builder(builder: (_) {
final parts = state.locationParts;
const maxVisible = 1;
final firstPart = parts.isNotEmpty ? parts.first : '';
const maxVisible = 4;
final visibleParts = parts.take(maxVisible).join(', ');
final remaining = parts.length - maxVisible;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
return Column(
children: [
if (parts.isNotEmpty) ...[
SvgPicture.asset(
GestureDetector(
onTap: remaining > 0 ? () => _showLocationModal(parts) : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2),
child: SvgPicture.asset(
'assets/icons/location_pin_icon.svg',
width: 18,
height: 18,
),
),
const SizedBox(width: 4),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 120),
child: Text(
firstPart,
Flexible(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: visibleParts,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
if (remaining > 0)
GestureDetector(
onTap: () => _showLocationModal(parts),
child: Text(
' +$remaining more',
TextSpan(
text: ' +$remaining more',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
@@ -667,17 +672,26 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
color: AppColors.accentOrange,
),
),
),
const SizedBox(width: 12),
],
),
textAlign: TextAlign.center,
),
),
],
),
),
const SizedBox(height: 6),
],
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
'assets/icons/calendar_icon.svg',
width: 18,
height: 18,
),
const SizedBox(width: 4),
Flexible(
child: Text(
Text(
agent.memberSinceText,
style: const TextStyle(
fontFamily: 'SourceSerif4',
@@ -685,9 +699,8 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
],
);

View File

@@ -11,6 +11,7 @@ abstract class RegisterRequest with _$RegisterRequest {
required String role,
String? firstName,
String? lastName,
String? agentTypeId,
}) = _RegisterRequest;
factory RegisterRequest.fromJson(Map<String, dynamic> json) =>

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RegisterRequest {
String get email; String get password; String get role; String? get firstName; String? get lastName;
String get email; String get password; String get role; String? get firstName; String? get lastName; String? get agentTypeId;
/// Create a copy of RegisterRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $RegisterRequestCopyWith<RegisterRequest> get copyWith => _$RegisterRequestCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RegisterRequest&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.role, role) || other.role == role)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName));
return identical(this, other) || (other.runtimeType == runtimeType&&other is RegisterRequest&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.role, role) || other.role == role)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.agentTypeId, agentTypeId) || other.agentTypeId == agentTypeId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,email,password,role,firstName,lastName);
int get hashCode => Object.hash(runtimeType,email,password,role,firstName,lastName,agentTypeId);
@override
String toString() {
return 'RegisterRequest(email: $email, password: $password, role: $role, firstName: $firstName, lastName: $lastName)';
return 'RegisterRequest(email: $email, password: $password, role: $role, firstName: $firstName, lastName: $lastName, agentTypeId: $agentTypeId)';
}
@@ -48,7 +48,7 @@ abstract mixin class $RegisterRequestCopyWith<$Res> {
factory $RegisterRequestCopyWith(RegisterRequest value, $Res Function(RegisterRequest) _then) = _$RegisterRequestCopyWithImpl;
@useResult
$Res call({
String email, String password, String role, String? firstName, String? lastName
String email, String password, String role, String? firstName, String? lastName, String? agentTypeId
});
@@ -65,13 +65,14 @@ class _$RegisterRequestCopyWithImpl<$Res>
/// Create a copy of RegisterRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? email = null,Object? password = null,Object? role = null,Object? firstName = freezed,Object? lastName = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? email = null,Object? password = null,Object? role = null,Object? firstName = freezed,Object? lastName = freezed,Object? agentTypeId = freezed,}) {
return _then(_self.copyWith(
email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable
as String,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable
as String,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as String,firstName: freezed == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String?,lastName: freezed == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String?,agentTypeId: freezed == agentTypeId ? _self.agentTypeId : agentTypeId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@@ -157,10 +158,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String email, String password, String role, String? firstName, String? lastName)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String email, String password, String role, String? firstName, String? lastName, String? agentTypeId)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _RegisterRequest() when $default != null:
return $default(_that.email,_that.password,_that.role,_that.firstName,_that.lastName);case _:
return $default(_that.email,_that.password,_that.role,_that.firstName,_that.lastName,_that.agentTypeId);case _:
return orElse();
}
@@ -178,10 +179,10 @@ return $default(_that.email,_that.password,_that.role,_that.firstName,_that.last
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String email, String password, String role, String? firstName, String? lastName) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String email, String password, String role, String? firstName, String? lastName, String? agentTypeId) $default,) {final _that = this;
switch (_that) {
case _RegisterRequest():
return $default(_that.email,_that.password,_that.role,_that.firstName,_that.lastName);case _:
return $default(_that.email,_that.password,_that.role,_that.firstName,_that.lastName,_that.agentTypeId);case _:
throw StateError('Unexpected subclass');
}
@@ -198,10 +199,10 @@ return $default(_that.email,_that.password,_that.role,_that.firstName,_that.last
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String email, String password, String role, String? firstName, String? lastName)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String email, String password, String role, String? firstName, String? lastName, String? agentTypeId)? $default,) {final _that = this;
switch (_that) {
case _RegisterRequest() when $default != null:
return $default(_that.email,_that.password,_that.role,_that.firstName,_that.lastName);case _:
return $default(_that.email,_that.password,_that.role,_that.firstName,_that.lastName,_that.agentTypeId);case _:
return null;
}
@@ -213,7 +214,7 @@ return $default(_that.email,_that.password,_that.role,_that.firstName,_that.last
@JsonSerializable()
class _RegisterRequest implements RegisterRequest {
const _RegisterRequest({required this.email, required this.password, required this.role, this.firstName, this.lastName});
const _RegisterRequest({required this.email, required this.password, required this.role, this.firstName, this.lastName, this.agentTypeId});
factory _RegisterRequest.fromJson(Map<String, dynamic> json) => _$RegisterRequestFromJson(json);
@override final String email;
@@ -221,6 +222,7 @@ class _RegisterRequest implements RegisterRequest {
@override final String role;
@override final String? firstName;
@override final String? lastName;
@override final String? agentTypeId;
/// Create a copy of RegisterRequest
/// with the given fields replaced by the non-null parameter values.
@@ -235,16 +237,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RegisterRequest&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.role, role) || other.role == role)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RegisterRequest&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.role, role) || other.role == role)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.agentTypeId, agentTypeId) || other.agentTypeId == agentTypeId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,email,password,role,firstName,lastName);
int get hashCode => Object.hash(runtimeType,email,password,role,firstName,lastName,agentTypeId);
@override
String toString() {
return 'RegisterRequest(email: $email, password: $password, role: $role, firstName: $firstName, lastName: $lastName)';
return 'RegisterRequest(email: $email, password: $password, role: $role, firstName: $firstName, lastName: $lastName, agentTypeId: $agentTypeId)';
}
@@ -255,7 +257,7 @@ abstract mixin class _$RegisterRequestCopyWith<$Res> implements $RegisterRequest
factory _$RegisterRequestCopyWith(_RegisterRequest value, $Res Function(_RegisterRequest) _then) = __$RegisterRequestCopyWithImpl;
@override @useResult
$Res call({
String email, String password, String role, String? firstName, String? lastName
String email, String password, String role, String? firstName, String? lastName, String? agentTypeId
});
@@ -272,13 +274,14 @@ class __$RegisterRequestCopyWithImpl<$Res>
/// Create a copy of RegisterRequest
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? email = null,Object? password = null,Object? role = null,Object? firstName = freezed,Object? lastName = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? email = null,Object? password = null,Object? role = null,Object? firstName = freezed,Object? lastName = freezed,Object? agentTypeId = freezed,}) {
return _then(_RegisterRequest(
email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable
as String,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable
as String,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as String,firstName: freezed == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String?,lastName: freezed == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String?,agentTypeId: freezed == agentTypeId ? _self.agentTypeId : agentTypeId // ignore: cast_nullable_to_non_nullable
as String?,
));
}

View File

@@ -13,6 +13,7 @@ _RegisterRequest _$RegisterRequestFromJson(Map<String, dynamic> json) =>
role: json['role'] as String,
firstName: json['firstName'] as String?,
lastName: json['lastName'] as String?,
agentTypeId: json['agentTypeId'] as String?,
);
Map<String, dynamic> _$RegisterRequestToJson(_RegisterRequest instance) =>
@@ -22,4 +23,5 @@ Map<String, dynamic> _$RegisterRequestToJson(_RegisterRequest instance) =>
'role': instance.role,
'firstName': instance.firstName,
'lastName': instance.lastName,
'agentTypeId': instance.agentTypeId,
};

View File

@@ -168,6 +168,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
required String email,
required String password,
required String role,
String? agentTypeId,
}) async {
state = state.copyWith(
status: AuthStatus.loading,
@@ -188,6 +189,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
role: role,
firstName: firstName,
lastName: lastName,
agentTypeId: role == 'AGENT' ? agentTypeId : null,
),
);

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
import 'package:real_estate_mobile/features/agents/presentation/providers/agents_provider.dart';
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
import 'package:real_estate_mobile/features/auth/presentation/widgets/auth_text_field.dart';
import 'package:real_estate_mobile/features/auth/presentation/widgets/or_divider.dart';
@@ -22,6 +24,16 @@ class _SignupScreenState extends ConsumerState<SignupScreen> {
String _selectedRole = 'USER';
bool _obscurePassword = true;
List<AgentType> _agentTypes = [];
String? _selectedAgentTypeId;
bool _loadingAgentTypes = false;
@override
void initState() {
super.initState();
_fetchAgentTypes();
}
@override
void dispose() {
_nameController.dispose();
@@ -30,6 +42,22 @@ class _SignupScreenState extends ConsumerState<SignupScreen> {
super.dispose();
}
Future<void> _fetchAgentTypes() async {
setState(() => _loadingAgentTypes = true);
try {
final repo = ref.read(agentsRepositoryProvider);
final types = await repo.getAgentTypes();
if (mounted) {
setState(() {
_agentTypes = types;
_loadingAgentTypes = false;
});
}
} catch (_) {
if (mounted) setState(() => _loadingAgentTypes = false);
}
}
String? _validateLocally() {
if (_nameController.text.trim().isEmpty) {
return 'Please enter your name';
@@ -40,6 +68,9 @@ class _SignupScreenState extends ConsumerState<SignupScreen> {
if (_passwordController.text.isEmpty) {
return 'Please enter a password';
}
if (_selectedRole == 'AGENT' && (_selectedAgentTypeId == null || _selectedAgentTypeId!.isEmpty)) {
return 'Please select an agent type';
}
return null;
}
@@ -60,6 +91,7 @@ class _SignupScreenState extends ConsumerState<SignupScreen> {
email: _emailController.text.trim(),
password: _passwordController.text,
role: _selectedRole,
agentTypeId: _selectedRole == 'AGENT' ? _selectedAgentTypeId : null,
);
}
@@ -206,19 +238,10 @@ class _SignupScreenState extends ConsumerState<SignupScreen> {
// Role toggle
RoleToggle(
selectedRole: _selectedRole,
onChanged: (role) => setState(() => _selectedRole = role),
),
const SizedBox(height: 20),
// Subtitle
Text(
'Enter Your Name to Continue',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w300,
color: AppColors.primaryDark.withValues(alpha: 0.7),
),
onChanged: (role) => setState(() {
_selectedRole = role;
if (role != 'AGENT') _selectedAgentTypeId = null;
}),
),
const SizedBox(height: 20),
@@ -296,6 +319,76 @@ class _SignupScreenState extends ConsumerState<SignupScreen> {
),
),
),
// Agent type selector (only when AGENT role is selected)
if (_selectedRole == 'AGENT') ...[
const SizedBox(height: 16),
Container(
height: 50,
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _loadingAgentTypes
? const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.accentOrange,
),
),
)
: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedAgentTypeId,
hint: Text(
'Select Agent Type',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w300,
color: AppColors.primaryDark.withValues(alpha: 0.5),
),
),
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: AppColors.primaryDark,
size: 22,
),
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
items: _agentTypes
.map((type) => DropdownMenuItem<String>(
value: type.id,
child: Text(type.name),
))
.toList(),
onChanged: (value) =>
setState(() => _selectedAgentTypeId = value),
),
),
),
if (authState.getFieldError('agentTypeId') != null)
Padding(
padding: const EdgeInsets.only(top: 6, left: 12),
child: Text(
authState.getFieldError('agentTypeId')!,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 12,
color: AppColors.error,
),
),
),
],
const SizedBox(height: 24),
// Sign Up button

View File

@@ -50,9 +50,19 @@ class _HeroSectionState extends ConsumerState<HeroSection> {
if (_selectedLocation != null) params['location'] = Uri.encodeComponent(_selectedLocation!);
if (_selectedCategory != null) params['category'] = Uri.encodeComponent(_selectedCategory!);
if (params.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select at least one filter to search'),
backgroundColor: AppColors.accentOrange,
),
);
return;
}
final queryString =
params.entries.map((e) => '${e.key}=${e.value}').join('&');
context.push('/agents/search${queryString.isNotEmpty ? '?$queryString' : ''}');
context.push('/agents/search?$queryString');
}
Future<void> _showTypePicker() async {

View File

@@ -341,20 +341,30 @@ class _MenuDrawer extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
children: [
SizedBox(
height: 20,
width: 20,
child: Center(
child: Icon(
Icons.logout_rounded,
color: AppColors.primaryDark,
size: 20,
size: 18,
),
SizedBox(width: 8),
Text(
),
),
const SizedBox(width: 8),
const Padding(
padding: EdgeInsets.only(top: 1),
child: Text(
'Log Out',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
height: 1.0,
),
),
),
],
@@ -366,25 +376,11 @@ class _MenuDrawer extends ConsumerWidget {
SizedBox(
width: double.infinity,
height: 54,
child: ElevatedButton.icon(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
context.push('/login');
},
icon: const Icon(
Icons.login,
color: Colors.white,
size: 20,
),
label: const Text(
'Log In',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accentOrange,
elevation: 0,
@@ -392,6 +388,38 @@ class _MenuDrawer extends ConsumerWidget {
borderRadius: BorderRadius.circular(15),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 20,
width: 20,
child: Center(
child: Icon(
Icons.login,
color: Colors.white,
size: 18,
),
),
),
const SizedBox(width: 8),
const Padding(
padding: EdgeInsets.only(top: 1),
child: Text(
'Log In',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.white,
height: 1.0,
),
),
),
],
),
),
),
const SizedBox(height: 12),