Merge pull request 'merge_branch' (#25) from merge_branch into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/25
This commit is contained in:
a.ghabeli
2025-08-10 08:40:32 +03:30
43 changed files with 1978 additions and 838 deletions
+16
View File
@@ -2,7 +2,10 @@ import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';
import 'package:tm_app/core/theme/theme_provider.dart';
import 'package:tm_app/data/repositories/home_repository.dart';
import 'package:tm_app/data/repositories/tender_detail_repository.dart';
import 'package:tm_app/data/services/home_service.dart';
import 'package:tm_app/data/services/tender_detail_service.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart';
import '../../data/repositories/auth_repository.dart';
import '../../data/services/auth_service.dart';
@@ -25,6 +28,9 @@ List<SingleChildWidget> get apiClients {
return [
Provider(create: (context) => AuthService(networkManager: context.read())),
Provider(create: (context) => HomeService(networkManager: context.read())),
Provider(
create: (context) => TenderDetailService(networkManager: context.read()),
),
];
}
@@ -32,6 +38,11 @@ List<SingleChildWidget> get repositories {
return [
Provider(create: (context) => AuthRepository(authService: context.read())),
Provider(create: (context) => HomeRepository(homeService: context.read())),
Provider(
create:
(context) =>
TenderDetailRepository(tenderDetailService: context.read()),
),
];
}
@@ -43,6 +54,11 @@ List<SingleChildWidget> get viewModels {
ChangeNotifierProvider(
create: (context) => HomeViewModel(homeRepository: context.read()),
),
ChangeNotifierProvider(
create:
(context) =>
TenderDetailViewModel(tenderDetailRepository: context.read()),
),
];
}
+2 -2
View File
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:tm_app/views/detail/detail_screen.dart';
import 'package:tm_app/views/detail/pages/detail_screen.dart';
import 'package:tm_app/views/home/pages/home_screen.dart';
import 'package:tm_app/views/login/login_screen.dart';
import 'package:tm_app/views/login/pages/login_screen.dart';
import '../../views/profile/profile_screen.dart';
import '../../views/tenders/tenders_screen.dart';
+1
View File
@@ -121,6 +121,7 @@ class AppColors {
// Red variations (status colors)
static const Color red0 = Color(0xFFF8D6D6);
static const Color red10 = Color(0xFFC02525);
static const Color red = Color(0xFFFF3B30);
// Other colors
static const Color paleOrange = Color(0xFFfff7ee);
@@ -0,0 +1,18 @@
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/services/model/tender_detail_request/tender_detail_request.dart';
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
import 'package:tm_app/data/services/tender_detail_service.dart';
class TenderDetailRepository {
TenderDetailRepository({required TenderDetailService tenderDetailService})
: _tenderDetailService = tenderDetailService;
final TenderDetailService _tenderDetailService;
Future<Result<TenderDetailResponseModel>> getTenderDetail({
required TenderDetailRequest request,
}) async {
return await _tenderDetailService.getTenderDetail(request: request);
}
}
@@ -0,0 +1,24 @@
const tenderDetailMockData = '''
{
"data": {
"date": "2025-05-21",
"status": "Open",
"title": "Operation, support and further development of open e-platform Operation, support and further development of open e-platform",
"id": "JNDFKMDV-100JF",
"approvalDate": "2025-05-21",
"submissionDate": "2025-05-21",
"client": "Procurement Notice Procurement NoticeProcurement Notice",
"deliveryLocation": "Norrbotten County",
"referenceNumber": "KLF 2025/120",
"country": "UK",
"flagAsset": "assets/icons/SE.png",
"locationTitle": "Lorem ipsum is amet",
"locationDescription": "Luleå Municipality is procuring on behalf of Norrbottens e-board (all municipalities in Norrbotten). The procurement includes operation, support, and further development of the existing open e-platform. No additional information, amendments, or answers to questions will be provided after June 16.",
"documentName": "PDF, Document",
"documentUrl": "https://www.tenders.com",
"profileMatch": 0.75,
"incompleteResume": true,
"incompleteResumeReason": "No experience in e-platform development"
}
}
''';
@@ -0,0 +1,16 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
part 'tender_detail_request.freezed.dart';
part 'tender_detail_request.g.dart';
@freezed
class TenderDetailRequest with _$TenderDetailRequest {
const factory TenderDetailRequest({
@JsonKey(name: 'id') required String id,
}) = _TenderDetailRequest;
factory TenderDetailRequest.fromJson(Map<String, dynamic> json) =>
_$TenderDetailRequestFromJson(json);
}
@@ -0,0 +1,177 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'tender_detail_request.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
TenderDetailRequest _$TenderDetailRequestFromJson(Map<String, dynamic> json) {
return _TenderDetailRequest.fromJson(json);
}
/// @nodoc
mixin _$TenderDetailRequest {
@JsonKey(name: "id")
String get id => throw _privateConstructorUsedError;
/// Serializes this TenderDetailRequest to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of TenderDetailRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$TenderDetailRequestCopyWith<TenderDetailRequest> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TenderDetailRequestCopyWith<$Res> {
factory $TenderDetailRequestCopyWith(
TenderDetailRequest value,
$Res Function(TenderDetailRequest) then,
) = _$TenderDetailRequestCopyWithImpl<$Res, TenderDetailRequest>;
@useResult
$Res call({@JsonKey(name: "id") String id});
}
/// @nodoc
class _$TenderDetailRequestCopyWithImpl<$Res, $Val extends TenderDetailRequest>
implements $TenderDetailRequestCopyWith<$Res> {
_$TenderDetailRequestCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of TenderDetailRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? id = null}) {
return _then(
_value.copyWith(
id:
null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$TenderDetailRequestImplCopyWith<$Res>
implements $TenderDetailRequestCopyWith<$Res> {
factory _$$TenderDetailRequestImplCopyWith(
_$TenderDetailRequestImpl value,
$Res Function(_$TenderDetailRequestImpl) then,
) = __$$TenderDetailRequestImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({@JsonKey(name: "id") String id});
}
/// @nodoc
class __$$TenderDetailRequestImplCopyWithImpl<$Res>
extends _$TenderDetailRequestCopyWithImpl<$Res, _$TenderDetailRequestImpl>
implements _$$TenderDetailRequestImplCopyWith<$Res> {
__$$TenderDetailRequestImplCopyWithImpl(
_$TenderDetailRequestImpl _value,
$Res Function(_$TenderDetailRequestImpl) _then,
) : super(_value, _then);
/// Create a copy of TenderDetailRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? id = null}) {
return _then(
_$TenderDetailRequestImpl(
id:
null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$TenderDetailRequestImpl implements _TenderDetailRequest {
const _$TenderDetailRequestImpl({@JsonKey(name: "id") required this.id});
factory _$TenderDetailRequestImpl.fromJson(Map<String, dynamic> json) =>
_$$TenderDetailRequestImplFromJson(json);
@override
@JsonKey(name: "id")
final String id;
@override
String toString() {
return 'TenderDetailRequest(id: $id)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TenderDetailRequestImpl &&
(identical(other.id, id) || other.id == id));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id);
/// Create a copy of TenderDetailRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$TenderDetailRequestImplCopyWith<_$TenderDetailRequestImpl> get copyWith =>
__$$TenderDetailRequestImplCopyWithImpl<_$TenderDetailRequestImpl>(
this,
_$identity,
);
@override
Map<String, dynamic> toJson() {
return _$$TenderDetailRequestImplToJson(this);
}
}
abstract class _TenderDetailRequest implements TenderDetailRequest {
const factory _TenderDetailRequest({
@JsonKey(name: "id") required final String id,
}) = _$TenderDetailRequestImpl;
factory _TenderDetailRequest.fromJson(Map<String, dynamic> json) =
_$TenderDetailRequestImpl.fromJson;
@override
@JsonKey(name: "id")
String get id;
/// Create a copy of TenderDetailRequest
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TenderDetailRequestImplCopyWith<_$TenderDetailRequestImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -0,0 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tender_detail_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$TenderDetailRequestImpl _$$TenderDetailRequestImplFromJson(
Map<String, dynamic> json,
) => _$TenderDetailRequestImpl(id: json['id'] as String);
Map<String, dynamic> _$$TenderDetailRequestImplToJson(
_$TenderDetailRequestImpl instance,
) => <String, dynamic>{'id': instance.id};
@@ -0,0 +1,34 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
part 'tender_detail_response_model.freezed.dart';
part 'tender_detail_response_model.g.dart';
@freezed
class TenderDetailResponseModel with _$TenderDetailResponseModel {
@JsonSerializable(explicitToJson: true)
const factory TenderDetailResponseModel({
required String? date,
required String? status,
required String? title,
required String? id,
required String? approvalDate,
required String? submissionDate,
required String? client,
required String? deliveryLocation,
required String? referenceNumber,
required String? country,
required String? flagAsset,
required String? locationTitle,
required String? locationDescription,
required String? documentName,
required String? documentUrl,
required double? profileMatch,
required bool? incompleteResume,
required String? incompleteResumeReason,
}) = _TenderDetailResponseModel;
factory TenderDetailResponseModel.fromJson(Map<String, dynamic> json) =>
_$TenderDetailResponseModelFromJson(json);
}
@@ -0,0 +1,599 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'tender_detail_response_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
TenderDetailResponseModel _$TenderDetailResponseModelFromJson(
Map<String, dynamic> json,
) {
return _TenderDetailResponseModel.fromJson(json);
}
/// @nodoc
mixin _$TenderDetailResponseModel {
String? get date => throw _privateConstructorUsedError;
String? get status => throw _privateConstructorUsedError;
String? get title => throw _privateConstructorUsedError;
String? get id => throw _privateConstructorUsedError;
String? get approvalDate => throw _privateConstructorUsedError;
String? get submissionDate => throw _privateConstructorUsedError;
String? get client => throw _privateConstructorUsedError;
String? get deliveryLocation => throw _privateConstructorUsedError;
String? get referenceNumber => throw _privateConstructorUsedError;
String? get country => throw _privateConstructorUsedError;
String? get flagAsset => throw _privateConstructorUsedError;
String? get locationTitle => throw _privateConstructorUsedError;
String? get locationDescription => throw _privateConstructorUsedError;
String? get documentName => throw _privateConstructorUsedError;
String? get documentUrl => throw _privateConstructorUsedError;
double? get profileMatch => throw _privateConstructorUsedError;
bool? get incompleteResume => throw _privateConstructorUsedError;
String? get incompleteResumeReason => throw _privateConstructorUsedError;
/// Serializes this TenderDetailResponseModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of TenderDetailResponseModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$TenderDetailResponseModelCopyWith<TenderDetailResponseModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TenderDetailResponseModelCopyWith<$Res> {
factory $TenderDetailResponseModelCopyWith(
TenderDetailResponseModel value,
$Res Function(TenderDetailResponseModel) then,
) = _$TenderDetailResponseModelCopyWithImpl<$Res, TenderDetailResponseModel>;
@useResult
$Res call({
String? date,
String? status,
String? title,
String? id,
String? approvalDate,
String? submissionDate,
String? client,
String? deliveryLocation,
String? referenceNumber,
String? country,
String? flagAsset,
String? locationTitle,
String? locationDescription,
String? documentName,
String? documentUrl,
double? profileMatch,
bool? incompleteResume,
String? incompleteResumeReason,
});
}
/// @nodoc
class _$TenderDetailResponseModelCopyWithImpl<
$Res,
$Val extends TenderDetailResponseModel
>
implements $TenderDetailResponseModelCopyWith<$Res> {
_$TenderDetailResponseModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of TenderDetailResponseModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? date = freezed,
Object? status = freezed,
Object? title = freezed,
Object? id = freezed,
Object? approvalDate = freezed,
Object? submissionDate = freezed,
Object? client = freezed,
Object? deliveryLocation = freezed,
Object? referenceNumber = freezed,
Object? country = freezed,
Object? flagAsset = freezed,
Object? locationTitle = freezed,
Object? locationDescription = freezed,
Object? documentName = freezed,
Object? documentUrl = freezed,
Object? profileMatch = freezed,
Object? incompleteResume = freezed,
Object? incompleteResumeReason = freezed,
}) {
return _then(
_value.copyWith(
date:
freezed == date
? _value.date
: date // ignore: cast_nullable_to_non_nullable
as String?,
status:
freezed == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String?,
title:
freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
id:
freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
approvalDate:
freezed == approvalDate
? _value.approvalDate
: approvalDate // ignore: cast_nullable_to_non_nullable
as String?,
submissionDate:
freezed == submissionDate
? _value.submissionDate
: submissionDate // ignore: cast_nullable_to_non_nullable
as String?,
client:
freezed == client
? _value.client
: client // ignore: cast_nullable_to_non_nullable
as String?,
deliveryLocation:
freezed == deliveryLocation
? _value.deliveryLocation
: deliveryLocation // ignore: cast_nullable_to_non_nullable
as String?,
referenceNumber:
freezed == referenceNumber
? _value.referenceNumber
: referenceNumber // ignore: cast_nullable_to_non_nullable
as String?,
country:
freezed == country
? _value.country
: country // ignore: cast_nullable_to_non_nullable
as String?,
flagAsset:
freezed == flagAsset
? _value.flagAsset
: flagAsset // ignore: cast_nullable_to_non_nullable
as String?,
locationTitle:
freezed == locationTitle
? _value.locationTitle
: locationTitle // ignore: cast_nullable_to_non_nullable
as String?,
locationDescription:
freezed == locationDescription
? _value.locationDescription
: locationDescription // ignore: cast_nullable_to_non_nullable
as String?,
documentName:
freezed == documentName
? _value.documentName
: documentName // ignore: cast_nullable_to_non_nullable
as String?,
documentUrl:
freezed == documentUrl
? _value.documentUrl
: documentUrl // ignore: cast_nullable_to_non_nullable
as String?,
profileMatch:
freezed == profileMatch
? _value.profileMatch
: profileMatch // ignore: cast_nullable_to_non_nullable
as double?,
incompleteResume:
freezed == incompleteResume
? _value.incompleteResume
: incompleteResume // ignore: cast_nullable_to_non_nullable
as bool?,
incompleteResumeReason:
freezed == incompleteResumeReason
? _value.incompleteResumeReason
: incompleteResumeReason // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$TenderDetailResponseModelImplCopyWith<$Res>
implements $TenderDetailResponseModelCopyWith<$Res> {
factory _$$TenderDetailResponseModelImplCopyWith(
_$TenderDetailResponseModelImpl value,
$Res Function(_$TenderDetailResponseModelImpl) then,
) = __$$TenderDetailResponseModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String? date,
String? status,
String? title,
String? id,
String? approvalDate,
String? submissionDate,
String? client,
String? deliveryLocation,
String? referenceNumber,
String? country,
String? flagAsset,
String? locationTitle,
String? locationDescription,
String? documentName,
String? documentUrl,
double? profileMatch,
bool? incompleteResume,
String? incompleteResumeReason,
});
}
/// @nodoc
class __$$TenderDetailResponseModelImplCopyWithImpl<$Res>
extends
_$TenderDetailResponseModelCopyWithImpl<
$Res,
_$TenderDetailResponseModelImpl
>
implements _$$TenderDetailResponseModelImplCopyWith<$Res> {
__$$TenderDetailResponseModelImplCopyWithImpl(
_$TenderDetailResponseModelImpl _value,
$Res Function(_$TenderDetailResponseModelImpl) _then,
) : super(_value, _then);
/// Create a copy of TenderDetailResponseModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? date = freezed,
Object? status = freezed,
Object? title = freezed,
Object? id = freezed,
Object? approvalDate = freezed,
Object? submissionDate = freezed,
Object? client = freezed,
Object? deliveryLocation = freezed,
Object? referenceNumber = freezed,
Object? country = freezed,
Object? flagAsset = freezed,
Object? locationTitle = freezed,
Object? locationDescription = freezed,
Object? documentName = freezed,
Object? documentUrl = freezed,
Object? profileMatch = freezed,
Object? incompleteResume = freezed,
Object? incompleteResumeReason = freezed,
}) {
return _then(
_$TenderDetailResponseModelImpl(
date:
freezed == date
? _value.date
: date // ignore: cast_nullable_to_non_nullable
as String?,
status:
freezed == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String?,
title:
freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
id:
freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
approvalDate:
freezed == approvalDate
? _value.approvalDate
: approvalDate // ignore: cast_nullable_to_non_nullable
as String?,
submissionDate:
freezed == submissionDate
? _value.submissionDate
: submissionDate // ignore: cast_nullable_to_non_nullable
as String?,
client:
freezed == client
? _value.client
: client // ignore: cast_nullable_to_non_nullable
as String?,
deliveryLocation:
freezed == deliveryLocation
? _value.deliveryLocation
: deliveryLocation // ignore: cast_nullable_to_non_nullable
as String?,
referenceNumber:
freezed == referenceNumber
? _value.referenceNumber
: referenceNumber // ignore: cast_nullable_to_non_nullable
as String?,
country:
freezed == country
? _value.country
: country // ignore: cast_nullable_to_non_nullable
as String?,
flagAsset:
freezed == flagAsset
? _value.flagAsset
: flagAsset // ignore: cast_nullable_to_non_nullable
as String?,
locationTitle:
freezed == locationTitle
? _value.locationTitle
: locationTitle // ignore: cast_nullable_to_non_nullable
as String?,
locationDescription:
freezed == locationDescription
? _value.locationDescription
: locationDescription // ignore: cast_nullable_to_non_nullable
as String?,
documentName:
freezed == documentName
? _value.documentName
: documentName // ignore: cast_nullable_to_non_nullable
as String?,
documentUrl:
freezed == documentUrl
? _value.documentUrl
: documentUrl // ignore: cast_nullable_to_non_nullable
as String?,
profileMatch:
freezed == profileMatch
? _value.profileMatch
: profileMatch // ignore: cast_nullable_to_non_nullable
as double?,
incompleteResume:
freezed == incompleteResume
? _value.incompleteResume
: incompleteResume // ignore: cast_nullable_to_non_nullable
as bool?,
incompleteResumeReason:
freezed == incompleteResumeReason
? _value.incompleteResumeReason
: incompleteResumeReason // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
@JsonSerializable(explicitToJson: true)
class _$TenderDetailResponseModelImpl implements _TenderDetailResponseModel {
const _$TenderDetailResponseModelImpl({
required this.date,
required this.status,
required this.title,
required this.id,
required this.approvalDate,
required this.submissionDate,
required this.client,
required this.deliveryLocation,
required this.referenceNumber,
required this.country,
required this.flagAsset,
required this.locationTitle,
required this.locationDescription,
required this.documentName,
required this.documentUrl,
required this.profileMatch,
required this.incompleteResume,
required this.incompleteResumeReason,
});
factory _$TenderDetailResponseModelImpl.fromJson(Map<String, dynamic> json) =>
_$$TenderDetailResponseModelImplFromJson(json);
@override
final String? date;
@override
final String? status;
@override
final String? title;
@override
final String? id;
@override
final String? approvalDate;
@override
final String? submissionDate;
@override
final String? client;
@override
final String? deliveryLocation;
@override
final String? referenceNumber;
@override
final String? country;
@override
final String? flagAsset;
@override
final String? locationTitle;
@override
final String? locationDescription;
@override
final String? documentName;
@override
final String? documentUrl;
@override
final double? profileMatch;
@override
final bool? incompleteResume;
@override
final String? incompleteResumeReason;
@override
String toString() {
return 'TenderDetailResponseModel(date: $date, status: $status, title: $title, id: $id, approvalDate: $approvalDate, submissionDate: $submissionDate, client: $client, deliveryLocation: $deliveryLocation, referenceNumber: $referenceNumber, country: $country, flagAsset: $flagAsset, locationTitle: $locationTitle, locationDescription: $locationDescription, documentName: $documentName, documentUrl: $documentUrl, profileMatch: $profileMatch, incompleteResume: $incompleteResume, incompleteResumeReason: $incompleteResumeReason)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TenderDetailResponseModelImpl &&
(identical(other.date, date) || other.date == date) &&
(identical(other.status, status) || other.status == status) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.id, id) || other.id == id) &&
(identical(other.approvalDate, approvalDate) ||
other.approvalDate == approvalDate) &&
(identical(other.submissionDate, submissionDate) ||
other.submissionDate == submissionDate) &&
(identical(other.client, client) || other.client == client) &&
(identical(other.deliveryLocation, deliveryLocation) ||
other.deliveryLocation == deliveryLocation) &&
(identical(other.referenceNumber, referenceNumber) ||
other.referenceNumber == referenceNumber) &&
(identical(other.country, country) || other.country == country) &&
(identical(other.flagAsset, flagAsset) ||
other.flagAsset == flagAsset) &&
(identical(other.locationTitle, locationTitle) ||
other.locationTitle == locationTitle) &&
(identical(other.locationDescription, locationDescription) ||
other.locationDescription == locationDescription) &&
(identical(other.documentName, documentName) ||
other.documentName == documentName) &&
(identical(other.documentUrl, documentUrl) ||
other.documentUrl == documentUrl) &&
(identical(other.profileMatch, profileMatch) ||
other.profileMatch == profileMatch) &&
(identical(other.incompleteResume, incompleteResume) ||
other.incompleteResume == incompleteResume) &&
(identical(other.incompleteResumeReason, incompleteResumeReason) ||
other.incompleteResumeReason == incompleteResumeReason));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
date,
status,
title,
id,
approvalDate,
submissionDate,
client,
deliveryLocation,
referenceNumber,
country,
flagAsset,
locationTitle,
locationDescription,
documentName,
documentUrl,
profileMatch,
incompleteResume,
incompleteResumeReason,
);
/// Create a copy of TenderDetailResponseModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$TenderDetailResponseModelImplCopyWith<_$TenderDetailResponseModelImpl>
get copyWith => __$$TenderDetailResponseModelImplCopyWithImpl<
_$TenderDetailResponseModelImpl
>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$TenderDetailResponseModelImplToJson(this);
}
}
abstract class _TenderDetailResponseModel implements TenderDetailResponseModel {
const factory _TenderDetailResponseModel({
required final String? date,
required final String? status,
required final String? title,
required final String? id,
required final String? approvalDate,
required final String? submissionDate,
required final String? client,
required final String? deliveryLocation,
required final String? referenceNumber,
required final String? country,
required final String? flagAsset,
required final String? locationTitle,
required final String? locationDescription,
required final String? documentName,
required final String? documentUrl,
required final double? profileMatch,
required final bool? incompleteResume,
required final String? incompleteResumeReason,
}) = _$TenderDetailResponseModelImpl;
factory _TenderDetailResponseModel.fromJson(Map<String, dynamic> json) =
_$TenderDetailResponseModelImpl.fromJson;
@override
String? get date;
@override
String? get status;
@override
String? get title;
@override
String? get id;
@override
String? get approvalDate;
@override
String? get submissionDate;
@override
String? get client;
@override
String? get deliveryLocation;
@override
String? get referenceNumber;
@override
String? get country;
@override
String? get flagAsset;
@override
String? get locationTitle;
@override
String? get locationDescription;
@override
String? get documentName;
@override
String? get documentUrl;
@override
double? get profileMatch;
@override
bool? get incompleteResume;
@override
String? get incompleteResumeReason;
/// Create a copy of TenderDetailResponseModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TenderDetailResponseModelImplCopyWith<_$TenderDetailResponseModelImpl>
get copyWith => throw _privateConstructorUsedError;
}
@@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tender_detail_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$TenderDetailResponseModelImpl _$$TenderDetailResponseModelImplFromJson(
Map<String, dynamic> json,
) => _$TenderDetailResponseModelImpl(
date: json['date'] as String?,
status: json['status'] as String?,
title: json['title'] as String?,
id: json['id'] as String?,
approvalDate: json['approvalDate'] as String?,
submissionDate: json['submissionDate'] as String?,
client: json['client'] as String?,
deliveryLocation: json['deliveryLocation'] as String?,
referenceNumber: json['referenceNumber'] as String?,
country: json['country'] as String?,
flagAsset: json['flagAsset'] as String?,
locationTitle: json['locationTitle'] as String?,
locationDescription: json['locationDescription'] as String?,
documentName: json['documentName'] as String?,
documentUrl: json['documentUrl'] as String?,
profileMatch: (json['profileMatch'] as num?)?.toDouble(),
incompleteResume: json['incompleteResume'] as bool?,
incompleteResumeReason: json['incompleteResumeReason'] as String?,
);
Map<String, dynamic> _$$TenderDetailResponseModelImplToJson(
_$TenderDetailResponseModelImpl instance,
) => <String, dynamic>{
'date': instance.date,
'status': instance.status,
'title': instance.title,
'id': instance.id,
'approvalDate': instance.approvalDate,
'submissionDate': instance.submissionDate,
'client': instance.client,
'deliveryLocation': instance.deliveryLocation,
'referenceNumber': instance.referenceNumber,
'country': instance.country,
'flagAsset': instance.flagAsset,
'locationTitle': instance.locationTitle,
'locationDescription': instance.locationDescription,
'documentName': instance.documentName,
'documentUrl': instance.documentUrl,
'profileMatch': instance.profileMatch,
'incompleteResume': instance.incompleteResume,
'incompleteResumeReason': instance.incompleteResumeReason,
};
@@ -0,0 +1,36 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:tm_app/core/utils/logger.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/services/mock_data/tender_detail_mock.dart';
import 'package:tm_app/data/services/model/tender_detail_request/tender_detail_request.dart';
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
import 'package:tm_app/data/services/network_manager.dart';
class TenderDetailService {
TenderDetailService({required NetworkManager networkManager})
: _networkManager = networkManager;
final NetworkManager _networkManager;
Future<Result<TenderDetailResponseModel>> getTenderDetail({
required TenderDetailRequest request,
}) async {
try {
await Future.delayed(const Duration(seconds: 2));
appLogger.info('get tender detail success');
return Result.ok(
TenderDetailResponseModel.fromJson(
jsonDecode(tenderDetailMockData)['data'],
),
);
} catch (e, stackTrace) {
appLogger.error('get tender detail failed: $e', stackTrace: stackTrace);
return Result.error(Exception(e));
}
}
}
+41 -6
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import '../core/utils/result.dart';
import '../data/models/user_model.dart';
import '../data/repositories/auth_repository.dart';
@@ -8,8 +7,25 @@ class AuthViewModel with ChangeNotifier {
final AuthRepository _authRepository;
AuthViewModel({required AuthRepository authRepository})
: _authRepository = authRepository;
: _authRepository = authRepository {
usernameController.addListener(_onTextChanged);
passwordController.addListener(_onTextChanged);
usernameFocus.addListener(_onTextChanged);
passwordFocus.addListener(_onTextChanged);
}
final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final FocusNode usernameFocus = FocusNode();
final FocusNode passwordFocus = FocusNode();
bool _obscurePassword = true;
bool get obscurePassword => _obscurePassword;
bool get isLoginEnabled =>
usernameController.text.isNotEmpty &&
passwordController.text.isNotEmpty;
// Auth state
bool _isLoading = false;
String? _errorMessage;
UserModel? _loggedInUser;
@@ -18,25 +34,44 @@ class AuthViewModel with ChangeNotifier {
String? get errorMessage => _errorMessage;
UserModel? get loggedInUser => _loggedInUser;
Future<void> login(String email, String password) async {
void togglePasswordVisibility() {
_obscurePassword = !_obscurePassword;
notifyListeners();
}
void _onTextChanged() {
notifyListeners();
}
Future<void> login() async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
final result = await _authRepository.login(email, password);
final result = await _authRepository.login(
usernameController.text.trim(),
passwordController.text.trim(),
);
switch (result) {
case Ok<UserModel>():
_loggedInUser = result.value;
// موفقیت‌آمیز
break;
case Error<UserModel>():
_errorMessage = result.error.toString();
// خطا
break;
}
_isLoading = false;
notifyListeners();
}
@override
void dispose() {
usernameController.dispose();
passwordController.dispose();
usernameFocus.dispose();
passwordFocus.dispose();
super.dispose();
}
}
-42
View File
@@ -1,42 +0,0 @@
// lib/view_models/login_view_model.dart
import 'package:flutter/material.dart';
class LoginViewModel with ChangeNotifier {
final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final FocusNode usernameFocus = FocusNode();
final FocusNode passwordFocus = FocusNode();
bool _obscurePassword = true;
LoginViewModel() {
usernameController.addListener(_onTextChanged);
passwordController.addListener(_onTextChanged);
usernameFocus.addListener(_onTextChanged);
passwordFocus.addListener(_onTextChanged);
}
bool get obscurePassword => _obscurePassword;
bool get isLoginEnabled =>
usernameController.text.isNotEmpty &&
passwordController.text.isNotEmpty;
void togglePasswordVisibility() {
_obscurePassword = !_obscurePassword;
notifyListeners();
}
void _onTextChanged() {
notifyListeners();
}
@override
void dispose() {
usernameController.dispose();
passwordController.dispose();
usernameFocus.dispose();
passwordFocus.dispose();
super.dispose();
}
}
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/repositories/tender_detail_repository.dart';
import 'package:tm_app/data/services/model/tender_detail_request/tender_detail_request.dart';
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
class TenderDetailViewModel with ChangeNotifier {
final TenderDetailRepository _tenderDetailRepository;
TenderDetailViewModel({required TenderDetailRepository tenderDetailRepository})
: _tenderDetailRepository = tenderDetailRepository;
bool _isLoading = false;
String? _errorMessage;
TenderDetailResponseModel? _tenderDetail;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
TenderDetailResponseModel? get tenderDetail => _tenderDetail;
Future<void> getTenderDetail({required String id}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
final request = TenderDetailRequest(id: id);
final result = await _tenderDetailRepository.getTenderDetail(request: request);
switch (result) {
case Ok<TenderDetailResponseModel>():
_tenderDetail = result.value;
break;
case Error<TenderDetailResponseModel>():
_errorMessage = result.error.toString();
break;
}
_isLoading = false;
notifyListeners();
}
}
@@ -1,67 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailDesktopScreen extends StatelessWidget {
const TenderDetailDesktopScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(context),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 740,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isBig: true,),
SizedBox(height: 24.0.h()),
const TenderDetailCard(),
SizedBox(height: 24.0.h()),
TenderDetailActions(),
],
),
),
),
),
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: Colors.white,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
}
@@ -1,62 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailMobileScreen extends StatelessWidget {
const TenderDetailMobileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(context),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isBig: false),
SizedBox(height: 24.0.h()),
const TenderDetailCard(),
SizedBox(height: 24.0.h()),
TenderDetailActions(),
],
),
),
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: Colors.white,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
}
-24
View File
@@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/display_size_detector.dart';
import 'package:tm_app/views/detail/detail_desktop_screen.dart';
import 'package:tm_app/views/detail/detail_mobile_screen.dart';
import 'package:tm_app/views/detail/detail_tablet_screen.dart';
class TenderDetailScreen extends StatelessWidget {
const TenderDetailScreen({super.key});
@override
Widget build(BuildContext context) {
final displaySize = MediaQuery.of(context).size.displaySize;
switch (displaySize) {
case DisplaySize.large:
case DisplaySize.medium:
return const TenderDetailDesktopScreen();
case DisplaySize.small:
return const TenderDetailTabletScreen();
case DisplaySize.extraSmall:
return const TenderDetailMobileScreen();
}
}
}
@@ -1,67 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailTabletScreen extends StatelessWidget {
const TenderDetailTabletScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(context),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 768,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isBig: true,),
SizedBox(height: 24.0.h()),
const TenderDetailCard(),
SizedBox(height: 24.0.h()),
TenderDetailActions(),
],
),
),
),
),
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: Colors.white,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
}
@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailDesktopPage extends StatefulWidget {
final String tenderId;
const TenderDetailDesktopPage({required this.tenderId, super.key});
@override
State<TenderDetailDesktopPage> createState() =>
_TenderDetailDesktopPageState();
}
class _TenderDetailDesktopPageState extends State<TenderDetailDesktopPage> {
@override
void initState() {
super.initState();
Future.microtask(() {
context.read<TenderDetailViewModel>().getTenderDetail(
id: widget.tenderId,
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.backgroundColor,
appBar: _buildAppBar(context),
body: Consumer<TenderDetailViewModel>(
builder: (context, tenderViewModel, child) {
if (tenderViewModel.isLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.secondary50),
);
}
if (tenderViewModel.errorMessage != null) {
return Center(child: Text(tenderViewModel.errorMessage!));
}
final detail = tenderViewModel.tenderDetail;
if (detail == null) {
return const Center(child: Text('No tender details available'));
}
return SingleChildScrollView(
child: Center(
child: SizedBox(
width: 740,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isScreenBig: true, detail: detail),
SizedBox(height: 32.0.h()),
TenderDetailActions(isScreenBig: true),
],
),
),
),
),
);
},
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: AppColors.backgroundColor,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
}
@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailMobilePage extends StatefulWidget {
final String tenderId;
const TenderDetailMobilePage({super.key, required this.tenderId});
@override
State<TenderDetailMobilePage> createState() => _TenderDetailMobilePageState();
}
class _TenderDetailMobilePageState extends State<TenderDetailMobilePage> {
@override
void initState() {
super.initState();
Future.microtask(() {
context.read<TenderDetailViewModel>().getTenderDetail(id: widget.tenderId);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.backgroundColor,
appBar: _buildAppBar(context),
body: Consumer<TenderDetailViewModel>(
builder: (context, tenderViewModel, child) {
if (tenderViewModel.isLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.secondary50),
);
}
if (tenderViewModel.errorMessage != null) {
return Center(child: Text(tenderViewModel.errorMessage!));
}
final detail = tenderViewModel.tenderDetail;
if (detail == null) {
return const Center(child: Text("No tender details available"));
}
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isScreenBig: false, detail: detail),
SizedBox(height: 24.0.h()),
TenderDetailActions(isScreenBig: false),
],
),
),
);
},
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: AppColors.backgroundColor,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/detail/pages/detail_desktop_page.dart';
import 'package:tm_app/views/detail/pages/detail_mobile_page.dart';
import 'package:tm_app/views/detail/pages/detail_tablet_page.dart';
import 'package:tm_app/views/shared/responsive_builder.dart';
class TenderDetailScreen extends StatelessWidget {
const TenderDetailScreen({super.key});
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return ResponsiveBuilder(
mobile: TenderDetailMobilePage(tenderId: 'JNDFKMDV-100JF',),
tablet: TenderDetailTabletPage(tenderId: 'JNDFKMDV-100JF',),
desktop: TenderDetailDesktopPage(tenderId: 'JNDFKMDV-100JF',),
);
}
}
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailTabletPage extends StatefulWidget {
final String tenderId;
const TenderDetailTabletPage({required this.tenderId, super.key});
@override
State<TenderDetailTabletPage> createState() => _TenderDetailTabletPageState();
}
class _TenderDetailTabletPageState extends State<TenderDetailTabletPage> {
@override
void initState() {
super.initState();
Future.microtask(() {
context.read<TenderDetailViewModel>().getTenderDetail(
id: widget.tenderId,
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.backgroundColor,
appBar: _buildAppBar(context),
body: Consumer<TenderDetailViewModel>(
builder: (context, tenderViewModel, child) {
if (tenderViewModel.isLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.secondary50),
);
}
if (tenderViewModel.errorMessage != null) {
return Center(child: Text(tenderViewModel.errorMessage!));
}
final detail = tenderViewModel.tenderDetail;
if (detail == null) {
return const Center(child: Text('No tender details available'));
}
return SingleChildScrollView(
child: Center(
child: SizedBox(
width: 768,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isScreenBig: true, detail: detail),
SizedBox(height: 28.0.h()),
TenderDetailActions(isScreenBig: true),
],
),
),
),
),
);
},
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: AppColors.backgroundColor,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
}
+143 -135
View File
@@ -8,156 +8,164 @@ class DeadlineItem extends StatelessWidget {
final String approvalDate;
final String submissionText;
final String submissionDate;
final bool isBig;
final bool isScreenBig;
const DeadlineItem({
required this.title, required this.approvalText, required this.approvalDate, required this.submissionText, required this.submissionDate, required this.isBig, super.key,
required this.title,
required this.approvalText,
required this.approvalDate,
required this.submissionText,
required this.submissionDate,
required this.isScreenBig,
super.key,
});
@override
Widget build(BuildContext context) {
return isBig ? Padding(
padding: EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
title,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
),
IntrinsicHeight(
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
approvalText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
SizedBox(height: 4.0.h()),
Text(
approvalDate,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
return isScreenBig
? Padding(
padding: EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
title,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
VerticalDivider(
color: AppColors.grey30,
thickness: 1,
width: 24,
),
IntrinsicHeight(
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
approvalText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
SizedBox(height: 4.0.h()),
Text(
approvalDate,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
),
VerticalDivider(
color: AppColors.grey30,
thickness: 1,
width: 24,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
submissionText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
SizedBox(height: 4.0.h()),
Text(
submissionDate,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
),
],
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
submissionText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
SizedBox(height: 4.0.h()),
Text(
submissionDate,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
SizedBox(height: 8.0.h()),
const Divider(),
],
),
)
: Padding(
padding: EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
title,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
],
),
),
SizedBox(height: 8.0.h()),
const Divider(),
],
),
) : Padding(
padding: EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
title,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
approvalText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
approvalText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
Text(
approvalDate,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
Text(
approvalDate,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
SizedBox(height: 4.0.h()),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
submissionText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
Text(
submissionDate,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
SizedBox(height: 8.0.h()),
const Divider(),
],
),
SizedBox(height: 4.0.h()),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
submissionText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
Text(
submissionDate,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
SizedBox(height: 8.0.h()),
const Divider(),
],
),
);
);
}
}
@@ -5,31 +5,56 @@ import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/login/widgets/login_button.dart';
class TenderDetailActions extends StatelessWidget {
const TenderDetailActions({super.key});
final bool isScreenBig;
const TenderDetailActions({required this.isScreenBig, super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
BaseButton(
isEnabled: true,
text: AppStrings.tenderSubmitButton,
backgroundColor: AppColors.lightBlue,
textColor: AppColors.mainBlue,
onPressed: () {},
),
SizedBox(height: 16.0.h()),
BaseButton(
isEnabled: true,
text: AppStrings.tenderRejectButton,
borderColor: AppColors.red10,
textColor: AppColors.red10,
backgroundColor: Colors.white,
borderWidth: 1,
onPressed: () {},
),
SizedBox(height: 16.0.h()),
],
);
return isScreenBig
? Row(
children: [
Spacer(),
BaseButton(
isEnabled: true,
text: AppStrings.tenderRejectButton,
textColor: AppColors.red10,
backgroundColor: AppColors.backgroundColor,
borderWidth: 1,
onPressed: () {},
width: 120.0.w(),
),
SizedBox(width: 5.0.w()),
BaseButton(
isEnabled: true,
text: AppStrings.tenderSubmitButton,
backgroundColor: AppColors.lightBlue,
textColor: AppColors.mainBlue,
onPressed: () {},
width: 120.0.w(),
),
],
)
: Column(
children: [
BaseButton(
isEnabled: true,
text: AppStrings.tenderSubmitButton,
backgroundColor: AppColors.lightBlue,
textColor: AppColors.mainBlue,
onPressed: () {},
),
SizedBox(height: 16.0.h()),
BaseButton(
isEnabled: true,
text: AppStrings.tenderRejectButton,
borderColor: AppColors.red10,
textColor: AppColors.red10,
backgroundColor: AppColors.backgroundColor,
borderWidth: 1,
onPressed: () {},
),
SizedBox(height: 16.0.h()),
],
);
}
}
@@ -20,11 +20,11 @@ class TenderDetailCard extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
Text(
AppStrings.tenderMatchProfile,
style: TextStyle(
fontSize: 12.0,
color: Colors.black87,
fontSize: 12.0.sp(),
color: AppColors.grey80,
fontWeight: FontWeight.w400,
),
),
@@ -46,7 +46,7 @@ class TenderDetailCard extends StatelessWidget {
value: 0.75,
backgroundColor: AppColors.grey20,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.jellyBean),
minHeight: 6,
minHeight: 6.0.h(),
),
),
SizedBox(height: 16.0.h()),
@@ -54,26 +54,26 @@ class TenderDetailCard extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.warning_rounded, color: Colors.red),
const Icon(Icons.warning_rounded, color: AppColors.red),
SizedBox(width: 8.0.w()),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
children: [
Text(
AppStrings.tenderIncompleteResume,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: Colors.black87,
fontSize: 16.0.sp(),
color: AppColors.grey80,
),
),
SizedBox(height: 4.0),
SizedBox(height: 4.0.h()),
Text(
AppStrings.tenderNoExperience,
style: TextStyle(
fontSize: 14.0,
color: Colors.black54,
fontSize: 14.0.sp(),
color: AppColors.grey70,
fontWeight: FontWeight.w400,
),
),
@@ -86,4 +86,4 @@ class TenderDetailCard extends StatelessWidget {
),
);
}
}
}
@@ -1,22 +1,24 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
import 'package:tm_app/views/detail/widgets/status_tag.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_info_section.dart';
import 'package:tm_app/views/detail/widgets/tender_document_section.dart';
import 'package:tm_app/views/detail/widgets/tender_location_section.dart';
class TenderDetailHeader extends StatelessWidget {
final bool isBig;
const TenderDetailHeader({required this.isBig, super.key});
final bool isScreenBig;
final TenderDetailResponseModel detail;
const TenderDetailHeader({required this.isScreenBig, required this.detail, super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: AppColors.backgroundColor,
borderRadius: BorderRadius.circular(5),
border: Border.all(width: 0.5, color: AppColors.grey50),
),
@@ -29,10 +31,10 @@ class TenderDetailHeader extends StatelessWidget {
SizedBox(height: 10.0.h()),
const Divider(),
SizedBox(height: 5.0.h()),
TenderDetailInfoSection(isBig: isBig,),
TenderLocationSection(),
TenderDetailInfoSection(isScreenBig: isScreenBig, detail: detail),
TenderLocationSection(detail: detail),
SizedBox(height: 16.0.h()),
TenderDocumentSection(),
TenderDocumentSection(detail: detail),
],
),
);
@@ -42,15 +44,15 @@ class TenderDetailHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppStrings.tenderDateExample,
detail.date ?? '',
style: TextStyle(
color: AppColors.grey60,
fontWeight: FontWeight.w400,
fontSize: 12.0.sp(),
),
),
const StatusTag(
text: AppStrings.tenderStatusOpen,
StatusTag(
text: detail.status ?? '',
textColor: AppColors.cyanTeal,
backgroundColor: AppColors.cyanAqua,
),
@@ -58,7 +60,7 @@ class TenderDetailHeader extends StatelessWidget {
);
Widget _buildTitle() => Text(
AppStrings.tenderTitleExample,
detail.title ?? '',
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
import 'package:tm_app/views/detail/widgets/deadline_item.dart';
import 'package:tm_app/views/detail/widgets/info_item.dart';
class TenderDetailInfoSection extends StatelessWidget {
final bool isBig;
final bool isScreenBig;
final TenderDetailResponseModel detail;
const TenderDetailInfoSection({required this.isBig ,super.key});
const TenderDetailInfoSection({required this.isScreenBig, required this.detail, super.key});
@override
Widget build(BuildContext context) {
@@ -15,26 +17,27 @@ class TenderDetailInfoSection extends StatelessWidget {
children: [
InfoItem(
title: AppStrings.tenderIdLabel,
value: AppStrings.tenderIdExample,
value: detail.id ?? '',
),
DeadlineItem(
title: AppStrings.tenderDeadlineLabel,
approvalText: AppStrings.tenderApprovalText,
approvalDate: AppStrings.tenderApprovalDateExample,
approvalDate: detail.approvalDate ?? '',
submissionText: AppStrings.tenderSubmissionText,
submissionDate: AppStrings.tenderSubmissionDateExample, isBig: isBig,
submissionDate: detail.submissionDate ?? '',
isScreenBig: isScreenBig,
),
InfoItem(
title: AppStrings.tenderClientLabel,
value: AppStrings.tenderClientExample,
value: detail.client ?? '',
),
InfoItem(
title: AppStrings.tenderDeliveryLocationsLabel,
value: AppStrings.tenderDeliveryLocationExample,
value: detail.deliveryLocation ?? '',
),
InfoItem(
title: AppStrings.tenderReferenceNumberLabel,
value: AppStrings.tenderReferenceNumberExample,
value: detail.referenceNumber ?? '',
),
],
);
@@ -1,29 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
class TenderDocumentSection extends StatelessWidget {
const TenderDocumentSection({super.key});
final TenderDetailResponseModel detail;
const TenderDocumentSection({super.key, required this.detail});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SvgPicture.asset(AssetsManager.export),
SizedBox(height: 6.0.h()),
Text(
AppStrings.tenderPdfDocument,
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 16.0.sp(),
color: Colors.blue,
decoration: TextDecoration.underline,
return InkWell(
onTap: () {
if (detail.documentUrl != null) {
//launchUrl(Uri.parse(detail.documentUrl!));
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SvgPicture.asset(AssetsManager.export),
SizedBox(height: 6.0.h()),
Text(
detail.documentName ?? '',
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 16.0.sp(),
color: AppColors.textBlue,
decoration: TextDecoration.underline,
),
),
),
],
],
),
);
}
}
@@ -4,9 +4,12 @@ import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
class TenderLocationSection extends StatelessWidget {
const TenderLocationSection({super.key});
final TenderDetailResponseModel detail;
const TenderLocationSection({super.key, required this.detail});
@override
Widget build(BuildContext context) {
@@ -18,7 +21,7 @@ class TenderLocationSection extends StatelessWidget {
SizedBox(
height: 45.0.h(),
child: Text(
AppStrings.tenderReferenceNumberLabel,
detail.locationTitle ?? '',
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
@@ -35,7 +38,7 @@ class TenderLocationSection extends StatelessWidget {
),
SizedBox(width: 4.0.w()),
Text(
AppStrings.tenderLocationCountry,
detail.country ?? '',
style: TextStyle(
color: AppColors.grey70,
fontSize: 14.0.sp(),
@@ -44,7 +47,7 @@ class TenderLocationSection extends StatelessWidget {
),
SizedBox(width: 4.0.w()),
Image.asset(
AssetsManager.seFlag,
detail.flagAsset ?? AssetsManager.seFlag,
height: 20.0.h(),
width: 20.0.w(),
),
@@ -57,7 +60,7 @@ class TenderLocationSection extends StatelessWidget {
),
SizedBox(height: 8.0.h()),
Text(
AppStrings.locationDescription,
detail.locationDescription ?? '',
style: TextStyle(fontWeight: FontWeight.w400, fontSize: 14.0.sp()),
),
SizedBox(height: 16.0.h()),
-94
View File
@@ -1,94 +0,0 @@
// lib/views/login_desktop_screen.dart
import 'package:flutter/material.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import '../../core/constants/assets.dart';
import '../../core/constants/strings.dart';
import '../../core/utils/size_config.dart';
import 'widgets/widgets.dart';
import '../../view_models/login_view_model.dart';
class LoginDesktopScreen extends StatefulWidget {
const LoginDesktopScreen({super.key});
@override
State<LoginDesktopScreen> createState() => _LoginDesktopScreenState();
}
class _LoginDesktopScreenState extends State<LoginDesktopScreen> {
late final LoginViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = LoginViewModel();
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: SizedBox(
width: 120.0.w(),
child: ListenableBuilder(
listenable: _viewModel,
builder: (context, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LoginLogo(width: 190.0.w(), height: 88.0.h()),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: _viewModel.usernameController,
focusNode: _viewModel.usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
fieldHeight: 60.0.h(),
borderRedus: 4.0.w(),
prefixIconPadding: 4.0.w(),
),
SizedBox(height: 16.0.h()),
LoginTextField(
controller: _viewModel.passwordController,
focusNode: _viewModel.passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
prefixIconPadding: 4.0.w(),
isPassword: true,
obscureText: _viewModel.obscurePassword,
fieldHeight: 60.0.h(),
onToggleVisibility: _viewModel.togglePasswordVisibility,
borderRedus: 4.0.w(),
),
SizedBox(height: 32.0.h()),
BaseButton(
isEnabled: _viewModel.isLoginEnabled,
onPressed: _viewModel.isLoginEnabled
? () => HomeRouteData().go(context)
: null,
),
],
);
},
),
),
),
),
);
}
}
-88
View File
@@ -1,88 +0,0 @@
// lib/views/login_screen.dart
import 'package:flutter/material.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import '../../core/constants/assets.dart';
import '../../core/constants/strings.dart';
import '../../core/utils/size_config.dart';
import 'widgets/widgets.dart';
import '../../view_models/login_view_model.dart';
class LoginMobileScreen extends StatefulWidget {
const LoginMobileScreen({super.key});
@override
State<LoginMobileScreen> createState() => _LoginMobileScreenState();
}
class _LoginMobileScreenState extends State<LoginMobileScreen> {
late final LoginViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = LoginViewModel();
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: ListenableBuilder(
listenable: _viewModel,
builder: (context, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LoginLogo(width: 190.0.w(), height: 88.0.h()),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: _viewModel.usernameController,
focusNode: _viewModel.usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
),
SizedBox(height: 16.0.h()),
LoginTextField(
controller: _viewModel.passwordController,
focusNode: _viewModel.passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
isPassword: true,
obscureText: _viewModel.obscurePassword,
onToggleVisibility: _viewModel.togglePasswordVisibility,
),
SizedBox(height: 32.0.h()),
BaseButton(
isEnabled: _viewModel.isLoginEnabled,
onPressed: _viewModel.isLoginEnabled
? () {
HomeRouteData().go(context);
}
: null,
),
],
);
},
),
),
),
);
}
}
-24
View File
@@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/display_size_detector.dart';
import 'package:tm_app/views/login/login_desktop_screen.dart';
import 'package:tm_app/views/login/login_mobile_screen.dart';
import 'package:tm_app/views/login/login_tablet_screen.dart';
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
final displaySize = MediaQuery.of(context).size.displaySize;
switch (displaySize) {
case DisplaySize.large:
case DisplaySize.medium:
return const LoginDesktopScreen();
case DisplaySize.small:
return const LoginTabletScreen();
case DisplaySize.extraSmall:
return const LoginMobileScreen();
}
}
}
-94
View File
@@ -1,94 +0,0 @@
// lib/views/login_tablet_screen.dart
import 'package:flutter/material.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import '../../core/constants/assets.dart';
import '../../core/constants/strings.dart';
import '../../core/utils/size_config.dart';
import 'widgets/widgets.dart';
import '../../view_models/login_view_model.dart';
class LoginTabletScreen extends StatefulWidget {
const LoginTabletScreen({super.key});
@override
State<LoginTabletScreen> createState() => _LoginTabletScreenState();
}
class _LoginTabletScreenState extends State<LoginTabletScreen> {
late final LoginViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = LoginViewModel();
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: SizedBox(
width: 120.0.w(),
child: ListenableBuilder(
listenable: _viewModel,
builder: (context, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LoginLogo(width: 190.0.w(), height: 88.0.h()),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: _viewModel.usernameController,
focusNode: _viewModel.usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
fieldHeight: 60.0.h(),
borderRedus: 4.0.w(),
prefixIconPadding: 4.0.w(),
),
SizedBox(height: 16.0.h()),
LoginTextField(
controller: _viewModel.passwordController,
focusNode: _viewModel.passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
prefixIconPadding: 4.0.w(),
isPassword: true,
obscureText: _viewModel.obscurePassword,
fieldHeight: 60.0.h(),
onToggleVisibility: _viewModel.togglePasswordVisibility,
borderRedus: 4.0.w(),
),
SizedBox(height: 32.0.h()),
BaseButton(
isEnabled: _viewModel.isLoginEnabled,
onPressed: _viewModel.isLoginEnabled
? () => HomeRouteData().go(context)
: null,
),
],
);
},
),
),
),
),
);
}
}
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/core/theme/colors.dart';
import '../../../core/constants/assets.dart';
import '../../../core/constants/strings.dart';
import '../../../core/utils/size_config.dart';
import '../../../view_models/auth_view_model.dart';
import '../widgets/widgets.dart';
class LoginDesktopPage extends StatelessWidget {
const LoginDesktopPage({super.key});
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: AppColors.backgroundColor,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: SizedBox(
width: 526,
child: Consumer<AuthViewModel>(
builder: (context, viewModel, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LoginLogo(width: 190.0.w(), height: 88.0.h()),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: viewModel.usernameController,
focusNode: viewModel.usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
fieldHeight: 60.0.h(),
borderRedus: 4.0.w(),
prefixIconPadding: 4.0.w(),
),
SizedBox(height: 16.0.h()),
LoginTextField(
controller: viewModel.passwordController,
focusNode: viewModel.passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
prefixIconPadding: 4.0.w(),
isPassword: true,
obscureText: viewModel.obscurePassword,
fieldHeight: 60.0.h(),
onToggleVisibility: viewModel.togglePasswordVisibility,
borderRedus: 4.0.w(),
),
SizedBox(height: 32.0.h()),
if (viewModel.isLoading)
const CircularProgressIndicator()
else
BaseButton(
isEnabled: viewModel.isLoginEnabled,
onPressed:
viewModel.isLoginEnabled
? () async {
await viewModel.login();
if (viewModel.loggedInUser != null) {
HomeRouteData().go(context);
} else if (viewModel.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(viewModel.errorMessage!),
),
);
}
}
: null,
),
],
);
},
),
),
),
),
);
}
}
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/core/theme/colors.dart';
import '../../../core/constants/assets.dart';
import '../../../core/constants/strings.dart';
import '../../../core/utils/size_config.dart';
import '../../../view_models/auth_view_model.dart';
import '../widgets/widgets.dart';
class LoginMobilePage extends StatelessWidget {
const LoginMobilePage({super.key});
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: AppColors.backgroundColor,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: Consumer<AuthViewModel>(
builder: (context, viewModel, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LoginLogo(width: 190.0.w(), height: 88.0.h()),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: viewModel.usernameController,
focusNode: viewModel.usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
),
SizedBox(height: 16.0.h()),
LoginTextField(
controller: viewModel.passwordController,
focusNode: viewModel.passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
isPassword: true,
obscureText: viewModel.obscurePassword,
onToggleVisibility: viewModel.togglePasswordVisibility,
),
SizedBox(height: 32.0.h()),
if (viewModel.isLoading)
const CircularProgressIndicator()
else
BaseButton(
isEnabled: viewModel.isLoginEnabled,
onPressed:
viewModel.isLoginEnabled
? () async {
await viewModel.login();
if (viewModel.loggedInUser != null) {
HomeRouteData().go(context);
} else if (viewModel.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(viewModel.errorMessage!),
),
);
}
}
: null,
),
],
);
},
),
),
),
);
}
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/login/pages/login_desktop_page.dart';
import 'package:tm_app/views/login/pages/login_mobile_page.dart';
import 'package:tm_app/views/login/pages/login_tablet_page.dart';
import 'package:tm_app/views/shared/responsive_builder.dart';
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return ResponsiveBuilder(
mobile: LoginMobilePage(),
tablet: LoginTabletPage(),
desktop: LoginDesktopPage(),
);
}
}
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/core/theme/colors.dart';
import '../../../core/constants/assets.dart';
import '../../../core/constants/strings.dart';
import '../../../core/utils/size_config.dart';
import '../../../view_models/auth_view_model.dart';
import '../widgets/widgets.dart';
class LoginTabletPage extends StatelessWidget {
const LoginTabletPage({super.key});
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: AppColors.backgroundColor,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: SizedBox(
width: 526,
child: Consumer<AuthViewModel>(
builder: (context, viewModel, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LoginLogo(width: 190.0.w(), height: 88.0.h()),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: viewModel.usernameController,
focusNode: viewModel.usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
fieldHeight: 60.0.h(),
borderRedus: 4.0.w(),
prefixIconPadding: 4.0.w(),
),
SizedBox(height: 16.0.h()),
LoginTextField(
controller: viewModel.passwordController,
focusNode: viewModel.passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
prefixIconPadding: 4.0.w(),
isPassword: true,
obscureText: viewModel.obscurePassword,
fieldHeight: 60.0.h(),
onToggleVisibility: viewModel.togglePasswordVisibility,
borderRedus: 4.0.w(),
),
SizedBox(height: 32.0.h()),
if (viewModel.isLoading)
const CircularProgressIndicator()
else
BaseButton(
isEnabled: viewModel.isLoginEnabled,
onPressed:
viewModel.isLoginEnabled
? () async {
await viewModel.login();
if (viewModel.loggedInUser != null) {
HomeRouteData().go(context);
} else if (viewModel.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(viewModel.errorMessage!),
),
);
}
}
: null,
),
],
);
},
),
),
),
),
);
}
}
+6 -4
View File
@@ -14,6 +14,7 @@ class BaseButton extends StatelessWidget {
final double? borderWidth;
final double? borderRadius;
final double? elevation;
final double? width;
const BaseButton({
required this.isEnabled,
@@ -25,21 +26,22 @@ class BaseButton extends StatelessWidget {
this.borderWidth,
this.borderRadius,
this.elevation,
this.width,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
width: width ?? double.infinity,
height: 55.0.h(),
child: ElevatedButton(
onPressed: isEnabled ? onPressed : null,
style: ElevatedButton.styleFrom(
backgroundColor:
backgroundColor ??
(isEnabled ? AppColors.mainBlue : Colors.grey[300]),
foregroundColor: textColor ?? Colors.white,
(isEnabled ? AppColors.mainBlue : AppColors.backgroundColor),
foregroundColor: textColor ?? AppColors.backgroundColor,
elevation: elevation ?? 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius ?? 28.0.w()),
@@ -54,7 +56,7 @@ class BaseButton extends StatelessWidget {
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.bold,
color: textColor ?? Colors.white,
color: textColor ?? AppColors.backgroundColor,
),
),
),
+2 -2
View File
@@ -52,7 +52,7 @@ class LoginTextField extends StatelessWidget {
width: 24.0.w(),
height: 24.0.h(),
colorFilter: ColorFilter.mode(
focusNode.hasFocus ? Colors.black87 : Colors.grey,
focusNode.hasFocus ? AppColors.grey80 : AppColors.grey60,
BlendMode.srcIn,
),
),
@@ -68,7 +68,7 @@ class LoginTextField extends StatelessWidget {
width: 24.0.w(),
height: 24.0.h(),
colorFilter: ColorFilter.mode(
focusNode.hasFocus ? Colors.black87 : Colors.grey,
focusNode.hasFocus ? AppColors.grey80 : AppColors.grey60,
BlendMode.srcIn,
),
),
+2 -1
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
import '../../../core/constants/strings.dart';
import '../../../core/utils/size_config.dart';
@@ -20,7 +21,7 @@ class LoginTitle extends StatelessWidget {
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w400,
color: Colors.grey,
color: AppColors.grey60,
),
textAlign: TextAlign.center,
),
+27 -48
View File
@@ -29,10 +29,10 @@ packages:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.13.0"
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
@@ -101,10 +101,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62"
sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb
url: "https://pub.dev"
source: hosted
version: "8.11.0"
version: "8.11.1"
characters:
dependency: transitive
description:
@@ -177,30 +177,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
device_frame:
dependency: transitive
description:
name: device_frame
sha256: "7b2ebb2a09d6cc0f086b51bd1412d7be83e0170056a7290349169be41164c86a"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
device_preview:
dependency: "direct main"
description:
name: device_preview
sha256: "88aa1cc73ee9a8ec771b309dcbc4000cc66b5d8456b825980997640ab1195bf5"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
dio:
dependency: "direct main"
description:
name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.dev"
source: hosted
version: "5.8.0+1"
version: "5.9.0"
dio_web_adapter:
dependency: transitive
description:
@@ -213,10 +197,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
version: "1.3.2"
ffi:
dependency: transitive
description:
@@ -254,11 +238,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_svg:
dependency: "direct main"
description:
@@ -337,10 +316,10 @@ packages:
dependency: transitive
description:
name: http
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.5.0"
http_multi_server:
dependency: transitive
description:
@@ -357,14 +336,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: transitive
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
io:
dependency: transitive
description:
@@ -382,21 +353,29 @@ packages:
source: hosted
version: "0.7.2"
json_annotation:
dependency: transitive
dependency: "direct main"
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
url: "https://pub.dev"
source: hosted
version: "6.9.5"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.9"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
@@ -593,10 +572,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
url: "https://pub.dev"
source: hosted
version: "2.4.10"
version: "2.4.11"
shared_preferences_foundation:
dependency: transitive
description:
@@ -782,10 +761,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "15.0.0"
version: "14.3.1"
watcher:
dependency: transitive
description:
@@ -843,5 +822,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.32.0"
dart: ">=3.7.2 <4.0.0"
flutter: ">=3.27.0"
+3 -2
View File
@@ -20,7 +20,7 @@ dependencies:
logger: ^2.6.0
dio: ^5.8.0+1
shared_preferences: ^2.5.3
json_annotation: ^4.8.1
dev_dependencies:
flutter_test:
@@ -29,7 +29,8 @@ dev_dependencies:
flutter_gen_runner:
build_runner: ^2.5.4
go_router_builder: ^3.0.0
json_serializable: ^6.9.5
json_serializable: ^6.7.1
flutter: