Update iOS deployment target to 13.0, enhance Flutter integration, and improve tender submission flow

- Changed the iOS deployment target from 12.0 to 13.0 in Podfile and project settings.
- Updated AppDelegate to support implicit Flutter engine initialization.
- Modified Info.plist to include scene configuration for multiple scenes.
- Enhanced CompletionOfDocumentsRouteData to accept a tenderId parameter.
- Implemented tender submission functionality in TenderDetailViewModel and updated UI components to reflect loading states and success/error messages.
- Added new strings for submission success and error notifications.
This commit is contained in:
AmirReza Jamali
2026-05-30 16:48:05 +03:30
parent ae08b946f6
commit e6b4720dcd
19 changed files with 445 additions and 55 deletions
+5 -2
View File
@@ -250,11 +250,14 @@ class FinalCompletionOfDocumentsRouteData extends GoRouteData
@TypedGoRoute<CompletionOfDocumentsRouteData>(path: '/completion_of_documents')
class CompletionOfDocumentsRouteData extends GoRouteData
with _$CompletionOfDocumentsRouteData {
const CompletionOfDocumentsRouteData();
final String? tenderId;
const CompletionOfDocumentsRouteData({this.tenderId});
@override
Widget build(BuildContext context, GoRouterState state) {
return const CompletionOfDocumentsScreen();
return tenderDetailProvider(
child: CompletionOfDocumentsScreen(tenderId: tenderId),
);
}
}
+12 -2
View File
@@ -308,10 +308,20 @@ RouteBase get $completionOfDocumentsRouteData => GoRouteData.$route(
mixin _$CompletionOfDocumentsRouteData on GoRouteData {
static CompletionOfDocumentsRouteData _fromState(GoRouterState state) =>
const CompletionOfDocumentsRouteData();
CompletionOfDocumentsRouteData(
tenderId: state.uri.queryParameters['tender-id'],
);
CompletionOfDocumentsRouteData get _self =>
this as CompletionOfDocumentsRouteData;
@override
String get location => GoRouteData.$location('/completion_of_documents');
String get location => GoRouteData.$location(
'/completion_of_documents',
queryParams: {
if (_self.tenderId != null) 'tender-id': _self.tenderId!,
},
);
@override
void go(BuildContext context) => context.go(location);
@@ -6,6 +6,7 @@ import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
import '../core/constants/tender_approval_status.dart';
import '../core/constants/tender_submision_mode.dart';
import '../data/services/model/tender_approvals_by_id_response/tender_approvals_by_id_response.dart';
import '../data/services/model/tender_approvals_data/tender_approvals_data.dart';
import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart';
@@ -143,4 +144,33 @@ class TenderDetailViewModel with ChangeNotifier {
_errorMessage = null;
notifyListeners();
}
/// Submits the tender for completion via the self-apply flow.
/// POSTs to /api/v1/tender-approvals with status "submitted" and
/// submission_mode "self-apply". Returns true on success.
Future<bool> submitForCompletion({required String tenderId}) async {
_isSubmitApprovalLoading = true;
_errorMessage = null;
notifyListeners();
bool success = false;
final result = await _tendersRepository.toggleApprovals(
tenderId: tenderId,
status: TenderApprovalStatus.submitted.value,
submissionMode: TenderSubmissionMode.selfApply.value,
);
switch (result) {
case Ok<TenderApprovalsToggleResponse>():
success = true;
_status = result.value.data?.status ?? '';
break;
case Error<TenderApprovalsToggleResponse>():
_errorMessage = result.error.toString();
break;
}
_isSubmitApprovalLoading = false;
notifyListeners();
return success;
}
}
+13 -4
View File
@@ -206,7 +206,9 @@ class YourTendersViewModel with ChangeNotifier {
offset: _tendersOffset,
);
await getTenders(requestModel: requestModel);
} else if (selectedStatus == YourTendersStrings.tendersSubmitted) {
} else if (selectedStatus == YourTendersStrings.tendersSubmitted ||
selectedStatus == TenderApprovalStatus.submitted.value ||
selectedStatus == TenderApprovalStatus.approved.value) {
final requestModel = GetTendersRequestModel(
status: TenderApprovalStatus.submitted.value,
createdFrom: startDateUnix,
@@ -299,10 +301,17 @@ class YourTendersViewModel with ChangeNotifier {
reset: true,
);
} else if (selectedStatus == TenderApprovalStatus.approved.value) {
await getTendersFeedback(
feedbackType: TenderApprovalStatus.approved.value,
reset: true,
// "Approved tenders" maps to submitted tender-approvals. The API has no
// `approved` value; approved tenders are the ones a company has submitted
// for pursuit (GET /api/v1/tender-approvals?status=submitted).
final requestModel = GetTendersRequestModel(
status: TenderApprovalStatus.submitted.value,
createdFrom: startDateUnix,
createdTo: endDateUnix,
limit: _tendersLimit,
offset: _tendersOffset,
);
await getTenders(requestModel: requestModel, reset: true);
}
}
@@ -7,7 +7,8 @@ import '../../../core/utils/size_config.dart';
import '../../shared/responsive_builder.dart';
class CompletionOfDocumentsScreen extends StatefulWidget {
const CompletionOfDocumentsScreen({super.key});
final String? tenderId;
const CompletionOfDocumentsScreen({this.tenderId, super.key});
@override
State<CompletionOfDocumentsScreen> createState() => _CompletionOfDocumentsScreenState();
@@ -41,10 +42,10 @@ class _CompletionOfDocumentsScreenState extends State<CompletionOfDocumentsScree
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return const ResponsiveBuilder(
mobile: CompletionOfDocumentsMobilePage(),
tablet: CompletionOfDocumentsTabletPage(),
desktop: CompletionOfDocumentsDesktopPage(),
return ResponsiveBuilder(
mobile: CompletionOfDocumentsMobilePage(tenderId: widget.tenderId),
tablet: CompletionOfDocumentsTabletPage(tenderId: widget.tenderId),
desktop: CompletionOfDocumentsDesktopPage(tenderId: widget.tenderId),
);
}
}
@@ -3,6 +3,10 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/utils/app_toast.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/completion_of_documents/strings/completion_of_documents_string.dart';
import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/shared/desktop_navigation_widget.dart';
@@ -10,7 +14,8 @@ import 'package:tm_app/views/shared/tablet_desktop_appbar.dart'
show TabletDesktopAppbar;
class CompletionOfDocumentsDesktopPage extends StatelessWidget {
const CompletionOfDocumentsDesktopPage({super.key});
final String? tenderId;
const CompletionOfDocumentsDesktopPage({this.tenderId, super.key});
@override
Widget build(BuildContext context) {
@@ -77,14 +82,17 @@ class CompletionOfDocumentsDesktopPage extends StatelessWidget {
SizedBox(height: 24.0.h()),
BaseButton(
isEnabled: true,
onPressed: () {},
text:
CompletionOfDocumentsStrings
.submitForCompletion,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary30,
Consumer<TenderDetailViewModel>(
builder: (context, viewModel, _) => BaseButton(
isEnabled: !viewModel.isSubmitApprovalLoading,
isLoading: viewModel.isSubmitApprovalLoading,
onPressed: () => _submitForCompletion(context),
text:
CompletionOfDocumentsStrings
.submitForCompletion,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary30,
),
),
SizedBox(height: 24.0.h()),
],
@@ -304,6 +312,26 @@ class CompletionOfDocumentsDesktopPage extends StatelessWidget {
);
}
Future<void> _submitForCompletion(BuildContext context) async {
final id = tenderId;
if (id == null || id.isEmpty) {
AppToast.error(context, CompletionOfDocumentsStrings.submitError);
return;
}
final viewModel = context.read<TenderDetailViewModel>();
final success = await viewModel.submitForCompletion(tenderId: id);
if (!context.mounted) return;
if (success) {
AppToast.success(context, CompletionOfDocumentsStrings.submitSuccess);
context.pop();
} else {
AppToast.error(
context,
viewModel.errorMessage ?? CompletionOfDocumentsStrings.submitError,
);
}
}
Widget _docCard({
required String title,
required String subtitle,
@@ -3,13 +3,18 @@ import 'package:flutter_svg/svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/utils/app_toast.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/completion_of_documents/strings/completion_of_documents_string.dart';
import 'package:tm_app/views/shared/base_button.dart';
import '../../shared/tender_app_bar.dart';
class CompletionOfDocumentsMobilePage extends StatelessWidget {
const CompletionOfDocumentsMobilePage({super.key});
final String? tenderId;
const CompletionOfDocumentsMobilePage({this.tenderId, super.key});
@override
Widget build(BuildContext context) {
@@ -242,12 +247,15 @@ class CompletionOfDocumentsMobilePage extends StatelessWidget {
),
),
SizedBox(height: 24.0.h()),
BaseButton(
isEnabled: true,
onPressed: () {},
text: CompletionOfDocumentsStrings.submitForCompletion,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary30,
Consumer<TenderDetailViewModel>(
builder: (context, viewModel, _) => BaseButton(
isEnabled: !viewModel.isSubmitApprovalLoading,
isLoading: viewModel.isSubmitApprovalLoading,
onPressed: () => _submitForCompletion(context),
text: CompletionOfDocumentsStrings.submitForCompletion,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary30,
),
),
],
),
@@ -256,6 +264,26 @@ class CompletionOfDocumentsMobilePage extends StatelessWidget {
);
}
Future<void> _submitForCompletion(BuildContext context) async {
final id = tenderId;
if (id == null || id.isEmpty) {
AppToast.error(context, CompletionOfDocumentsStrings.submitError);
return;
}
final viewModel = context.read<TenderDetailViewModel>();
final success = await viewModel.submitForCompletion(tenderId: id);
if (!context.mounted) return;
if (success) {
AppToast.success(context, CompletionOfDocumentsStrings.submitSuccess);
context.pop();
} else {
AppToast.error(
context,
viewModel.errorMessage ?? CompletionOfDocumentsStrings.submitError,
);
}
}
Widget _docCard({
required String title,
required String subtitle,
@@ -3,6 +3,10 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/utils/app_toast.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/completion_of_documents/strings/completion_of_documents_string.dart';
import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/shared/tablet_desktop_appbar.dart';
@@ -10,7 +14,8 @@ import 'package:tm_app/views/shared/tablet_navigation_widget.dart';
import 'package:tm_app/views/shared/tender_app_bar.dart';
class CompletionOfDocumentsTabletPage extends StatelessWidget {
const CompletionOfDocumentsTabletPage({super.key});
final String? tenderId;
const CompletionOfDocumentsTabletPage({this.tenderId, super.key});
@override
Widget build(BuildContext context) {
@@ -83,14 +88,17 @@ class CompletionOfDocumentsTabletPage extends StatelessWidget {
SizedBox(height: 24.0.h()),
BaseButton(
isEnabled: true,
onPressed: () {},
text:
CompletionOfDocumentsStrings
.submitForCompletion,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary30,
Consumer<TenderDetailViewModel>(
builder: (context, viewModel, _) => BaseButton(
isEnabled: !viewModel.isSubmitApprovalLoading,
isLoading: viewModel.isSubmitApprovalLoading,
onPressed: () => _submitForCompletion(context),
text:
CompletionOfDocumentsStrings
.submitForCompletion,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary30,
),
),
],
),
@@ -309,6 +317,26 @@ class CompletionOfDocumentsTabletPage extends StatelessWidget {
);
}
Future<void> _submitForCompletion(BuildContext context) async {
final id = tenderId;
if (id == null || id.isEmpty) {
AppToast.error(context, CompletionOfDocumentsStrings.submitError);
return;
}
final viewModel = context.read<TenderDetailViewModel>();
final success = await viewModel.submitForCompletion(tenderId: id);
if (!context.mounted) return;
if (success) {
AppToast.success(context, CompletionOfDocumentsStrings.submitSuccess);
context.pop();
} else {
AppToast.error(
context,
viewModel.errorMessage ?? CompletionOfDocumentsStrings.submitError,
);
}
}
// -------------------- Document Card --------------------
Widget _docCard({
required String title,
@@ -7,4 +7,6 @@ class CompletionOfDocumentsStrings {
'Set Deadline for Completion of documents :';
static const String note = 'Note:';
static const String submitForCompletion = 'Submit for completion';
static const String submitSuccess = 'Tender submitted for completion';
static const String submitError = 'Failed to submit tender for completion';
}
@@ -75,9 +75,9 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
// submissionMode: value,
// );
const CompletionOfDocumentsRouteData().push(
context,
);
CompletionOfDocumentsRouteData(
tenderId: widget.detail.id!,
).push(context);
} else if (value ==
TenderSubmissionMode.partnership.value) {
Future.delayed(Duration.zero, () {
@@ -213,8 +213,9 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
const FinalCompletionOfDocumentsRouteData()
.push(context);
} else {
const CompletionOfDocumentsRouteData()
.push(context);
CompletionOfDocumentsRouteData(
tenderId: widget.detail.id!,
).push(context);
}
}
} else if (value ==
@@ -23,4 +23,5 @@ class ProfileStrings {
static const String mainDataTab = 'Main Data';
static const String membersTab = 'Members';
static const String subjectTab = 'Subject';
static const String appVersion = 'Version 1.0.7';
}
@@ -98,6 +98,15 @@ class MainDataTab extends StatelessWidget {
),
),
),
SizedBox(height: 24.0.h()),
Text(
ProfileStrings.appVersion,
style: TextStyle(
fontSize: 12.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey70,
),
),
],
),
);