add auth register

This commit is contained in:
Phuoc Nguyen
2025-11-07 15:55:02 +07:00
parent ce7396f729
commit 9e55983d82
16 changed files with 2376 additions and 110 deletions

58
docs/auth.sh Normal file
View File

@@ -0,0 +1,58 @@
GET SESSION
curl --location --request POST 'https://land.dbiz.com//api/method/dbiz_common.dbiz_common.api.auth.get_session' \
--data ''
DATA RETURN
{
"message": {
"data": {
"sid": "edb6059ecf147f268176cd4aff8ca034a75ebb8ff23464f9913c9537",
"csrf_token": "d0077178c349f69bc1456401d9a3d90ef0f7b9df3e08cfd26794a53f"
}
},
"home_page": "/app",
"full_name": "PublicAPI"
}
GET CITY
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
--header 'X-Frappe-Csrf-Token: 3d072ea39d245c2340ecc42d0825f6cbb7e674943c9c74346c8e4629' \
--header 'Content-Type: application/json' \
--header 'Cookie: full_name=PublicAPI; sid=b5a564f45d2fbe8b47dab89382801bf9a6e5c618500feb84a738f205; system_user=no; user_id=public_api%40dbiz.com; user_image=' \
--data '{
"doctype": "City",
"fields": ["city_name","name","code"],
"limit_page_length": 0
}'
GET ROLE
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
--header 'X-Frappe-Csrf-Token: 3d072ea39d245c2340ecc42d0825f6cbb7e674943c9c74346c8e4629' \
--header 'Content-Type: application/json' \
--header 'Cookie: full_name=PublicAPI; sid=b5a564f45d2fbe8b47dab89382801bf9a6e5c618500feb84a738f205; system_user=no; user_id=public_api%40dbiz.com; user_image=' \
--data '{
"doctype": "Customer Group",
"fields": ["customer_group_name","name","value"],
"filters": {"is_group": 0, "is_active" : 1, "customer" : 1},
"limit_page_length": 0
}'
REGISTER
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.user.register' \
--header 'X-Frappe-Csrf-Token: 3d072ea39d245c2340ecc42d0825f6cbb7e674943c9c74346c8e4629' \
--header 'Cookie: sid=b5a564f45d2fbe8b47dab89382801bf9a6e5c618500feb84a738f205; full_name=PublicAPI; sid=b5a564f45d2fbe8b47dab89382801bf9a6e5c618500feb84a738f205; system_user=no; user_id=public_api%40dbiz.com; user_image=' \
--header 'Content-Type: application/json' \
--data-raw '{
"full_name" : "Nguyễn Lê Duy Tiến",
"phone" : "091321236",
"email" : "tiennld6@dbiz.com",
"customer_group_code" : "ACT",
"company_name" : null,
"city_code" : "01",
"tax_code" : "091231",
"id_card_front_base64" : "base64 tr",
"id_card_back_base64" : "base64 str",
"certificates_base64" : [
"bas64_1 str","base64_2 str"
]
}'

View File

@@ -0,0 +1,245 @@
/// Authentication Remote Data Source
///
/// Handles all authentication-related API calls.
library;
import 'package:dio/dio.dart';
import 'package:worker/core/errors/exceptions.dart';
import 'package:worker/features/auth/data/models/auth_session_model.dart';
import 'package:worker/features/auth/domain/entities/city.dart';
import 'package:worker/features/auth/domain/entities/customer_group.dart';
/// Authentication Remote Data Source
///
/// Provides methods for:
/// - Getting session (CSRF token and SID)
/// - Fetching cities for registration
/// - Fetching customer groups (roles) for registration
/// - User registration
class AuthRemoteDataSource {
final Dio _dio;
AuthRemoteDataSource(this._dio);
/// Get Session
///
/// Fetches session data including SID and CSRF token.
/// This should be called before making authenticated requests.
///
/// API: POST /api/method/dbiz_common.dbiz_common.api.auth.get_session
Future<GetSessionResponse> getSession() async {
try {
final response = await _dio.post<Map<String, dynamic>>(
'/api/method/dbiz_common.dbiz_common.api.auth.get_session',
data: '',
);
if (response.statusCode == 200 && response.data != null) {
return GetSessionResponse.fromJson(response.data!);
} else {
throw ServerException(
'Failed to get session: ${response.statusCode}',
);
}
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw const UnauthorizedException();
} else if (e.response?.statusCode == 404) {
throw NotFoundException('Session endpoint not found');
} else {
throw NetworkException(
e.message ?? 'Failed to get session',
);
}
} catch (e) {
throw ServerException('Unexpected error: $e');
}
}
/// Get Cities
///
/// Fetches list of cities/provinces for address selection.
/// Requires authenticated session (CSRF token and Cookie).
///
/// API: POST /api/method/frappe.client.get_list
/// DocType: City
Future<List<City>> getCities({
required String csrfToken,
required String sid,
}) async {
try {
final response = await _dio.post<Map<String, dynamic>>(
'/api/method/frappe.client.get_list',
data: {
'doctype': 'City',
'fields': ['city_name', 'name', 'code'],
'limit_page_length': 0,
},
options: Options(
headers: {
'X-Frappe-Csrf-Token': csrfToken,
'Cookie': 'sid=$sid',
},
),
);
if (response.statusCode == 200 && response.data != null) {
final message = response.data!['message'];
if (message is List) {
return message
.map((json) => City.fromJson(json as Map<String, dynamic>))
.toList();
} else {
throw ServerException('Invalid response format for cities');
}
} else {
throw ServerException(
'Failed to get cities: ${response.statusCode}',
);
}
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw const UnauthorizedException();
} else if (e.response?.statusCode == 404) {
throw NotFoundException('Cities endpoint not found');
} else {
throw NetworkException(
e.message ?? 'Failed to get cities',
);
}
} catch (e) {
throw ServerException('Unexpected error: $e');
}
}
/// Get Customer Groups (Roles)
///
/// Fetches list of customer groups for user role selection.
/// Requires authenticated session (CSRF token and Cookie).
///
/// API: POST /api/method/frappe.client.get_list
/// DocType: Customer Group
Future<List<CustomerGroup>> getCustomerGroups({
required String csrfToken,
required String sid,
}) async {
try {
final response = await _dio.post<Map<String, dynamic>>(
'/api/method/frappe.client.get_list',
data: {
'doctype': 'Customer Group',
'fields': ['customer_group_name', 'name', 'value'],
'filters': {
'is_group': 0,
'is_active': 1,
'customer': 1,
},
'limit_page_length': 0,
},
options: Options(
headers: {
'X-Frappe-Csrf-Token': csrfToken,
'Cookie': 'sid=$sid',
},
),
);
if (response.statusCode == 200 && response.data != null) {
final message = response.data!['message'];
if (message is List) {
return message
.map((json) =>
CustomerGroup.fromJson(json as Map<String, dynamic>))
.toList();
} else {
throw ServerException('Invalid response format for customer groups');
}
} else {
throw ServerException(
'Failed to get customer groups: ${response.statusCode}',
);
}
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw const UnauthorizedException();
} else if (e.response?.statusCode == 404) {
throw NotFoundException('Customer groups endpoint not found');
} else {
throw NetworkException(
e.message ?? 'Failed to get customer groups',
);
}
} catch (e) {
throw ServerException('Unexpected error: $e');
}
}
/// Register User
///
/// Registers a new user with the provided information.
/// Requires authenticated session (CSRF token and Cookie).
///
/// API: POST /api/method/building_material.building_material.api.user.register
Future<Map<String, dynamic>> register({
required String csrfToken,
required String sid,
required String fullName,
required String phone,
required String email,
required String customerGroupCode,
required String cityCode,
String? companyName,
String? taxCode,
String? idCardFrontBase64,
String? idCardBackBase64,
List<String>? certificatesBase64,
}) async {
try {
final response = await _dio.post<Map<String, dynamic>>(
'/api/method/building_material.building_material.api.user.register',
data: {
'full_name': fullName,
'phone': phone,
'email': email,
'customer_group_code': customerGroupCode,
'city_code': cityCode,
'company_name': companyName,
'tax_code': taxCode,
'id_card_front_base64': idCardFrontBase64,
'id_card_back_base64': idCardBackBase64,
'certificates_base64': certificatesBase64 ?? [],
},
options: Options(
headers: {
'X-Frappe-Csrf-Token': csrfToken,
'Cookie': 'sid=$sid',
},
),
);
if (response.statusCode == 200 && response.data != null) {
return response.data!;
} else {
throw ServerException(
'Failed to register: ${response.statusCode}',
);
}
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw const UnauthorizedException();
} else if (e.response?.statusCode == 400) {
throw ValidationException(
e.response?.data?['message'] as String? ?? 'Validation error',
);
} else if (e.response?.statusCode == 404) {
throw NotFoundException('Register endpoint not found');
} else {
throw NetworkException(
e.message ?? 'Failed to register',
);
}
} catch (e) {
throw ServerException('Unexpected error: $e');
}
}
}

View File

@@ -57,6 +57,48 @@ sealed class AuthSessionResponse with _$AuthSessionResponse {
_$AuthSessionResponseFromJson(json);
}
/// Session Data (for GET SESSION API)
///
/// Represents the session data structure from get_session API.
@freezed
sealed class GetSessionData with _$GetSessionData {
const factory GetSessionData({
required String sid,
@JsonKey(name: 'csrf_token') required String csrfToken,
}) = _GetSessionData;
factory GetSessionData.fromJson(Map<String, dynamic> json) =>
_$GetSessionDataFromJson(json);
}
/// Get Session Message
///
/// Wrapper for session data in get_session API response.
@freezed
sealed class GetSessionMessage with _$GetSessionMessage {
const factory GetSessionMessage({
required GetSessionData data,
}) = _GetSessionMessage;
factory GetSessionMessage.fromJson(Map<String, dynamic> json) =>
_$GetSessionMessageFromJson(json);
}
/// Get Session Response
///
/// Complete response from get_session API.
@freezed
sealed class GetSessionResponse with _$GetSessionResponse {
const factory GetSessionResponse({
required GetSessionMessage message,
@JsonKey(name: 'home_page') required String homePage,
@JsonKey(name: 'full_name') required String fullName,
}) = _GetSessionResponse;
factory GetSessionResponse.fromJson(Map<String, dynamic> json) =>
_$GetSessionResponseFromJson(json);
}
/// Session Storage Model
///
/// Simplified model for storing session data in Hive.
@@ -73,7 +115,17 @@ sealed class SessionData with _$SessionData {
factory SessionData.fromJson(Map<String, dynamic> json) =>
_$SessionDataFromJson(json);
/// Create from API response
/// Create from get_session API response
factory SessionData.fromGetSessionResponse(GetSessionResponse response) {
return SessionData(
sid: response.message.data.sid,
csrfToken: response.message.data.csrfToken,
fullName: response.fullName,
createdAt: DateTime.now(),
);
}
/// Create from auth login API response
factory SessionData.fromAuthResponse(AuthSessionResponse response) {
return SessionData(
sid: response.message.sid,

View File

@@ -834,6 +834,822 @@ $LoginMessageCopyWith<$Res> get message {
}
/// @nodoc
mixin _$GetSessionData {
String get sid;@JsonKey(name: 'csrf_token') String get csrfToken;
/// Create a copy of GetSessionData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$GetSessionDataCopyWith<GetSessionData> get copyWith => _$GetSessionDataCopyWithImpl<GetSessionData>(this as GetSessionData, _$identity);
/// Serializes this GetSessionData to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is GetSessionData&&(identical(other.sid, sid) || other.sid == sid)&&(identical(other.csrfToken, csrfToken) || other.csrfToken == csrfToken));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,sid,csrfToken);
@override
String toString() {
return 'GetSessionData(sid: $sid, csrfToken: $csrfToken)';
}
}
/// @nodoc
abstract mixin class $GetSessionDataCopyWith<$Res> {
factory $GetSessionDataCopyWith(GetSessionData value, $Res Function(GetSessionData) _then) = _$GetSessionDataCopyWithImpl;
@useResult
$Res call({
String sid,@JsonKey(name: 'csrf_token') String csrfToken
});
}
/// @nodoc
class _$GetSessionDataCopyWithImpl<$Res>
implements $GetSessionDataCopyWith<$Res> {
_$GetSessionDataCopyWithImpl(this._self, this._then);
final GetSessionData _self;
final $Res Function(GetSessionData) _then;
/// Create a copy of GetSessionData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? sid = null,Object? csrfToken = null,}) {
return _then(_self.copyWith(
sid: null == sid ? _self.sid : sid // ignore: cast_nullable_to_non_nullable
as String,csrfToken: null == csrfToken ? _self.csrfToken : csrfToken // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [GetSessionData].
extension GetSessionDataPatterns on GetSessionData {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _GetSessionData value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _GetSessionData() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _GetSessionData value) $default,){
final _that = this;
switch (_that) {
case _GetSessionData():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _GetSessionData value)? $default,){
final _that = this;
switch (_that) {
case _GetSessionData() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String sid, @JsonKey(name: 'csrf_token') String csrfToken)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _GetSessionData() when $default != null:
return $default(_that.sid,_that.csrfToken);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String sid, @JsonKey(name: 'csrf_token') String csrfToken) $default,) {final _that = this;
switch (_that) {
case _GetSessionData():
return $default(_that.sid,_that.csrfToken);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String sid, @JsonKey(name: 'csrf_token') String csrfToken)? $default,) {final _that = this;
switch (_that) {
case _GetSessionData() when $default != null:
return $default(_that.sid,_that.csrfToken);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _GetSessionData implements GetSessionData {
const _GetSessionData({required this.sid, @JsonKey(name: 'csrf_token') required this.csrfToken});
factory _GetSessionData.fromJson(Map<String, dynamic> json) => _$GetSessionDataFromJson(json);
@override final String sid;
@override@JsonKey(name: 'csrf_token') final String csrfToken;
/// Create a copy of GetSessionData
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$GetSessionDataCopyWith<_GetSessionData> get copyWith => __$GetSessionDataCopyWithImpl<_GetSessionData>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$GetSessionDataToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _GetSessionData&&(identical(other.sid, sid) || other.sid == sid)&&(identical(other.csrfToken, csrfToken) || other.csrfToken == csrfToken));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,sid,csrfToken);
@override
String toString() {
return 'GetSessionData(sid: $sid, csrfToken: $csrfToken)';
}
}
/// @nodoc
abstract mixin class _$GetSessionDataCopyWith<$Res> implements $GetSessionDataCopyWith<$Res> {
factory _$GetSessionDataCopyWith(_GetSessionData value, $Res Function(_GetSessionData) _then) = __$GetSessionDataCopyWithImpl;
@override @useResult
$Res call({
String sid,@JsonKey(name: 'csrf_token') String csrfToken
});
}
/// @nodoc
class __$GetSessionDataCopyWithImpl<$Res>
implements _$GetSessionDataCopyWith<$Res> {
__$GetSessionDataCopyWithImpl(this._self, this._then);
final _GetSessionData _self;
final $Res Function(_GetSessionData) _then;
/// Create a copy of GetSessionData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? sid = null,Object? csrfToken = null,}) {
return _then(_GetSessionData(
sid: null == sid ? _self.sid : sid // ignore: cast_nullable_to_non_nullable
as String,csrfToken: null == csrfToken ? _self.csrfToken : csrfToken // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
mixin _$GetSessionMessage {
GetSessionData get data;
/// Create a copy of GetSessionMessage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$GetSessionMessageCopyWith<GetSessionMessage> get copyWith => _$GetSessionMessageCopyWithImpl<GetSessionMessage>(this as GetSessionMessage, _$identity);
/// Serializes this GetSessionMessage to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is GetSessionMessage&&(identical(other.data, data) || other.data == data));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,data);
@override
String toString() {
return 'GetSessionMessage(data: $data)';
}
}
/// @nodoc
abstract mixin class $GetSessionMessageCopyWith<$Res> {
factory $GetSessionMessageCopyWith(GetSessionMessage value, $Res Function(GetSessionMessage) _then) = _$GetSessionMessageCopyWithImpl;
@useResult
$Res call({
GetSessionData data
});
$GetSessionDataCopyWith<$Res> get data;
}
/// @nodoc
class _$GetSessionMessageCopyWithImpl<$Res>
implements $GetSessionMessageCopyWith<$Res> {
_$GetSessionMessageCopyWithImpl(this._self, this._then);
final GetSessionMessage _self;
final $Res Function(GetSessionMessage) _then;
/// Create a copy of GetSessionMessage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? data = null,}) {
return _then(_self.copyWith(
data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as GetSessionData,
));
}
/// Create a copy of GetSessionMessage
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$GetSessionDataCopyWith<$Res> get data {
return $GetSessionDataCopyWith<$Res>(_self.data, (value) {
return _then(_self.copyWith(data: value));
});
}
}
/// Adds pattern-matching-related methods to [GetSessionMessage].
extension GetSessionMessagePatterns on GetSessionMessage {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _GetSessionMessage value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _GetSessionMessage() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _GetSessionMessage value) $default,){
final _that = this;
switch (_that) {
case _GetSessionMessage():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _GetSessionMessage value)? $default,){
final _that = this;
switch (_that) {
case _GetSessionMessage() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( GetSessionData data)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _GetSessionMessage() when $default != null:
return $default(_that.data);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( GetSessionData data) $default,) {final _that = this;
switch (_that) {
case _GetSessionMessage():
return $default(_that.data);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( GetSessionData data)? $default,) {final _that = this;
switch (_that) {
case _GetSessionMessage() when $default != null:
return $default(_that.data);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _GetSessionMessage implements GetSessionMessage {
const _GetSessionMessage({required this.data});
factory _GetSessionMessage.fromJson(Map<String, dynamic> json) => _$GetSessionMessageFromJson(json);
@override final GetSessionData data;
/// Create a copy of GetSessionMessage
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$GetSessionMessageCopyWith<_GetSessionMessage> get copyWith => __$GetSessionMessageCopyWithImpl<_GetSessionMessage>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$GetSessionMessageToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _GetSessionMessage&&(identical(other.data, data) || other.data == data));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,data);
@override
String toString() {
return 'GetSessionMessage(data: $data)';
}
}
/// @nodoc
abstract mixin class _$GetSessionMessageCopyWith<$Res> implements $GetSessionMessageCopyWith<$Res> {
factory _$GetSessionMessageCopyWith(_GetSessionMessage value, $Res Function(_GetSessionMessage) _then) = __$GetSessionMessageCopyWithImpl;
@override @useResult
$Res call({
GetSessionData data
});
@override $GetSessionDataCopyWith<$Res> get data;
}
/// @nodoc
class __$GetSessionMessageCopyWithImpl<$Res>
implements _$GetSessionMessageCopyWith<$Res> {
__$GetSessionMessageCopyWithImpl(this._self, this._then);
final _GetSessionMessage _self;
final $Res Function(_GetSessionMessage) _then;
/// Create a copy of GetSessionMessage
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? data = null,}) {
return _then(_GetSessionMessage(
data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as GetSessionData,
));
}
/// Create a copy of GetSessionMessage
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$GetSessionDataCopyWith<$Res> get data {
return $GetSessionDataCopyWith<$Res>(_self.data, (value) {
return _then(_self.copyWith(data: value));
});
}
}
/// @nodoc
mixin _$GetSessionResponse {
GetSessionMessage get message;@JsonKey(name: 'home_page') String get homePage;@JsonKey(name: 'full_name') String get fullName;
/// Create a copy of GetSessionResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$GetSessionResponseCopyWith<GetSessionResponse> get copyWith => _$GetSessionResponseCopyWithImpl<GetSessionResponse>(this as GetSessionResponse, _$identity);
/// Serializes this GetSessionResponse to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is GetSessionResponse&&(identical(other.message, message) || other.message == message)&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.fullName, fullName) || other.fullName == fullName));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,message,homePage,fullName);
@override
String toString() {
return 'GetSessionResponse(message: $message, homePage: $homePage, fullName: $fullName)';
}
}
/// @nodoc
abstract mixin class $GetSessionResponseCopyWith<$Res> {
factory $GetSessionResponseCopyWith(GetSessionResponse value, $Res Function(GetSessionResponse) _then) = _$GetSessionResponseCopyWithImpl;
@useResult
$Res call({
GetSessionMessage message,@JsonKey(name: 'home_page') String homePage,@JsonKey(name: 'full_name') String fullName
});
$GetSessionMessageCopyWith<$Res> get message;
}
/// @nodoc
class _$GetSessionResponseCopyWithImpl<$Res>
implements $GetSessionResponseCopyWith<$Res> {
_$GetSessionResponseCopyWithImpl(this._self, this._then);
final GetSessionResponse _self;
final $Res Function(GetSessionResponse) _then;
/// Create a copy of GetSessionResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? message = null,Object? homePage = null,Object? fullName = null,}) {
return _then(_self.copyWith(
message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as GetSessionMessage,homePage: null == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable
as String,fullName: null == fullName ? _self.fullName : fullName // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of GetSessionResponse
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$GetSessionMessageCopyWith<$Res> get message {
return $GetSessionMessageCopyWith<$Res>(_self.message, (value) {
return _then(_self.copyWith(message: value));
});
}
}
/// Adds pattern-matching-related methods to [GetSessionResponse].
extension GetSessionResponsePatterns on GetSessionResponse {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _GetSessionResponse value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _GetSessionResponse() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _GetSessionResponse value) $default,){
final _that = this;
switch (_that) {
case _GetSessionResponse():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _GetSessionResponse value)? $default,){
final _that = this;
switch (_that) {
case _GetSessionResponse() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( GetSessionMessage message, @JsonKey(name: 'home_page') String homePage, @JsonKey(name: 'full_name') String fullName)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _GetSessionResponse() when $default != null:
return $default(_that.message,_that.homePage,_that.fullName);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( GetSessionMessage message, @JsonKey(name: 'home_page') String homePage, @JsonKey(name: 'full_name') String fullName) $default,) {final _that = this;
switch (_that) {
case _GetSessionResponse():
return $default(_that.message,_that.homePage,_that.fullName);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( GetSessionMessage message, @JsonKey(name: 'home_page') String homePage, @JsonKey(name: 'full_name') String fullName)? $default,) {final _that = this;
switch (_that) {
case _GetSessionResponse() when $default != null:
return $default(_that.message,_that.homePage,_that.fullName);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _GetSessionResponse implements GetSessionResponse {
const _GetSessionResponse({required this.message, @JsonKey(name: 'home_page') required this.homePage, @JsonKey(name: 'full_name') required this.fullName});
factory _GetSessionResponse.fromJson(Map<String, dynamic> json) => _$GetSessionResponseFromJson(json);
@override final GetSessionMessage message;
@override@JsonKey(name: 'home_page') final String homePage;
@override@JsonKey(name: 'full_name') final String fullName;
/// Create a copy of GetSessionResponse
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$GetSessionResponseCopyWith<_GetSessionResponse> get copyWith => __$GetSessionResponseCopyWithImpl<_GetSessionResponse>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$GetSessionResponseToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _GetSessionResponse&&(identical(other.message, message) || other.message == message)&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.fullName, fullName) || other.fullName == fullName));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,message,homePage,fullName);
@override
String toString() {
return 'GetSessionResponse(message: $message, homePage: $homePage, fullName: $fullName)';
}
}
/// @nodoc
abstract mixin class _$GetSessionResponseCopyWith<$Res> implements $GetSessionResponseCopyWith<$Res> {
factory _$GetSessionResponseCopyWith(_GetSessionResponse value, $Res Function(_GetSessionResponse) _then) = __$GetSessionResponseCopyWithImpl;
@override @useResult
$Res call({
GetSessionMessage message,@JsonKey(name: 'home_page') String homePage,@JsonKey(name: 'full_name') String fullName
});
@override $GetSessionMessageCopyWith<$Res> get message;
}
/// @nodoc
class __$GetSessionResponseCopyWithImpl<$Res>
implements _$GetSessionResponseCopyWith<$Res> {
__$GetSessionResponseCopyWithImpl(this._self, this._then);
final _GetSessionResponse _self;
final $Res Function(_GetSessionResponse) _then;
/// Create a copy of GetSessionResponse
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? message = null,Object? homePage = null,Object? fullName = null,}) {
return _then(_GetSessionResponse(
message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as GetSessionMessage,homePage: null == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable
as String,fullName: null == fullName ? _self.fullName : fullName // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of GetSessionResponse
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$GetSessionMessageCopyWith<$Res> get message {
return $GetSessionMessageCopyWith<$Res>(_self.message, (value) {
return _then(_self.copyWith(message: value));
});
}
}
/// @nodoc
mixin _$SessionData {

View File

@@ -93,6 +93,57 @@ Map<String, dynamic> _$AuthSessionResponseToJson(
'full_name': instance.fullName,
};
_GetSessionData _$GetSessionDataFromJson(Map<String, dynamic> json) =>
$checkedCreate('_GetSessionData', json, ($checkedConvert) {
final val = _GetSessionData(
sid: $checkedConvert('sid', (v) => v as String),
csrfToken: $checkedConvert('csrf_token', (v) => v as String),
);
return val;
}, fieldKeyMap: const {'csrfToken': 'csrf_token'});
Map<String, dynamic> _$GetSessionDataToJson(_GetSessionData instance) =>
<String, dynamic>{'sid': instance.sid, 'csrf_token': instance.csrfToken};
_GetSessionMessage _$GetSessionMessageFromJson(Map<String, dynamic> json) =>
$checkedCreate('_GetSessionMessage', json, ($checkedConvert) {
final val = _GetSessionMessage(
data: $checkedConvert(
'data',
(v) => GetSessionData.fromJson(v as Map<String, dynamic>),
),
);
return val;
});
Map<String, dynamic> _$GetSessionMessageToJson(_GetSessionMessage instance) =>
<String, dynamic>{'data': instance.data.toJson()};
_GetSessionResponse _$GetSessionResponseFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'_GetSessionResponse',
json,
($checkedConvert) {
final val = _GetSessionResponse(
message: $checkedConvert(
'message',
(v) => GetSessionMessage.fromJson(v as Map<String, dynamic>),
),
homePage: $checkedConvert('home_page', (v) => v as String),
fullName: $checkedConvert('full_name', (v) => v as String),
);
return val;
},
fieldKeyMap: const {'homePage': 'home_page', 'fullName': 'full_name'},
);
Map<String, dynamic> _$GetSessionResponseToJson(_GetSessionResponse instance) =>
<String, dynamic>{
'message': instance.message.toJson(),
'home_page': instance.homePage,
'full_name': instance.fullName,
};
_SessionData _$SessionDataFromJson(Map<String, dynamic> json) => $checkedCreate(
'_SessionData',
json,

View File

@@ -0,0 +1,60 @@
/// Domain Entity: City
///
/// Represents a city/province for address selection.
library;
/// City Entity
class City {
/// Unique city identifier
final String name;
/// City code (e.g., "01", "02")
final String code;
/// Display name
final String cityName;
const City({
required this.name,
required this.code,
required this.cityName,
});
/// Create from JSON map
factory City.fromJson(Map<String, dynamic> json) {
return City(
name: json['name'] as String,
code: json['code'] as String,
cityName: json['city_name'] as String,
);
}
/// Convert to JSON map
Map<String, dynamic> toJson() {
return {
'name': name,
'code': code,
'city_name': cityName,
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is City &&
other.name == name &&
other.code == code &&
other.cityName == cityName;
}
@override
int get hashCode {
return Object.hash(name, code, cityName);
}
@override
String toString() {
return 'City(name: $name, code: $code, cityName: $cityName)';
}
}

View File

@@ -0,0 +1,60 @@
/// Domain Entity: Customer Group (Role)
///
/// Represents a customer group/role for user registration.
library;
/// Customer Group Entity
class CustomerGroup {
/// Unique identifier
final String name;
/// Display name
final String customerGroupName;
/// Group value/code
final String? value;
const CustomerGroup({
required this.name,
required this.customerGroupName,
this.value,
});
/// Create from JSON map
factory CustomerGroup.fromJson(Map<String, dynamic> json) {
return CustomerGroup(
name: json['name'] as String,
customerGroupName: json['customer_group_name'] as String,
value: json['value'] as String?,
);
}
/// Convert to JSON map
Map<String, dynamic> toJson() {
return {
'name': name,
'customer_group_name': customerGroupName,
if (value != null) 'value': value,
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CustomerGroup &&
other.name == name &&
other.customerGroupName == customerGroupName &&
other.value == value;
}
@override
int get hashCode {
return Object.hash(name, customerGroupName, value);
}
@override
String toString() {
return 'CustomerGroup(name: $name, customerGroupName: $customerGroupName, value: $value)';
}
}

View File

@@ -4,6 +4,7 @@
/// Matches design from html/register.html
library;
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
@@ -12,12 +13,17 @@ import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/utils/validators.dart';
import 'package:worker/features/auth/domain/entities/business_unit.dart';
import 'package:worker/features/auth/domain/entities/city.dart';
import 'package:worker/features/auth/domain/entities/customer_group.dart';
import 'package:worker/features/auth/presentation/providers/cities_provider.dart';
import 'package:worker/features/auth/presentation/providers/customer_groups_provider.dart';
import 'package:worker/features/auth/presentation/providers/session_provider.dart';
import 'package:worker/features/auth/presentation/widgets/phone_input_field.dart';
import 'package:worker/features/auth/presentation/widgets/file_upload_card.dart';
import 'package:worker/features/auth/presentation/widgets/role_dropdown.dart';
/// Registration Page
///
@@ -65,16 +71,60 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
final _companyFocus = FocusNode();
// State
String? _selectedRole;
String? _selectedCity;
CustomerGroup? _selectedRole;
City? _selectedCity;
File? _idCardFile;
File? _certificateFile;
bool _termsAccepted = false;
bool _passwordVisible = false;
bool _isLoading = false;
bool _isLoadingData = true;
bool _hasInitialized = false;
final _imagePicker = ImagePicker();
/// Initialize session and load data
/// This should be called from build method or after widget is mounted
Future<void> _initializeData() async {
if (!mounted) return;
setState(() {
_isLoadingData = true;
_hasInitialized = true;
});
try {
// Step 1: Get session (public API user)
await ref.read(sessionProvider.notifier).getSession();
// Step 2: Fetch cities and customer groups in parallel using the session
await Future.wait([
ref.read(citiesProvider.notifier).fetchCities(),
ref.read(customerGroupsProvider.notifier).fetchCustomerGroups(),
]);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Lỗi tải dữ liệu: $e'),
backgroundColor: AppColors.danger,
action: SnackBarAction(
label: 'Thử lại',
textColor: AppColors.white,
onPressed: _initializeData,
),
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoadingData = false;
});
}
}
}
@override
void dispose() {
_fullNameController.dispose();
@@ -95,8 +145,23 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
}
/// Check if verification section should be shown
/// Note: This is based on the old role system
/// TODO: Update this logic based on actual customer group requirements
bool get _shouldShowVerification {
return _selectedRole == 'worker' || _selectedRole == 'dealer';
// For now, always hide verification section since we're using customer groups
return false;
}
/// Convert file to base64 string
Future<String?> _fileToBase64(File? file) async {
if (file == null) return null;
try {
final bytes = await file.readAsBytes();
return base64Encode(bytes);
} catch (e) {
debugPrint('Error converting file to base64: $e');
return null;
}
}
/// Pick image from gallery or camera
@@ -238,48 +303,60 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
});
try {
// TODO: Implement actual registration API call
// Include widget.selectedBusinessUnit?.id in the API request
// Example:
// final result = await authRepository.register(
// fullName: _fullNameController.text.trim(),
// phone: _phoneController.text.trim(),
// email: _emailController.text.trim(),
// password: _passwordController.text,
// role: _selectedRole,
// businessUnitId: widget.selectedBusinessUnit?.id,
// ...
// );
// Get session state for CSRF token and SID
final sessionState = ref.read(sessionProvider);
// For now, simulate API delay
await Future.delayed(const Duration(seconds: 2));
if (!sessionState.hasSession) {
throw Exception('Session không hợp lệ. Vui lòng thử lại.');
}
// Convert files to base64
final idCardFrontBase64 = await _fileToBase64(_idCardFile);
final certificatesBase64 = _certificateFile != null
? [await _fileToBase64(_certificateFile)]
: <String?>[];
// Remove null values from certificates list
final validCertificates = certificatesBase64
.whereType<String>()
.toList();
// Call registration API
final dataSource = ref.read(authRemoteDataSourceProvider);
final response = await dataSource.register(
csrfToken: sessionState.csrfToken!,
sid: sessionState.sid!,
fullName: _fullNameController.text.trim(),
phone: _phoneController.text.trim(),
email: _emailController.text.trim(),
customerGroupCode: _selectedRole?.value ?? _selectedRole?.name ?? '',
cityCode: _selectedCity?.code ?? '',
companyName: _companyController.text.trim().isEmpty
? null
: _companyController.text.trim(),
taxCode: _taxCodeController.text.trim().isEmpty
? null
: _taxCodeController.text.trim(),
idCardFrontBase64: idCardFrontBase64,
idCardBackBase64: null, // Not collecting back side in current UI
certificatesBase64: validCertificates,
);
if (mounted) {
// Navigate based on role
if (_shouldShowVerification) {
// For workers/dealers with verification, show pending page
// TODO: Navigate to pending approval page
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Đăng ký thành công! Tài khoản đang chờ xét duyệt.',
'Đăng ký thành công!',
// response['message']?.toString() ?? 'Đăng ký thành công!',
),
duration: Duration(seconds: 1),
backgroundColor: AppColors.success,
),
);
context.pop();
} else {
// For other roles, navigate to OTP verification
// TODO: Navigate to OTP verification page
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đăng ký thành công! Vui lòng xác thực OTP.'),
backgroundColor: AppColors.success,
),
);
// context.push('/otp-verification');
context.pop();
}
Future<void>.delayed(const Duration(seconds: 1)).then((_) => context.goLogin());
}
} catch (e) {
if (mounted) {
@@ -301,6 +378,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
@override
Widget build(BuildContext context) {
// Initialize data on first build
if (!_hasInitialized) {
// Use addPostFrameCallback to avoid calling setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeData();
});
}
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
@@ -320,7 +405,21 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
),
centerTitle: false,
),
body: SafeArea(
body: _isLoadingData
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: AppSpacing.md),
Text(
'Đang tải dữ liệu...',
style: TextStyle(color: AppColors.grey500),
),
],
),
)
: SafeArea(
child: Form(
key: _formKey,
child: SingleChildScrollView(
@@ -442,29 +541,9 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
),
const SizedBox(height: AppSpacing.md),
// Role Selection
// Role Selection (Customer Groups)
_buildLabel('Vai trò *'),
RoleDropdown(
value: _selectedRole,
onChanged: (value) {
setState(() {
_selectedRole = value;
// Clear verification fields when role changes
if (!_shouldShowVerification) {
_idNumberController.clear();
_taxCodeController.clear();
_idCardFile = null;
_certificateFile = null;
}
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Vui lòng chọn vai trò';
}
return null;
},
),
_buildCustomerGroupDropdown(),
const SizedBox(height: AppSpacing.md),
// Verification Section (conditional)
@@ -488,47 +567,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
// City/Province
_buildLabel('Tỉnh/Thành phố *'),
DropdownButtonFormField<String>(
value: _selectedCity,
decoration: _buildInputDecoration(
hintText: 'Chọn tỉnh/thành phố',
prefixIcon: Icons.location_city,
),
items: const [
DropdownMenuItem(
value: 'hanoi',
child: Text('Hà Nội'),
),
DropdownMenuItem(
value: 'hcm',
child: Text('TP. Hồ Chí Minh'),
),
DropdownMenuItem(
value: 'danang',
child: Text('Đà Nẵng'),
),
DropdownMenuItem(
value: 'haiphong',
child: Text('Hải Phòng'),
),
DropdownMenuItem(
value: 'cantho',
child: Text('Cần Thơ'),
),
DropdownMenuItem(value: 'other', child: Text('Khác')),
],
onChanged: (value) {
setState(() {
_selectedCity = value;
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Vui lòng chọn tỉnh/thành phố';
}
return null;
},
),
_buildCityDropdown(),
const SizedBox(height: AppSpacing.md),
// Terms and Conditions
@@ -712,6 +751,143 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
);
}
/// Build customer group dropdown
Widget _buildCustomerGroupDropdown() {
final customerGroupsAsync = ref.watch(customerGroupsProvider);
return customerGroupsAsync.when(
data: (groups) {
return DropdownButtonFormField<CustomerGroup>(
value: _selectedRole,
decoration: _buildInputDecoration(
hintText: 'Chọn vai trò',
prefixIcon: Icons.work,
),
items: groups
.map(
(group) => DropdownMenuItem(
value: group,
child: Text(group.customerGroupName),
),
)
.toList(),
onChanged: (value) {
setState(() {
_selectedRole = value;
// Clear verification fields when role changes
if (!_shouldShowVerification) {
_idNumberController.clear();
_taxCodeController.clear();
_idCardFile = null;
_certificateFile = null;
}
});
},
validator: (value) {
if (value == null) {
return 'Vui lòng chọn vai trò';
}
return null;
},
);
},
loading: () => const SizedBox(
height: 48,
child: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => Container(
height: 48,
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: AppColors.danger.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
border: Border.all(color: AppColors.danger),
),
child: Row(
children: [
const Icon(Icons.error_outline, color: AppColors.danger, size: 20),
const SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
'Lỗi tải vai trò',
style: const TextStyle(color: AppColors.danger, fontSize: 12),
),
),
TextButton(
onPressed: _initializeData,
child: const Text('Thử lại', style: TextStyle(fontSize: 12)),
),
],
),
),
);
}
/// Build city dropdown
Widget _buildCityDropdown() {
final citiesAsync = ref.watch(citiesProvider);
return citiesAsync.when(
data: (cities) {
return DropdownButtonFormField<City>(
value: _selectedCity,
decoration: _buildInputDecoration(
hintText: 'Chọn tỉnh/thành phố',
prefixIcon: Icons.location_city,
),
items: cities
.map(
(city) => DropdownMenuItem(
value: city,
child: Text(city.cityName),
),
)
.toList(),
onChanged: (value) {
setState(() {
_selectedCity = value;
});
},
validator: (value) {
if (value == null) {
return 'Vui lòng chọn tỉnh/thành phố';
}
return null;
},
);
},
loading: () => const SizedBox(
height: 48,
child: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => Container(
height: 48,
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: AppColors.danger.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
border: Border.all(color: AppColors.danger),
),
child: Row(
children: [
const Icon(Icons.error_outline, color: AppColors.danger, size: 20),
const SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
'Lỗi tải danh sách tỉnh/thành phố',
style: const TextStyle(color: AppColors.danger, fontSize: 12),
),
),
TextButton(
onPressed: _initializeData,
child: const Text('Thử lại', style: TextStyle(fontSize: 12)),
),
],
),
),
);
}
/// Build verification section
Widget _buildVerificationSection() {
return Container(

View File

@@ -0,0 +1,62 @@
/// Cities Provider
///
/// Manages the list of cities/provinces for address selection
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
import 'package:worker/features/auth/domain/entities/city.dart';
import 'package:worker/features/auth/presentation/providers/session_provider.dart';
part 'cities_provider.g.dart';
/// Cities Provider
///
/// Fetches list of cities from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
@Riverpod(keepAlive: true)
class Cities extends _$Cities {
@override
Future<List<City>> build() async {
// Don't auto-fetch on build, wait for manual call
return [];
}
/// Fetch cities from API
Future<void> fetchCities() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final sessionState = ref.read(sessionProvider);
if (!sessionState.hasSession) {
throw Exception('No active session. Please get session first.');
}
final dataSource = ref.read(authRemoteDataSourceProvider);
return await dataSource.getCities(
csrfToken: sessionState.csrfToken!,
sid: sessionState.sid!,
);
});
}
/// Refresh cities list
Future<void> refresh() async {
await fetchCities();
}
}
/// Provider to get a specific city by code
@riverpod
City? cityByCode(Ref ref, String code) {
final citiesAsync = ref.watch(citiesProvider);
return citiesAsync.whenOrNull(
data: (List<City> cities) => cities.firstWhere(
(City city) => city.code == code,
orElse: () => cities.first,
),
);
}

View File

@@ -0,0 +1,160 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cities_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Cities Provider
///
/// Fetches list of cities from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
@ProviderFor(Cities)
const citiesProvider = CitiesProvider._();
/// Cities Provider
///
/// Fetches list of cities from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
final class CitiesProvider extends $AsyncNotifierProvider<Cities, List<City>> {
/// Cities Provider
///
/// Fetches list of cities from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
const CitiesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'citiesProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$citiesHash();
@$internal
@override
Cities create() => Cities();
}
String _$citiesHash() => r'0de4a7d44e576d74ecd875ddad46d6cb52a38bf8';
/// Cities Provider
///
/// Fetches list of cities from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
abstract class _$Cities extends $AsyncNotifier<List<City>> {
FutureOr<List<City>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AsyncValue<List<City>>, List<City>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<City>>, List<City>>,
AsyncValue<List<City>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Provider to get a specific city by code
@ProviderFor(cityByCode)
const cityByCodeProvider = CityByCodeFamily._();
/// Provider to get a specific city by code
final class CityByCodeProvider extends $FunctionalProvider<City?, City?, City?>
with $Provider<City?> {
/// Provider to get a specific city by code
const CityByCodeProvider._({
required CityByCodeFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'cityByCodeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$cityByCodeHash();
@override
String toString() {
return r'cityByCodeProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<City?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
City? create(Ref ref) {
final argument = this.argument as String;
return cityByCode(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(City? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<City?>(value),
);
}
@override
bool operator ==(Object other) {
return other is CityByCodeProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$cityByCodeHash() => r'4e4a9a72526b0a366b8697244f2e7e2aedf7cabe';
/// Provider to get a specific city by code
final class CityByCodeFamily extends $Family
with $FunctionalFamilyOverride<City?, String> {
const CityByCodeFamily._()
: super(
retry: null,
name: r'cityByCodeProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider to get a specific city by code
CityByCodeProvider call(String code) =>
CityByCodeProvider._(argument: code, from: this);
@override
String toString() => r'cityByCodeProvider';
}

View File

@@ -0,0 +1,65 @@
/// Customer Groups Provider
///
/// Manages the list of customer groups (roles) for user registration
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
import 'package:worker/features/auth/domain/entities/customer_group.dart';
import 'package:worker/features/auth/presentation/providers/session_provider.dart';
part 'customer_groups_provider.g.dart';
/// Customer Groups Provider
///
/// Fetches list of customer groups (roles) from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
@Riverpod(keepAlive: true)
class CustomerGroups extends _$CustomerGroups {
@override
Future<List<CustomerGroup>> build() async {
// Don't auto-fetch on build, wait for manual call
return [];
}
/// Fetch customer groups from API
Future<void> fetchCustomerGroups() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final sessionState = ref.read(sessionProvider);
if (!sessionState.hasSession) {
throw Exception('No active session. Please get session first.');
}
final dataSource = ref.read(authRemoteDataSourceProvider);
return await dataSource.getCustomerGroups(
csrfToken: sessionState.csrfToken!,
sid: sessionState.sid!,
);
});
}
/// Refresh customer groups list
Future<void> refresh() async {
await fetchCustomerGroups();
}
}
/// Provider to get a specific customer group by code/value
@riverpod
CustomerGroup? customerGroupByCode(
Ref ref,
String code,
) {
final groupsAsync = ref.watch(customerGroupsProvider);
return groupsAsync.whenOrNull(
data: (List<CustomerGroup> groups) => groups.firstWhere(
(CustomerGroup group) => group.value == code || group.name == code,
orElse: () => groups.first,
),
);
}

View File

@@ -0,0 +1,164 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'customer_groups_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Customer Groups Provider
///
/// Fetches list of customer groups (roles) from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
@ProviderFor(CustomerGroups)
const customerGroupsProvider = CustomerGroupsProvider._();
/// Customer Groups Provider
///
/// Fetches list of customer groups (roles) from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
final class CustomerGroupsProvider
extends $AsyncNotifierProvider<CustomerGroups, List<CustomerGroup>> {
/// Customer Groups Provider
///
/// Fetches list of customer groups (roles) from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
const CustomerGroupsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'customerGroupsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$customerGroupsHash();
@$internal
@override
CustomerGroups create() => CustomerGroups();
}
String _$customerGroupsHash() => r'df9107ef844e3cd320804af8d5dcf2fee2462208';
/// Customer Groups Provider
///
/// Fetches list of customer groups (roles) from API for registration form.
/// Requires active session (CSRF token and SID).
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
abstract class _$CustomerGroups extends $AsyncNotifier<List<CustomerGroup>> {
FutureOr<List<CustomerGroup>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref as $Ref<AsyncValue<List<CustomerGroup>>, List<CustomerGroup>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<CustomerGroup>>, List<CustomerGroup>>,
AsyncValue<List<CustomerGroup>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Provider to get a specific customer group by code/value
@ProviderFor(customerGroupByCode)
const customerGroupByCodeProvider = CustomerGroupByCodeFamily._();
/// Provider to get a specific customer group by code/value
final class CustomerGroupByCodeProvider
extends $FunctionalProvider<CustomerGroup?, CustomerGroup?, CustomerGroup?>
with $Provider<CustomerGroup?> {
/// Provider to get a specific customer group by code/value
const CustomerGroupByCodeProvider._({
required CustomerGroupByCodeFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'customerGroupByCodeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$customerGroupByCodeHash();
@override
String toString() {
return r'customerGroupByCodeProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<CustomerGroup?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
CustomerGroup? create(Ref ref) {
final argument = this.argument as String;
return customerGroupByCode(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(CustomerGroup? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<CustomerGroup?>(value),
);
}
@override
bool operator ==(Object other) {
return other is CustomerGroupByCodeProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$customerGroupByCodeHash() =>
r'0ddf96a19d7c20c9fe3ddfd2af80ac49bfe76512';
/// Provider to get a specific customer group by code/value
final class CustomerGroupByCodeFamily extends $Family
with $FunctionalFamilyOverride<CustomerGroup?, String> {
const CustomerGroupByCodeFamily._()
: super(
retry: null,
name: r'customerGroupByCodeProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider to get a specific customer group by code/value
CustomerGroupByCodeProvider call(String code) =>
CustomerGroupByCodeProvider._(argument: code, from: this);
@override
String toString() => r'customerGroupByCodeProvider';
}

View File

@@ -0,0 +1,107 @@
/// Session Provider
///
/// Manages authentication session (SID and CSRF token)
library;
import 'package:curl_logger_dio_interceptor/curl_logger_dio_interceptor.dart';
import 'package:dio/dio.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
import 'package:worker/features/auth/data/models/auth_session_model.dart';
part 'session_provider.g.dart';
/// Provider for Dio instance
@Riverpod(keepAlive: true)
Dio dio(Ref ref) {
final dio = Dio(
BaseOptions(
baseUrl: 'https://land.dbiz.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
),
);
// Add curl logger interceptor for debugging
dio.interceptors.add(CurlLoggerDioInterceptor(printOnSuccess: true));
return dio;
}
/// Provider for AuthRemoteDataSource
@Riverpod(keepAlive: true)
AuthRemoteDataSource authRemoteDataSource(Ref ref) {
final dio = ref.watch(dioProvider);
return AuthRemoteDataSource(dio);
}
/// Session State
class SessionState {
final String? sid;
final String? csrfToken;
final bool isLoading;
final String? error;
const SessionState({
this.sid,
this.csrfToken,
this.isLoading = false,
this.error,
});
SessionState copyWith({
String? sid,
String? csrfToken,
bool? isLoading,
String? error,
}) {
return SessionState(
sid: sid ?? this.sid,
csrfToken: csrfToken ?? this.csrfToken,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
bool get hasSession => sid != null && csrfToken != null;
}
/// Session Provider
///
/// Manages the authentication session including SID and CSRF token.
/// This should be called before making any authenticated requests.
/// keepAlive: true ensures the session persists across the app lifecycle.
@Riverpod(keepAlive: true)
class Session extends _$Session {
@override
SessionState build() {
return const SessionState();
}
/// Get session from API
Future<void> getSession() async {
state = state.copyWith(isLoading: true, error: null);
try {
final dataSource = ref.read(authRemoteDataSourceProvider);
final response = await dataSource.getSession();
state = SessionState(
sid: response.message.data.sid,
csrfToken: response.message.data.csrfToken,
isLoading: false,
);
} catch (e) {
state = SessionState(
isLoading: false,
error: e.toString(),
);
rethrow;
}
}
/// Clear session
void clearSession() {
state = const SessionState();
}
}

View File

@@ -0,0 +1,181 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'session_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for Dio instance
@ProviderFor(dio)
const dioProvider = DioProvider._();
/// Provider for Dio instance
final class DioProvider extends $FunctionalProvider<Dio, Dio, Dio>
with $Provider<Dio> {
/// Provider for Dio instance
const DioProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'dioProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$dioHash();
@$internal
@override
$ProviderElement<Dio> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
Dio create(Ref ref) {
return dio(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Dio value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Dio>(value),
);
}
}
String _$dioHash() => r'2bc10725a1b646cfaabd88c722e5101c06837c75';
/// Provider for AuthRemoteDataSource
@ProviderFor(authRemoteDataSource)
const authRemoteDataSourceProvider = AuthRemoteDataSourceProvider._();
/// Provider for AuthRemoteDataSource
final class AuthRemoteDataSourceProvider
extends
$FunctionalProvider<
AuthRemoteDataSource,
AuthRemoteDataSource,
AuthRemoteDataSource
>
with $Provider<AuthRemoteDataSource> {
/// Provider for AuthRemoteDataSource
const AuthRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'authRemoteDataSourceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$authRemoteDataSourceHash();
@$internal
@override
$ProviderElement<AuthRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
AuthRemoteDataSource create(Ref ref) {
return authRemoteDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AuthRemoteDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AuthRemoteDataSource>(value),
);
}
}
String _$authRemoteDataSourceHash() =>
r'6a18a0d3fee86c512b6eec01b8e8fda53a307a4c';
/// Session Provider
///
/// Manages the authentication session including SID and CSRF token.
/// This should be called before making any authenticated requests.
/// keepAlive: true ensures the session persists across the app lifecycle.
@ProviderFor(Session)
const sessionProvider = SessionProvider._();
/// Session Provider
///
/// Manages the authentication session including SID and CSRF token.
/// This should be called before making any authenticated requests.
/// keepAlive: true ensures the session persists across the app lifecycle.
final class SessionProvider extends $NotifierProvider<Session, SessionState> {
/// Session Provider
///
/// Manages the authentication session including SID and CSRF token.
/// This should be called before making any authenticated requests.
/// keepAlive: true ensures the session persists across the app lifecycle.
const SessionProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'sessionProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$sessionHash();
@$internal
@override
Session create() => Session();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SessionState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SessionState>(value),
);
}
}
String _$sessionHash() => r'9c755f010681d87ab3898c4daaa920501104df46';
/// Session Provider
///
/// Manages the authentication session including SID and CSRF token.
/// This should be called before making any authenticated requests.
/// keepAlive: true ensures the session persists across the app lifecycle.
abstract class _$Session extends $Notifier<SessionState> {
SessionState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<SessionState, SessionState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<SessionState, SessionState>,
SessionState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -273,6 +273,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
curl_logger_dio_interceptor:
dependency: "direct main"
description:
name: curl_logger_dio_interceptor
sha256: f20d89187a321d2150e1412bca30ebf4d89130bafc648ce21bd4f1ef4062b214
url: "https://pub.dev"
source: hosted
version: "1.0.0"
custom_lint:
dependency: "direct dev"
description:

View File

@@ -51,6 +51,7 @@ dependencies:
dio: ^5.4.3+1
connectivity_plus: ^6.0.3
pretty_dio_logger: ^1.3.1
curl_logger_dio_interceptor: ^1.0.0
dio_cache_interceptor: ^3.5.0
dio_cache_interceptor_hive_store: ^3.2.2