added logic for detail

This commit is contained in:
llsajjad
2025-08-09 19:26:56 +03:30
parent 83ca169059
commit d0afc23be1
18 changed files with 1051 additions and 150 deletions
+8 -1
View File
@@ -1,6 +1,9 @@
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/tender_detail_repository.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';
@@ -21,12 +24,13 @@ List<SingleChildWidget> get cores {
List<SingleChildWidget> get apiClients {
return [
Provider(create: (context) => AuthService(networkManager: context.read())),
];
Provider(create: (context) => TenderDetailService(networkManager: context.read())),];
}
List<SingleChildWidget> get repositories {
return [
Provider(create: (context) => AuthRepository(authService: context.read())),
Provider(create: (context) => TenderDetailRepository(tenderDetailService: context.read())),
];
}
@@ -34,6 +38,9 @@ List<SingleChildWidget> get viewModels {
return [
ChangeNotifierProvider(
create: (context) => AuthViewModel(authRepository: context.read()),
),
ChangeNotifierProvider(
create: (context) => TenderDetailViewModel(tenderDetailRepository: context.read()),
),
];
}
@@ -0,0 +1,14 @@
import 'package:tm_app/core/utils/result.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 String id}) async {
return _tenderDetailService.getTenderDetail(id: id);
}
}
@@ -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.pngtest@example.compassword123",
"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,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,34 @@
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_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 String id}) async {
try {
await Future.delayed(Duration(seconds: 2)); // شبیه سازی لود
appLogger.info('get tender detail success');
return Result.ok(
TenderDetailResponseModel.fromJson(
jsonDecode(tenderDetailMockData)['data'],
),
);
} on DioException catch (e) {
appLogger.error('get tender detail failed: $e');
return Result.error(e);
} on Exception catch (e, stackTrace) {
appLogger.error('get tender detail failed: $e', stackTrace: stackTrace);
return Result.error(Exception(e));
}
}
}
@@ -0,0 +1,39 @@
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_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 result = await _tenderDetailRepository.getTenderDetail(id: id);
switch (result) {
case Ok<TenderDetailResponseModel>():
_tenderDetail = result.value;
break;
case Error<TenderDetailResponseModel>():
_errorMessage = result.error.toString();
break;
}
_isLoading = false;
notifyListeners();
}
}
+55 -22
View File
@@ -1,42 +1,75 @@
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_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailDesktopPage extends StatelessWidget {
const TenderDetailDesktopPage({super.key});
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) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: AppColors.backgroundColor,
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(isScreenBig: true),
SizedBox(height: 24.0.h()),
const TenderDetailCard(),
SizedBox(height: 24.0.h()),
TenderDetailActions(isScreenBig: true),
],
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),
],
),
),
),
),
),
),
);
},
),
);
}
+52 -17
View File
@@ -1,35 +1,70 @@
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_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailMobilePage extends StatelessWidget {
const TenderDetailMobilePage({super.key});
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: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isScreenBig: false),
SizedBox(height: 24.0.h()),
const TenderDetailCard(),
SizedBox(height: 24.0.h()),
TenderDetailActions(isScreenBig: false,),
],
),
),
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),
],
),
),
);
},
),
);
}
+3 -3
View File
@@ -12,9 +12,9 @@ class TenderDetailScreen extends StatelessWidget {
Widget build(BuildContext context) {
SizeConfig.init(context);
return ResponsiveBuilder(
mobile: TenderDetailMobilePage(),
tablet: TenderDetailTabletPage(),
desktop: TenderDetailDesktopPage(),
mobile: TenderDetailMobilePage(tenderId: 'JNDFKMDV-100JF',),
tablet: TenderDetailTabletPage(tenderId: 'JNDFKMDV-100JF',),
desktop: TenderDetailDesktopPage(tenderId: 'JNDFKMDV-100JF',),
);
}
}
+54 -20
View File
@@ -1,40 +1,74 @@
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_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailTabletPage extends StatelessWidget {
const TenderDetailTabletPage({super.key});
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: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 768,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isScreenBig: true),
SizedBox(height: 24.0.h()),
const TenderDetailCard(),
SizedBox(height: 24.0.h()),
TenderDetailActions(isScreenBig: true,),
],
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),
],
),
),
),
),
),
),
);
},
),
);
}
@@ -1,7 +1,7 @@
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';
@@ -9,7 +9,9 @@ import 'package:tm_app/views/detail/widgets/tender_location_section.dart';
class TenderDetailHeader extends StatelessWidget {
final bool isScreenBig;
const TenderDetailHeader({required this.isScreenBig, super.key});
final TenderDetailResponseModel detail;
const TenderDetailHeader({required this.isScreenBig, required this.detail, super.key});
@override
Widget build(BuildContext context) {
@@ -29,10 +31,10 @@ class TenderDetailHeader extends StatelessWidget {
SizedBox(height: 10.0.h()),
const Divider(),
SizedBox(height: 5.0.h()),
TenderDetailInfoSection(isScreenBig: isScreenBig),
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 isScreenBig;
final TenderDetailResponseModel detail;
const TenderDetailInfoSection({required this.isScreenBig, super.key});
const TenderDetailInfoSection({required this.isScreenBig, required this.detail, super.key});
@override
Widget build(BuildContext context) {
@@ -15,27 +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,
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,30 +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: AppColors.textBlue,
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()),
+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"
+2 -1
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:
@@ -28,6 +28,7 @@ dev_dependencies:
flutter_lints: ^5.0.0
build_runner: ^2.5.4
go_router_builder: ^3.0.0
json_serializable: ^6.7.1
flutter: