Merge pull request 'profile_changes' (#224) from profile_changes into develop

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/224
This commit is contained in:
a.ghabeli
2026-05-24 12:13:00 +03:30
21 changed files with 1236 additions and 396 deletions
+5
View File
@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.16602 1.66602H7.49935C3.33268 1.66602 1.66602 3.33268 1.66602 7.49935V12.4993C1.66602 16.666 3.33268 18.3327 7.49935 18.3327H12.4993C16.666 18.3327 18.3327 16.666 18.3327 12.4993V10.8327" stroke="#444444" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.3675 2.51639L6.80088 9.08306C6.55088 9.33306 6.30088 9.82472 6.25088 10.1831L5.89254 12.6914C5.75921 13.5997 6.40088 14.2331 7.30921 14.1081L9.81754 13.7497C10.1675 13.6997 10.6592 13.4497 10.9175 13.1997L17.4842 6.63306C18.6175 5.49972 19.1509 4.18306 17.4842 2.51639C15.8175 0.849722 14.5009 1.38306 13.3675 2.51639Z" stroke="#444444" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.4258 3.45898C12.9841 5.45065 14.5424 7.00898 16.5424 7.57565" stroke="#444444" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1012 B

+1
View File
@@ -71,6 +71,7 @@ class AssetsManager {
//profile page
static const sun = 'assets/icons/sun.svg';
static const moon = 'assets/icons/moon.svg';
static const editting = 'assets/icons/editting.svg';
//Liked tenders
static const trash = 'assets/icons/trash.svg';
+6 -5
View File
@@ -1,8 +1,9 @@
String timeConvertor(int time) {
DateTime date = DateTime.fromMillisecondsSinceEpoch(time);
import 'package:intl/intl.dart';
String formattedDate =
"${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}";
String timeConvertor(int timestamp) {
// Convert seconds → milliseconds
final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
return formattedDate;
// Format to yyyy-MM-dd
return DateFormat('yyyy-MM-dd').format(date);
}
@@ -16,4 +16,10 @@ class ProfileRepository {
Future<Result<CompanyProfileResponse>> getCompanyProfile() {
return _profileService.getCompanyProfile();
}
Future<Result<ProfileResponse>> updateProfileKeyWords({
required List<String> keyWords,
}) {
return _profileService.updateProfileKeyWords(keyWords: keyWords);
}
}
+1
View File
@@ -1,4 +1,5 @@
class ProfileApi {
static const String profile = '/api/v1/profile';
static const String companyProfile = '/api/v1/companies';
static const String updateProfileKeyWords = '/api/v1/profile/keywords';
}
@@ -25,6 +25,7 @@ abstract class ProfileData with _$ProfileData {
@JsonKey(name: 'last_login_at') required int? lastLoginAt,
@JsonKey(name: 'created_at') required int? createdAt,
@JsonKey(name: 'updated_at') required int? updatedAt,
List<String>? keywords,
}) = _ProfileData;
factory ProfileData.fromJson(Map<String, Object?> json) =>
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ProfileData {
String? get id;@JsonKey(name: 'full_name') String? get fullName; String? get username; String? get email; String? get role; String? get status; String? get department; String? get position; String? get phone; List<CompanyProfileData>? get companies;@JsonKey(name: 'profile_image') String? get profileImage;@JsonKey(name: 'is_verified') bool? get isVerified;@JsonKey(name: 'last_login_at') int? get lastLoginAt;@JsonKey(name: 'created_at') int? get createdAt;@JsonKey(name: 'updated_at') int? get updatedAt;
String? get id;@JsonKey(name: 'full_name') String? get fullName; String? get username; String? get email; String? get role; String? get status; String? get department; String? get position; String? get phone; List<CompanyProfileData>? get companies;@JsonKey(name: 'profile_image') String? get profileImage;@JsonKey(name: 'is_verified') bool? get isVerified;@JsonKey(name: 'last_login_at') int? get lastLoginAt;@JsonKey(name: 'created_at') int? get createdAt;@JsonKey(name: 'updated_at') int? get updatedAt; List<String>? get keywords;
/// Create a copy of ProfileData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $ProfileDataCopyWith<ProfileData> get copyWith => _$ProfileDataCopyWithImpl<Prof
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.fullName, fullName) || other.fullName == fullName)&&(identical(other.username, username) || other.username == username)&&(identical(other.email, email) || other.email == email)&&(identical(other.role, role) || other.role == role)&&(identical(other.status, status) || other.status == status)&&(identical(other.department, department) || other.department == department)&&(identical(other.position, position) || other.position == position)&&(identical(other.phone, phone) || other.phone == phone)&&const DeepCollectionEquality().equals(other.companies, companies)&&(identical(other.profileImage, profileImage) || other.profileImage == profileImage)&&(identical(other.isVerified, isVerified) || other.isVerified == isVerified)&&(identical(other.lastLoginAt, lastLoginAt) || other.lastLoginAt == lastLoginAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.fullName, fullName) || other.fullName == fullName)&&(identical(other.username, username) || other.username == username)&&(identical(other.email, email) || other.email == email)&&(identical(other.role, role) || other.role == role)&&(identical(other.status, status) || other.status == status)&&(identical(other.department, department) || other.department == department)&&(identical(other.position, position) || other.position == position)&&(identical(other.phone, phone) || other.phone == phone)&&const DeepCollectionEquality().equals(other.companies, companies)&&(identical(other.profileImage, profileImage) || other.profileImage == profileImage)&&(identical(other.isVerified, isVerified) || other.isVerified == isVerified)&&(identical(other.lastLoginAt, lastLoginAt) || other.lastLoginAt == lastLoginAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.keywords, keywords));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,fullName,username,email,role,status,department,position,phone,const DeepCollectionEquality().hash(companies),profileImage,isVerified,lastLoginAt,createdAt,updatedAt);
int get hashCode => Object.hash(runtimeType,id,fullName,username,email,role,status,department,position,phone,const DeepCollectionEquality().hash(companies),profileImage,isVerified,lastLoginAt,createdAt,updatedAt,const DeepCollectionEquality().hash(keywords));
@override
String toString() {
return 'ProfileData(id: $id, fullName: $fullName, username: $username, email: $email, role: $role, status: $status, department: $department, position: $position, phone: $phone, companies: $companies, profileImage: $profileImage, isVerified: $isVerified, lastLoginAt: $lastLoginAt, createdAt: $createdAt, updatedAt: $updatedAt)';
return 'ProfileData(id: $id, fullName: $fullName, username: $username, email: $email, role: $role, status: $status, department: $department, position: $position, phone: $phone, companies: $companies, profileImage: $profileImage, isVerified: $isVerified, lastLoginAt: $lastLoginAt, createdAt: $createdAt, updatedAt: $updatedAt, keywords: $keywords)';
}
@@ -48,7 +48,7 @@ abstract mixin class $ProfileDataCopyWith<$Res> {
factory $ProfileDataCopyWith(ProfileData value, $Res Function(ProfileData) _then) = _$ProfileDataCopyWithImpl;
@useResult
$Res call({
String? id,@JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies,@JsonKey(name: 'profile_image') String? profileImage,@JsonKey(name: 'is_verified') bool? isVerified,@JsonKey(name: 'last_login_at') int? lastLoginAt,@JsonKey(name: 'created_at') int? createdAt,@JsonKey(name: 'updated_at') int? updatedAt
String? id,@JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies,@JsonKey(name: 'profile_image') String? profileImage,@JsonKey(name: 'is_verified') bool? isVerified,@JsonKey(name: 'last_login_at') int? lastLoginAt,@JsonKey(name: 'created_at') int? createdAt,@JsonKey(name: 'updated_at') int? updatedAt, List<String>? keywords
});
@@ -65,7 +65,7 @@ class _$ProfileDataCopyWithImpl<$Res>
/// Create a copy of ProfileData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? fullName = freezed,Object? username = freezed,Object? email = freezed,Object? role = freezed,Object? status = freezed,Object? department = freezed,Object? position = freezed,Object? phone = freezed,Object? companies = freezed,Object? profileImage = freezed,Object? isVerified = freezed,Object? lastLoginAt = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? fullName = freezed,Object? username = freezed,Object? email = freezed,Object? role = freezed,Object? status = freezed,Object? department = freezed,Object? position = freezed,Object? phone = freezed,Object? companies = freezed,Object? profileImage = freezed,Object? isVerified = freezed,Object? lastLoginAt = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? keywords = freezed,}) {
return _then(_self.copyWith(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,fullName: freezed == fullName ? _self.fullName : fullName // ignore: cast_nullable_to_non_nullable
@@ -82,7 +82,8 @@ as String?,isVerified: freezed == isVerified ? _self.isVerified : isVerified //
as bool?,lastLoginAt: freezed == lastLoginAt ? _self.lastLoginAt : lastLoginAt // ignore: cast_nullable_to_non_nullable
as int?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as int?,
as int?,keywords: freezed == keywords ? _self.keywords : keywords // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
@@ -167,10 +168,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? id, @JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies, @JsonKey(name: 'profile_image') String? profileImage, @JsonKey(name: 'is_verified') bool? isVerified, @JsonKey(name: 'last_login_at') int? lastLoginAt, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'updated_at') int? updatedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? id, @JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies, @JsonKey(name: 'profile_image') String? profileImage, @JsonKey(name: 'is_verified') bool? isVerified, @JsonKey(name: 'last_login_at') int? lastLoginAt, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'updated_at') int? updatedAt, List<String>? keywords)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProfileData() when $default != null:
return $default(_that.id,_that.fullName,_that.username,_that.email,_that.role,_that.status,_that.department,_that.position,_that.phone,_that.companies,_that.profileImage,_that.isVerified,_that.lastLoginAt,_that.createdAt,_that.updatedAt);case _:
return $default(_that.id,_that.fullName,_that.username,_that.email,_that.role,_that.status,_that.department,_that.position,_that.phone,_that.companies,_that.profileImage,_that.isVerified,_that.lastLoginAt,_that.createdAt,_that.updatedAt,_that.keywords);case _:
return orElse();
}
@@ -188,10 +189,10 @@ return $default(_that.id,_that.fullName,_that.username,_that.email,_that.role,_t
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? id, @JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies, @JsonKey(name: 'profile_image') String? profileImage, @JsonKey(name: 'is_verified') bool? isVerified, @JsonKey(name: 'last_login_at') int? lastLoginAt, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'updated_at') int? updatedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? id, @JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies, @JsonKey(name: 'profile_image') String? profileImage, @JsonKey(name: 'is_verified') bool? isVerified, @JsonKey(name: 'last_login_at') int? lastLoginAt, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'updated_at') int? updatedAt, List<String>? keywords) $default,) {final _that = this;
switch (_that) {
case _ProfileData():
return $default(_that.id,_that.fullName,_that.username,_that.email,_that.role,_that.status,_that.department,_that.position,_that.phone,_that.companies,_that.profileImage,_that.isVerified,_that.lastLoginAt,_that.createdAt,_that.updatedAt);case _:
return $default(_that.id,_that.fullName,_that.username,_that.email,_that.role,_that.status,_that.department,_that.position,_that.phone,_that.companies,_that.profileImage,_that.isVerified,_that.lastLoginAt,_that.createdAt,_that.updatedAt,_that.keywords);case _:
throw StateError('Unexpected subclass');
}
@@ -208,10 +209,10 @@ return $default(_that.id,_that.fullName,_that.username,_that.email,_that.role,_t
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? id, @JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies, @JsonKey(name: 'profile_image') String? profileImage, @JsonKey(name: 'is_verified') bool? isVerified, @JsonKey(name: 'last_login_at') int? lastLoginAt, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'updated_at') int? updatedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? id, @JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies, @JsonKey(name: 'profile_image') String? profileImage, @JsonKey(name: 'is_verified') bool? isVerified, @JsonKey(name: 'last_login_at') int? lastLoginAt, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'updated_at') int? updatedAt, List<String>? keywords)? $default,) {final _that = this;
switch (_that) {
case _ProfileData() when $default != null:
return $default(_that.id,_that.fullName,_that.username,_that.email,_that.role,_that.status,_that.department,_that.position,_that.phone,_that.companies,_that.profileImage,_that.isVerified,_that.lastLoginAt,_that.createdAt,_that.updatedAt);case _:
return $default(_that.id,_that.fullName,_that.username,_that.email,_that.role,_that.status,_that.department,_that.position,_that.phone,_that.companies,_that.profileImage,_that.isVerified,_that.lastLoginAt,_that.createdAt,_that.updatedAt,_that.keywords);case _:
return null;
}
@@ -223,7 +224,7 @@ return $default(_that.id,_that.fullName,_that.username,_that.email,_that.role,_t
@JsonSerializable()
class _ProfileData implements ProfileData {
const _ProfileData({required this.id, @JsonKey(name: 'full_name') required this.fullName, required this.username, required this.email, required this.role, required this.status, required this.department, required this.position, required this.phone, required final List<CompanyProfileData>? companies, @JsonKey(name: 'profile_image') required this.profileImage, @JsonKey(name: 'is_verified') required this.isVerified, @JsonKey(name: 'last_login_at') required this.lastLoginAt, @JsonKey(name: 'created_at') required this.createdAt, @JsonKey(name: 'updated_at') required this.updatedAt}): _companies = companies;
const _ProfileData({required this.id, @JsonKey(name: 'full_name') required this.fullName, required this.username, required this.email, required this.role, required this.status, required this.department, required this.position, required this.phone, required final List<CompanyProfileData>? companies, @JsonKey(name: 'profile_image') required this.profileImage, @JsonKey(name: 'is_verified') required this.isVerified, @JsonKey(name: 'last_login_at') required this.lastLoginAt, @JsonKey(name: 'created_at') required this.createdAt, @JsonKey(name: 'updated_at') required this.updatedAt, final List<String>? keywords}): _companies = companies,_keywords = keywords;
factory _ProfileData.fromJson(Map<String, dynamic> json) => _$ProfileDataFromJson(json);
@override final String? id;
@@ -249,6 +250,15 @@ class _ProfileData implements ProfileData {
@override@JsonKey(name: 'last_login_at') final int? lastLoginAt;
@override@JsonKey(name: 'created_at') final int? createdAt;
@override@JsonKey(name: 'updated_at') final int? updatedAt;
final List<String>? _keywords;
@override List<String>? get keywords {
final value = _keywords;
if (value == null) return null;
if (_keywords is EqualUnmodifiableListView) return _keywords;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
/// Create a copy of ProfileData
/// with the given fields replaced by the non-null parameter values.
@@ -263,16 +273,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.fullName, fullName) || other.fullName == fullName)&&(identical(other.username, username) || other.username == username)&&(identical(other.email, email) || other.email == email)&&(identical(other.role, role) || other.role == role)&&(identical(other.status, status) || other.status == status)&&(identical(other.department, department) || other.department == department)&&(identical(other.position, position) || other.position == position)&&(identical(other.phone, phone) || other.phone == phone)&&const DeepCollectionEquality().equals(other._companies, _companies)&&(identical(other.profileImage, profileImage) || other.profileImage == profileImage)&&(identical(other.isVerified, isVerified) || other.isVerified == isVerified)&&(identical(other.lastLoginAt, lastLoginAt) || other.lastLoginAt == lastLoginAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.fullName, fullName) || other.fullName == fullName)&&(identical(other.username, username) || other.username == username)&&(identical(other.email, email) || other.email == email)&&(identical(other.role, role) || other.role == role)&&(identical(other.status, status) || other.status == status)&&(identical(other.department, department) || other.department == department)&&(identical(other.position, position) || other.position == position)&&(identical(other.phone, phone) || other.phone == phone)&&const DeepCollectionEquality().equals(other._companies, _companies)&&(identical(other.profileImage, profileImage) || other.profileImage == profileImage)&&(identical(other.isVerified, isVerified) || other.isVerified == isVerified)&&(identical(other.lastLoginAt, lastLoginAt) || other.lastLoginAt == lastLoginAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other._keywords, _keywords));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,fullName,username,email,role,status,department,position,phone,const DeepCollectionEquality().hash(_companies),profileImage,isVerified,lastLoginAt,createdAt,updatedAt);
int get hashCode => Object.hash(runtimeType,id,fullName,username,email,role,status,department,position,phone,const DeepCollectionEquality().hash(_companies),profileImage,isVerified,lastLoginAt,createdAt,updatedAt,const DeepCollectionEquality().hash(_keywords));
@override
String toString() {
return 'ProfileData(id: $id, fullName: $fullName, username: $username, email: $email, role: $role, status: $status, department: $department, position: $position, phone: $phone, companies: $companies, profileImage: $profileImage, isVerified: $isVerified, lastLoginAt: $lastLoginAt, createdAt: $createdAt, updatedAt: $updatedAt)';
return 'ProfileData(id: $id, fullName: $fullName, username: $username, email: $email, role: $role, status: $status, department: $department, position: $position, phone: $phone, companies: $companies, profileImage: $profileImage, isVerified: $isVerified, lastLoginAt: $lastLoginAt, createdAt: $createdAt, updatedAt: $updatedAt, keywords: $keywords)';
}
@@ -283,7 +293,7 @@ abstract mixin class _$ProfileDataCopyWith<$Res> implements $ProfileDataCopyWith
factory _$ProfileDataCopyWith(_ProfileData value, $Res Function(_ProfileData) _then) = __$ProfileDataCopyWithImpl;
@override @useResult
$Res call({
String? id,@JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies,@JsonKey(name: 'profile_image') String? profileImage,@JsonKey(name: 'is_verified') bool? isVerified,@JsonKey(name: 'last_login_at') int? lastLoginAt,@JsonKey(name: 'created_at') int? createdAt,@JsonKey(name: 'updated_at') int? updatedAt
String? id,@JsonKey(name: 'full_name') String? fullName, String? username, String? email, String? role, String? status, String? department, String? position, String? phone, List<CompanyProfileData>? companies,@JsonKey(name: 'profile_image') String? profileImage,@JsonKey(name: 'is_verified') bool? isVerified,@JsonKey(name: 'last_login_at') int? lastLoginAt,@JsonKey(name: 'created_at') int? createdAt,@JsonKey(name: 'updated_at') int? updatedAt, List<String>? keywords
});
@@ -300,7 +310,7 @@ class __$ProfileDataCopyWithImpl<$Res>
/// Create a copy of ProfileData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? fullName = freezed,Object? username = freezed,Object? email = freezed,Object? role = freezed,Object? status = freezed,Object? department = freezed,Object? position = freezed,Object? phone = freezed,Object? companies = freezed,Object? profileImage = freezed,Object? isVerified = freezed,Object? lastLoginAt = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? fullName = freezed,Object? username = freezed,Object? email = freezed,Object? role = freezed,Object? status = freezed,Object? department = freezed,Object? position = freezed,Object? phone = freezed,Object? companies = freezed,Object? profileImage = freezed,Object? isVerified = freezed,Object? lastLoginAt = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? keywords = freezed,}) {
return _then(_ProfileData(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,fullName: freezed == fullName ? _self.fullName : fullName // ignore: cast_nullable_to_non_nullable
@@ -317,7 +327,8 @@ as String?,isVerified: freezed == isVerified ? _self.isVerified : isVerified //
as bool?,lastLoginAt: freezed == lastLoginAt ? _self.lastLoginAt : lastLoginAt // ignore: cast_nullable_to_non_nullable
as int?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as int?,
as int?,keywords: freezed == keywords ? _self._keywords : keywords // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
@@ -25,6 +25,8 @@ _ProfileData _$ProfileDataFromJson(Map<String, dynamic> json) => _ProfileData(
lastLoginAt: (json['last_login_at'] as num?)?.toInt(),
createdAt: (json['created_at'] as num?)?.toInt(),
updatedAt: (json['updated_at'] as num?)?.toInt(),
keywords:
(json['keywords'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$ProfileDataToJson(_ProfileData instance) =>
@@ -44,4 +46,5 @@ Map<String, dynamic> _$ProfileDataToJson(_ProfileData instance) =>
'last_login_at': instance.lastLoginAt,
'created_at': instance.createdAt,
'updated_at': instance.updatedAt,
'keywords': instance.keywords,
};
+15
View File
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:tm_app/data/services/api/profile_api.dart';
import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.dart';
import 'package:tm_app/data/services/model/profile_response/profile_response.dart';
@@ -28,4 +30,17 @@ class ProfileService {
);
return result;
}
Future<Result<ProfileResponse>> updateProfileKeyWords({
required List<String> keyWords,
}) async {
Map<String, dynamic> data = {'keywords': keyWords};
final result = await _networkManager.makeRequest(
ProfileApi.updateProfileKeyWords,
method: 'PUT',
data: jsonEncode(data),
(json) => ProfileResponse.fromJson(json),
);
return result;
}
}
+4
View File
@@ -77,6 +77,9 @@ class $AssetsIconsGen {
/// File path: assets/icons/edit.svg
String get edit => 'assets/icons/edit.svg';
/// File path: assets/icons/editting.svg
String get editting => 'assets/icons/editting.svg';
/// File path: assets/icons/filter.svg
String get filter => 'assets/icons/filter.svg';
@@ -200,6 +203,7 @@ class $AssetsIconsGen {
danger,
dislike,
edit,
editting,
filter,
home,
homeActive,
+21
View File
@@ -94,4 +94,25 @@ class ProfileViewModel with ChangeNotifier {
_loggedOut = false;
notifyListeners();
}
Future<void> updateProfileKeyWords({required List<String> keyWords}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
final result = await _profileRepository.updateProfileKeyWords(
keyWords: keyWords,
);
switch (result) {
case Ok<ProfileResponse>():
_profileData = result.value.data;
break;
case Error<ProfileResponse>():
_errorMessage = result.error.toString();
break;
}
_isLoading = false;
notifyListeners();
_errorMessage = null;
notifyListeners();
}
}
+32 -123
View File
@@ -1,19 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/view_models/profile_view_model.dart';
import '../../../core/constants/assets.dart';
import '../../../core/constants/common_strings.dart';
import '../../../core/theme/colors.dart';
import '../../../core/utils/app_toast.dart';
import '../../../core/utils/size_config.dart';
import '../../../view_models/auth_view_model.dart';
import '../../shared/desktop_navigation_widget.dart';
import '../strings/profile_strings.dart';
import '../widgets/theme_toggle.dart';
import '../widgets/title_description.dart';
import '../widgets/main_data_tab.dart';
import '../widgets/members_tab.dart';
import '../widgets/profile_tab_bar.dart';
import '../widgets/subject_tab.dart';
class DesktopProfilePage extends StatefulWidget {
const DesktopProfilePage({super.key});
@@ -22,12 +21,15 @@ class DesktopProfilePage extends StatefulWidget {
State<DesktopProfilePage> createState() => _DesktopProfilePageState();
}
class _DesktopProfilePageState extends State<DesktopProfilePage> {
class _DesktopProfilePageState extends State<DesktopProfilePage>
with SingleTickerProviderStateMixin {
late ProfileViewModel viewModel;
late final TabController tabController;
@override
void initState() {
super.initState();
viewModel = context.read<ProfileViewModel>();
tabController = TabController(length: 3, vsync: this);
viewModel.addListener(_profileListener);
}
@@ -56,150 +58,57 @@ class _DesktopProfilePageState extends State<DesktopProfilePage> {
const DesktopNavigationWidget(
currentIndex: 4, // Tenders index
),
Consumer<ProfileViewModel>(
Expanded(
child: Consumer<ProfileViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading) {
return const Expanded(
child: Center(
return const Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
),
);
}
if (viewModel.errorMessage != null) {
return Expanded(
child: Center(child: Text(viewModel.errorMessage!)),
);
return Center(child: Text(viewModel.errorMessage!));
}
if (viewModel.profileData == null) {
return const Expanded(
child: Center(child: Text(CommonStrings.noData)),
);
return const Center(child: Text(CommonStrings.noData));
}
return Expanded(
child: SingleChildScrollView(
child: Column(
children: [
SizedBox(
return SizedBox(
width: 740,
height: double.infinity,
child: Column(
children: [
SizedBox(height: 36.0.h()),
TitleDescription(
title: ProfileStrings.fullname,
description:
viewModel.profileData!.fullName ?? '',
),
TitleDescription(
title: ProfileStrings.username,
description:
viewModel.profileData!.username ?? '',
),
TitleDescription(
title: ProfileStrings.email,
description: viewModel.profileData!.email ?? '',
),
TitleDescription(
title: ProfileStrings.phone,
description: viewModel.profileData!.phone ?? '',
),
TitleDescription(
title: ProfileStrings.companyName,
description:
viewModel
.profileData!
.companies!
.first
.name ??
'',
),
SizedBox(height: 24.0.h()),
// Theme Toggle
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
ProfileStrings.settings,
style: TextStyle(
fontSize: 20.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
),
SizedBox(height: 36.0.h()),
_themeChangeRow(),
SizedBox(height: 24.0.h()),
Divider(
color: AppColors.dividerColor,
thickness: 1,
),
SizedBox(height: 24.0.h()),
// Spacer(),
Align(
alignment: AlignmentDirectional.centerEnd,
child: InkWell(
onTap: () => viewModel.logout(),
borderRadius: BorderRadius.circular(100),
child: Container(
width: 156,
height: 56.0.h(),
decoration: BoxDecoration(
color: AppColors.primary20,
borderRadius: BorderRadius.circular(100),
),
alignment: Alignment.center,
child: Text(
ProfileStrings.logout,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w500,
color: AppColors.primaryColor,
),
),
),
const SizedBox(height: 46),
ProfileTabBar(
controller: tabController,
titles: [
ProfileStrings.mainDataTab,
ProfileStrings.membersTab,
ProfileStrings.subjectTab,
],
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
MainDataTab(
profileData: viewModel.profileData!,
onLogout: () => viewModel.logout(),
),
const MembersTab(),
const SubjectTab(),
],
),
),
],
),
),
);
},
),
],
),
);
}
Widget _themeChangeRow() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0.w()),
child: Row(
children: [
Text(
ProfileStrings.appTheme,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
const Spacer(),
SvgPicture.asset(AssetsManager.sun),
SizedBox(width: 12.0.w()),
SizedBox(
width: 52.0.w(),
height: 32.0.h(),
child: const ThemeToggle(),
),
SizedBox(width: 12.0.w()),
SvgPicture.asset(AssetsManager.moon),
],
),
);
+28 -106
View File
@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/view_models/profile_view_model.dart';
import 'package:tm_app/views/profile/widgets/theme_toggle.dart';
import '../../../core/constants/assets.dart';
import '../../../core/constants/common_strings.dart';
import '../../../core/theme/colors.dart';
import '../../../core/utils/app_toast.dart';
@@ -13,7 +10,10 @@ import '../../../core/utils/size_config.dart';
import '../../../view_models/auth_view_model.dart';
import '../../shared/tender_app_bar.dart';
import '../strings/profile_strings.dart';
import '../widgets/title_description.dart';
import '../widgets/main_data_tab.dart';
import '../widgets/members_tab.dart';
import '../widgets/profile_tab_bar.dart';
import '../widgets/subject_tab.dart';
class MobileProfilePage extends StatefulWidget {
const MobileProfilePage({super.key});
@@ -22,12 +22,15 @@ class MobileProfilePage extends StatefulWidget {
State<MobileProfilePage> createState() => _MobileProfilePageState();
}
class _MobileProfilePageState extends State<MobileProfilePage> {
class _MobileProfilePageState extends State<MobileProfilePage>
with SingleTickerProviderStateMixin {
late ProfileViewModel viewModel;
late final TabController tabController;
@override
void initState() {
super.initState();
viewModel = context.read<ProfileViewModel>();
tabController = TabController(length: 3, vsync: this);
viewModel.addListener(_profileListener);
// Load notification info saved in SharedPreferences
}
@@ -53,7 +56,8 @@ class _MobileProfilePageState extends State<MobileProfilePage> {
return Scaffold(
backgroundColor: AppColors.backgroundColor,
appBar: tenderMobileAppBar(title: ProfileStrings.profileTitle),
body: Consumer<ProfileViewModel>(
body: SafeArea(
child: Consumer<ProfileViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading) {
return const Center(
@@ -69,121 +73,39 @@ class _MobileProfilePageState extends State<MobileProfilePage> {
return const Center(child: Text(CommonStrings.noData));
}
return SafeArea(
child: Padding(
return Padding(
padding: EdgeInsets.symmetric(
horizontal: 24.0.w(),
vertical: 16.0.h(),
),
child: Column(
children: [
SizedBox(height: 36.0.h()),
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
'Last Notification',
style: TextStyle(
fontSize: 18.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
),
SizedBox(height: 24.0.h()),
TitleDescription(
title: ProfileStrings.fullname,
description: viewModel.profileData!.fullName ?? '',
),
TitleDescription(
title: ProfileStrings.username,
description: viewModel.profileData!.username ?? '',
),
TitleDescription(
title: ProfileStrings.email,
description: viewModel.profileData!.email ?? '',
),
TitleDescription(
title: ProfileStrings.phone,
description: viewModel.profileData!.phone ?? '',
),
TitleDescription(
title: ProfileStrings.companyName,
description:
viewModel.profileData!.companies!.first.name ?? '',
),
SizedBox(height: 32.0.h()),
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
ProfileStrings.settings,
style: TextStyle(
fontSize: 20.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
),
SizedBox(height: 36.0.h()),
_themeChangeRow(),
SizedBox(height: 24.0.h()),
Divider(color: AppColors.dividerColor, thickness: 1),
SizedBox(height: 24.0.h()),
const Spacer(),
InkWell(
onTap: () => viewModel.logout(),
borderRadius: BorderRadius.circular(100),
child: Container(
width: double.infinity,
height: 56.0.h(),
decoration: BoxDecoration(
color: AppColors.primary20,
borderRadius: BorderRadius.circular(100),
),
alignment: Alignment.center,
child: Text(
ProfileStrings.logout,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w500,
color: AppColors.primaryColor,
ProfileTabBar(
controller: tabController,
titles: [
ProfileStrings.mainDataTab,
ProfileStrings.membersTab,
ProfileStrings.subjectTab,
],
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
MainDataTab(
profileData: viewModel.profileData!,
onLogout: () => viewModel.logout(),
),
const MembersTab(),
const SubjectTab(),
],
),
),
],
),
),
);
},
),
);
}
Widget _themeChangeRow() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0.w()),
child: Row(
children: [
Text(
ProfileStrings.appTheme,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
const Spacer(),
SvgPicture.asset(AssetsManager.sun),
SizedBox(width: 12.0.w()),
SizedBox(
width: 52.0.w(),
height: 32.0.h(),
child: const ThemeToggle(),
),
SizedBox(width: 12.0.w()),
SvgPicture.asset(AssetsManager.moon),
],
),
);
}
+24 -116
View File
@@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/common_strings.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/view_models/profile_view_model.dart';
@@ -13,8 +11,9 @@ import '../../../view_models/auth_view_model.dart';
import '../../shared/tablet_navigation_widget.dart';
import '../../shared/tender_app_bar.dart';
import '../strings/profile_strings.dart';
import '../widgets/theme_toggle.dart';
import '../widgets/title_description.dart';
import '../widgets/main_data_tab.dart';
import '../widgets/members_tab.dart';
import '../widgets/profile_tab_bar.dart';
class TabletProfilePage extends StatefulWidget {
const TabletProfilePage({super.key});
@@ -23,12 +22,15 @@ class TabletProfilePage extends StatefulWidget {
State<TabletProfilePage> createState() => _TabletProfilePageState();
}
class _TabletProfilePageState extends State<TabletProfilePage> {
class _TabletProfilePageState extends State<TabletProfilePage>
with SingleTickerProviderStateMixin {
late ProfileViewModel viewModel;
late final TabController tabController;
@override
void initState() {
super.initState();
viewModel = context.read<ProfileViewModel>();
tabController = TabController(length: 3, vsync: this);
viewModel.addListener(_profileListener);
}
@@ -84,91 +86,25 @@ class _TabletProfilePageState extends State<TabletProfilePage> {
),
child: Column(
children: [
SizedBox(height: 128.0.h()),
TitleDescription(
title: ProfileStrings.fullname,
description: viewModel.profileData!.fullName ?? '',
),
TitleDescription(
title: ProfileStrings.username,
description: viewModel.profileData!.username ?? '',
),
TitleDescription(
title: ProfileStrings.email,
description: viewModel.profileData!.email ?? '',
),
TitleDescription(
title: ProfileStrings.phone,
description: viewModel.profileData!.phone ?? '',
),
TitleDescription(
title: ProfileStrings.companyName,
description:
viewModel.profileData!.companies!.first.name ?? '',
),
// TitleDescription(
// title: ProfileStrings.companyName,
// description: viewModel.companyProfileData!.name!,
// ),
// TitleDescription(
// title: ProfileStrings.nationalId,
// description: viewModel.companyProfileData!.id!,
// ),
// TitleDescription(
// title: ProfileStrings.registrationNumber,
// description:
// viewModel.companyProfileData!.registrationNumber!,
// ),
// const TitleDescription(
// title: ProfileStrings.businessType,
// description: '-',
// ),
// TitleDescription(
// title: ProfileStrings.founded,
// description:
// viewModel.companyProfileData!.foundedYear!.toString(),
// ),
SizedBox(height: 24.0.h()),
// Theme Toggle
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
ProfileStrings.settings,
style: TextStyle(
fontSize: 20.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
),
SizedBox(height: 36.0.h()),
_themeChangeRow(),
SizedBox(height: 24.0.h()),
Divider(color: AppColors.dividerColor, thickness: 1),
SizedBox(height: 24.0.h()),
// Spacer(),
Align(
alignment: AlignmentDirectional.centerStart,
child: InkWell(
onTap: () => viewModel.logout(),
borderRadius: BorderRadius.circular(100),
child: Container(
width: 156,
height: 56.0.h(),
decoration: BoxDecoration(
color: AppColors.primary20,
borderRadius: BorderRadius.circular(100),
),
alignment: Alignment.center,
child: Text(
ProfileStrings.logout,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w500,
color: AppColors.primaryColor,
),
ProfileTabBar(
controller: tabController,
titles: [
ProfileStrings.mainDataTab,
ProfileStrings.membersTab,
ProfileStrings.subjectTab,
],
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
MainDataTab(
profileData: viewModel.profileData!,
onLogout: () => viewModel.logout(),
),
const MembersTab(),
const Center(child: Text('Subject Tab Content')),
],
),
),
],
@@ -179,32 +115,4 @@ class _TabletProfilePageState extends State<TabletProfilePage> {
},
);
}
Widget _themeChangeRow() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0.w()),
child: Row(
children: [
Text(
ProfileStrings.appTheme,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
const Spacer(),
SvgPicture.asset(AssetsManager.sun),
SizedBox(width: 12.0.w()),
SizedBox(
width: 52.0.w(),
height: 32.0.h(),
child: const ThemeToggle(),
),
SizedBox(width: 12.0.w()),
SvgPicture.asset(AssetsManager.moon),
],
),
);
}
}
@@ -20,4 +20,7 @@ class ProfileStrings {
static const String position = 'position';
static const String phone = 'phone';
static const String fullname = 'full name';
static const String mainDataTab = 'Main Data';
static const String membersTab = 'Members';
static const String subjectTab = 'Subject';
}
@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/login/widgets/widgets.dart';
import '../../../core/constants/assets.dart';
import '../../../core/theme/colors.dart';
class EditKeyWordBottomSheet extends StatefulWidget {
const EditKeyWordBottomSheet({
required this.keyWords,
required this.isDesktop,
this.onSubmit,
super.key,
});
final List<String> keyWords;
final Future<void> Function(List<String> keyWords)? onSubmit;
final bool isDesktop;
@override
State<EditKeyWordBottomSheet> createState() => _EditKeyWordBottomSheetState();
}
class _EditKeyWordBottomSheetState extends State<EditKeyWordBottomSheet> {
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
late final List<String> _keywords = List.from(widget.keyWords);
@override
Widget build(BuildContext context) {
return Wrap(
children: [
Container(
width: double.infinity,
padding: EdgeInsets.only(
left: 24.0.w(),
right: 24.0.w(),
top: 24.0.h(),
bottom: MediaQuery.of(context).viewInsets.bottom,
),
decoration: BoxDecoration(
color: AppColors.backgroundColor,
borderRadius: BorderRadius.vertical(
top: const Radius.circular(24),
bottom:
widget.isDesktop ? const Radius.circular(24) : Radius.zero,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Edit Key Word',
style: TextStyle(
fontSize: 20.0.sp(),
fontWeight: FontWeight.w600,
),
),
IconButton(
onPressed: () => context.pop(),
icon: SvgPicture.asset(
AssetsManager.closeCircle,
width: 20.0.w(),
height: 20.0.w(),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
AppColors.grey60,
BlendMode.srcIn,
),
),
),
],
),
SizedBox(height: 16.0.h()),
Divider(color: AppColors.grey20),
SizedBox(height: 24.0.h()),
TextField(
focusNode: _focusNode,
controller: _controller,
onSubmitted: (value) {
if (value.trim().isEmpty) {
return;
}
setState(() {
_keywords.add(value);
});
_controller.clear();
Future.delayed(const Duration(milliseconds: 50), () {
_focusNode.requestFocus();
});
},
decoration: InputDecoration(
hintText: 'Key Word',
hintStyle: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
border: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.grey40),
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.grey40),
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.grey40),
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
errorBorder: const OutlineInputBorder(
borderSide: BorderSide(color: AppColors.errorColor),
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
),
SizedBox(height: 16.0.h()),
SizedBox(
height: 200.0.h(), // Set a fixed height for the container
child: SingleChildScrollView(
child: Wrap(
spacing: 8,
runSpacing: 16.0.h(),
children:
_keywords
.map(
(e) => _chip(
label: e,
backgroundColor: Colors.green[200]!,
onDelete:
() => setState(() {
_keywords.remove(e);
}),
),
)
.toList(),
// [
// _chip(label: 'UIUX', backgroundColor: Colors.green[200]!),
// _chip(
// label: 'Lorem ipsum',
// backgroundColor: Colors.orange[100]!,
// ),
// _chip(label: 'sample', backgroundColor: Colors.grey[300]!),
// _chip(label: 'Unknown', backgroundColor: Colors.red[200]!),
// _chip(
// label: 'Lorem ipsum 02',
// backgroundColor: Colors.blue[100]!,
// ),
// ],
),
),
),
SizedBox(height: 32.0.h()),
BaseButton(
isEnabled: true,
text: 'Submit Request',
backgroundColor: AppColors.primary20,
textColor: AppColors.mainBlue,
onPressed: () async {
await widget.onSubmit?.call(_keywords);
if (context.mounted) {
context.pop();
}
},
),
SizedBox(height: 24.0.h()),
],
),
),
],
);
}
Widget _chip({
required String label,
required Color backgroundColor,
required VoidCallback onDelete,
}) {
return InputChip(
label: Text(label),
backgroundColor: backgroundColor,
onDeleted: onDelete,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(color: Colors.transparent),
),
);
}
}
@@ -0,0 +1,297 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:tm_app/core/utils/size_config.dart';
// Note: Ensure your size_config and theme imports are present
// import 'package:tm_app/core/utils/size_config.dart';
// import 'package:tm_app/core/theme/colors.dart';
class EditKeyWordBottomSheet extends StatefulWidget {
const EditKeyWordBottomSheet({super.key});
@override
State<EditKeyWordBottomSheet> createState() => _EditKeyWordBottomSheetState();
}
class _EditKeyWordBottomSheetState extends State<EditKeyWordBottomSheet> {
final List<String> _keywords = [
'1234',
'Lorem ipsum',
'sample',
'Unknown',
'Lorem ipsum 02',
];
// Location selection state
bool _isDropdownOpen = false;
final List<Map<String, String>> _selectedCountries = [
{'code': 'NO', 'name': 'Norway', 'flag': '🇳🇴'},
{'code': 'SE', 'name': 'Sweden', 'flag': '🇸🇪'},
];
final List<Map<String, String>> _allCountries = [
{'code': 'AU', 'name': 'Australia', 'flag': '🇦🇺'},
{'code': 'AT', 'name': 'Austria', 'flag': '🇦🇹'},
{'code': 'AZ', 'name': 'Azerbaijan', 'flag': '🇦🇿'},
{'code': 'BS', 'name': 'Bahamas', 'flag': '🇧🇸'},
{'code': 'BH', 'name': 'Bahrain', 'flag': '🇧🇭'},
];
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: EdgeInsets.only(
left: 24.0.w(),
right: 24.0.w(),
top: 24.0.h(),
bottom: MediaQuery.of(context).viewInsets.bottom + 24.0.h(),
),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// KEY WORD SECTION
_buildSectionHeader('Key Word'),
SizedBox(height: 16.0.h()),
Wrap(
spacing: 8,
runSpacing: 12.0.h(),
children: _keywords.map((e) => _buildKeywordChip(e)).toList(),
),
SizedBox(height: 32.0.h()),
const Divider(color: Color(0xFFF1F1F1)),
SizedBox(height: 24.0.h()),
// LOCATION SECTION
_buildSectionHeader('Location'),
SizedBox(height: 16.0.h()),
Stack(
clipBehavior: Clip.none,
children: [
// 1. Selected Country Chips (Below the input)
Padding(
padding: EdgeInsets.only(top: 64.0.h()),
child: Wrap(
spacing: 12,
runSpacing: 8,
children:
_selectedCountries
.map((c) => _buildCountryChip(c))
.toList(),
),
),
// 2. The Pseudo-TextField
GestureDetector(
onTap: () => setState(() => _isDropdownOpen = !_isDropdownOpen),
child: Container(
height: 52.0.h(),
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFD1E3FF)),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Location',
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 16.0.sp(),
),
),
Icon(
_isDropdownOpen
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
color: Colors.grey,
),
],
),
),
),
// 3. Floating Dropdown Menu (Positioned ABOVE the input field)
if (_isDropdownOpen)
Positioned(
bottom: 60.0.h(), // Sits above the text field
left: 0,
right: 0,
child: Material(
elevation: 12,
shadowColor: Colors.black26,
borderRadius: BorderRadius.circular(12),
color: Colors.white,
child: Container(
constraints: BoxConstraints(maxHeight: 220.0.h()),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade100),
),
child: ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: _allCountries.length,
separatorBuilder:
(context, index) => const Divider(
height: 1,
color: Color(0xFFF5F5F5),
),
itemBuilder: (context, index) {
final country = _allCountries[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
),
leading: Text(
country['flag']!,
style: const TextStyle(fontSize: 22),
),
title: Text(
country['name']!,
style: TextStyle(
fontSize: 15.0.sp(),
color: Colors.black87,
),
),
onTap: () {
setState(() {
// Add if not already selected
if (!_selectedCountries.any(
(c) => c['code'] == country['code'],
)) {
_selectedCountries.add(country);
}
_isDropdownOpen = false;
});
},
);
},
),
),
),
),
],
),
SizedBox(height: 40.0.h()),
// SAVE BUTTON
SizedBox(
width: double.infinity,
height: 54.0.h(),
child: ElevatedButton(
onPressed: () => context.pop(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFFE8F1FF,
), // Matches "Save" blue background
foregroundColor: const Color(
0xFF1A73E8,
), // Matches "Save" text color
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
),
child: Text(
'Save',
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
Widget _buildSectionHeader(String title) {
return Row(
children: [
Text(
title,
style: TextStyle(
fontSize: 18.0.sp(),
fontWeight: FontWeight.w500,
color: const Color(0xFF616161),
),
),
SizedBox(width: 8.0.w()),
Icon(
Icons.edit_outlined,
size: 20.0.w(),
color: const Color(0xFF757575),
),
],
);
}
Widget _buildKeywordChip(String label) {
// Coloring logic to match screenshot
Color bg = const Color(0xFFF5F5F5);
Color text = const Color(0xFF424242);
if (label == '1234') bg = const Color(0xFFE8F5E9);
if (label == 'Lorem ipsum') bg = const Color(0xFFFFF3E0);
if (label == 'Unknown') bg = const Color(0xFFFFEBEE);
if (label == 'Lorem ipsum 02' || label == 'sample')
bg = const Color(0xFFECEFF1);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
style: TextStyle(
color: text,
fontSize: 14.0.sp(),
fontWeight: FontWeight.w400,
),
),
);
}
Widget _buildCountryChip(Map<String, String> country) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
country['code']!,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w600,
color: const Color(0xFF424242),
),
),
SizedBox(width: 6.0.w()),
Text(country['flag']!, style: const TextStyle(fontSize: 18)),
SizedBox(width: 6.0.w()),
GestureDetector(
onTap: () => setState(() => _selectedCountries.remove(country)),
child: const Icon(Icons.cancel, size: 18, color: Colors.grey),
),
],
),
);
}
}
@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:tm_app/core/utils/size_config.dart';
import '../../../core/constants/assets.dart';
import '../../../core/theme/colors.dart';
import '../../../data/services/model/profile_data/profile_data.dart';
import '../strings/profile_strings.dart';
import 'theme_toggle.dart';
import 'title_description.dart';
class MainDataTab extends StatelessWidget {
const MainDataTab({
required this.profileData,
required this.onLogout,
super.key,
});
final ProfileData profileData;
final VoidCallback onLogout;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 36.0.h()),
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
'Last Notification',
style: TextStyle(
fontSize: 18.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
),
SizedBox(height: 24.0.h()),
TitleDescription(
title: ProfileStrings.fullname,
description: profileData.fullName ?? '',
),
TitleDescription(
title: ProfileStrings.username,
description: profileData.username ?? '',
),
TitleDescription(
title: ProfileStrings.email,
description: profileData.email ?? '',
),
TitleDescription(
title: ProfileStrings.phone,
description: profileData.phone ?? '',
),
TitleDescription(
title: ProfileStrings.companyName,
description: profileData.companies!.first.name ?? '',
),
SizedBox(height: 32.0.h()),
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
ProfileStrings.settings,
style: TextStyle(
fontSize: 20.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
),
SizedBox(height: 36.0.h()),
_themeChangeRow(),
SizedBox(height: 24.0.h()),
Divider(color: AppColors.dividerColor, thickness: 1),
SizedBox(height: 24.0.h()),
InkWell(
onTap: onLogout,
borderRadius: BorderRadius.circular(100),
child: Container(
width: double.infinity,
height: 56.0.h(),
decoration: BoxDecoration(
color: AppColors.primary20,
borderRadius: BorderRadius.circular(100),
),
alignment: Alignment.center,
child: Text(
ProfileStrings.logout,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w500,
color: AppColors.primaryColor,
),
),
),
),
],
),
);
}
Widget _themeChangeRow() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0.w()),
child: Row(
children: [
Text(
ProfileStrings.appTheme,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
const Spacer(),
SvgPicture.asset(AssetsManager.sun),
SizedBox(width: 12.0.w()),
SizedBox(
width: 52.0.w(),
height: 32.0.h(),
child: const ThemeToggle(),
),
SizedBox(width: 12.0.w()),
SvgPicture.asset(AssetsManager.moon),
],
),
);
}
}
+116
View File
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
class MembersTab extends StatelessWidget {
const MembersTab({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(height: 36.0.h()),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_columnName('Full Name'),
_columnName('Position'),
_columnName('Access level'),
],
),
SizedBox(height: 24.0.h()),
Divider(color: AppColors.grey50),
SizedBox(height: 24.0.h()),
Expanded(
child: ListView.separated(
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
final member = _members[index];
return _member(
fullName: member.fullName,
position: member.position,
accessLevel: member.accessLevel,
);
},
separatorBuilder:
(context, index) =>
Divider(color: AppColors.grey50.withValues(alpha: 0.2)),
itemCount: _members.length,
),
),
],
);
}
Widget _columnName(String text) {
return SizedBox(
width: 100.0.w(),
child: Text(
text,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
);
}
Widget _member({
required String fullName,
required String position,
required String accessLevel,
}) {
return SizedBox(
height: 43.0.h(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_memberDetail(title: fullName),
// SizedBox(width: 40.0.w()),
_memberDetail(title: position),
// SizedBox(width: 40.0.w()),
_memberDetail(title: accessLevel),
],
),
);
}
Widget _memberDetail({required String title}) {
return SizedBox(
width: 100.0.w(),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey80,
),
),
),
);
}
}
class Member {
final String fullName;
final String position;
final String accessLevel;
Member({
required this.fullName,
required this.position,
required this.accessLevel,
});
}
final List<Member> _members = [
Member(fullName: 'Rick Novak', position: 'Director', accessLevel: 'Level 01'),
Member(fullName: 'Sara Novak', position: 'Director', accessLevel: 'Level 01'),
Member(fullName: 'Sarah Connor', position: 'Chief', accessLevel: 'Level 02'),
];
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/size_config.dart';
import '../../../core/theme/colors.dart';
class ProfileTabBar extends StatefulWidget {
const ProfileTabBar({
required this.controller,
required this.titles,
this.onTap,
super.key,
});
final TabController controller;
final List<String> titles;
final ValueChanged<int>? onTap;
@override
State<ProfileTabBar> createState() => _ProfileTabBarState();
}
class _ProfileTabBarState extends State<ProfileTabBar> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Container(
width: double.infinity,
height: 51.0.h(),
padding: EdgeInsets.symmetric(horizontal: 8.0.w(), vertical: 8.0.h()),
decoration: BoxDecoration(
color: AppColors.grey10,
borderRadius: BorderRadius.circular(10),
),
alignment: Alignment.center,
child: TabBar(
controller: widget.controller,
onTap: widget.onTap,
indicatorSize: TabBarIndicatorSize.tab,
dividerColor: Colors.transparent,
indicator: BoxDecoration(
color: AppColors.primary0,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.12),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
labelColor: AppColors.mainBlue,
unselectedLabelColor: AppColors.grey60,
labelStyle: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
),
tabs: [
Tab(text: widget.titles[0]),
Tab(text: widget.titles[1]),
Tab(text: widget.titles[2]),
],
),
),
);
}
}
+219
View File
@@ -0,0 +1,219 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import '../../../view_models/profile_view_model.dart';
import 'edit_key_word_bottom_sheet.dart';
class SubjectTab extends StatelessWidget {
const SubjectTab({super.key});
final bool isDesktop = true;
@override
Widget build(BuildContext context) {
final viewModel = context.read<ProfileViewModel>();
return Column(
children: [
SizedBox(height: 36.0.h()),
_companyActivity(companyActivity: 'IT'),
SizedBox(height: 24.0.h()),
_budget(budget: 'Telecom Solutions GmbH'),
SizedBox(height: 24.0.h()),
_keyWord(
onEditPressed: () {
if (isDesktop) {
showDialog(
context: context,
builder: (_) {
return Dialog(
child: SizedBox(
width: 560,
child: EditKeyWordBottomSheet(
isDesktop: true,
keyWords: viewModel.profileData?.keywords ?? [],
onSubmit: (keyWords) async {
context.pop();
await viewModel.updateProfileKeyWords(
keyWords: keyWords,
);
},
),
),
);
},
);
} else {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (_) {
return EditKeyWordBottomSheet(
isDesktop: false,
keyWords: viewModel.profileData?.keywords ?? [],
onSubmit: (keyWords) async {
context.pop();
await viewModel.updateProfileKeyWords(keyWords: keyWords);
},
);
},
);
}
},
),
SizedBox(height: 16.0.h()),
Wrap(
spacing: 8,
runSpacing: 16.0.h(),
// children: [
// _chip(label: 'UIUX', backgroundColor: Colors.green[200]!),
// _chip(label: 'Lorem ipsum', backgroundColor: Colors.orange[100]!),
// _chip(label: 'sample', backgroundColor: Colors.grey[300]!),
// _chip(label: 'Unknown', backgroundColor: Colors.red[200]!),
// _chip(label: 'Lorem ipsum 02', backgroundColor: Colors.blue[100]!),
// ],
children:
viewModel.profileData!.keywords!
.map(
(e) => _chip(label: e, backgroundColor: Colors.green[200]!),
)
.toList(),
),
SizedBox(height: 24.0.h()),
Divider(color: AppColors.grey20),
_location(onEditePressed: () {}),
],
);
}
Widget _companyActivity({required String companyActivity}) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0.w()),
child: SizedBox(
height: 43.0.h(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Company Activity ',
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
),
Text(
companyActivity,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey80,
),
),
],
),
),
);
}
Widget _budget({required String budget}) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0.w()),
child: SizedBox(
height: 43.0.h(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Budget',
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
),
Text(
budget,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey80,
),
),
],
),
),
);
}
Widget _keyWord({required VoidCallback onEditPressed}) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0.w()),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Key Word',
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
),
IconButton(
onPressed: onEditPressed,
icon: SvgPicture.asset(
AssetsManager.editting,
colorFilter: ColorFilter.mode(AppColors.grey70, BlendMode.srcIn),
),
),
],
),
);
}
Widget _chip({required String label, required Color backgroundColor}) {
return Chip(
label: Text(label),
backgroundColor: backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(color: Colors.transparent),
),
);
}
Widget _location({required VoidCallback onEditePressed}) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0.w()),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Location',
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
),
IconButton(
onPressed: onEditePressed,
icon: SvgPicture.asset(
AssetsManager.editting,
colorFilter: ColorFilter.mode(AppColors.grey70, BlendMode.srcIn),
),
),
],
),
);
}
}