diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index 77efb55..350f69c 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -16,7 +16,8 @@ class AppConfig { // // Handle different platforms for local development if (kIsWeb) { // For web, use localhost - return 'https://app.opplens.com'; + // return 'https://app.opplens.com'; + return 'http://localhost:8081'; } else { // For Android emulator, use 10.0.2.2 (special IP for host machine) return 'http://10.0.2.2:8081'; diff --git a/lib/main.dart b/lib/main.dart index 0522f59..9fb1f48 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,7 +17,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - + // Initialize the logger appLogger.init(); @@ -55,7 +55,7 @@ class _MyAppState extends State { final themeProvider = context.watch(); return MaterialApp.router( - title: 'Flutter Demo', + title: 'OppLens', debugShowCheckedModeBanner: false, scrollBehavior: MaterialScrollBehavior().copyWith( dragDevices: { diff --git a/lib/view_models/liked_tenders_view_model.dart b/lib/view_models/liked_tenders_view_model.dart index 55c5b05..68fe799 100644 --- a/lib/view_models/liked_tenders_view_model.dart +++ b/lib/view_models/liked_tenders_view_model.dart @@ -4,6 +4,7 @@ import 'package:tm_app/data/repositories/liked_tenders_repository.dart'; import 'package:tm_app/data/repositories/tenders_repository.dart'; import 'package:tm_app/data/services/model/liked_tender_feedback/liked_tender_feedback.dart'; import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart'; +import 'package:tm_app/data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart'; import '../data/services/model/feedback_response/feedback_response.dart'; import '../data/services/model/tender_approvals_by_id_response/tender_approvals_by_id_response.dart'; @@ -34,6 +35,18 @@ class LikedTendersViewModel with ChangeNotifier { int currentPage = 1; + // Approval status properties + bool approvalStatus = false; + String _status = ''; + String? _errorMessage; + bool _isSubmitApprovalLoading = false; + bool _isRejectApprovalLoading = false; + + String get status => _status; + String? get approvalErrorMessage => _errorMessage; + bool get isSubmitApprovalLoading => _isSubmitApprovalLoading; + bool get isRejectApprovalLoading => _isRejectApprovalLoading; + Future getLikedTenders({bool reset = false}) async { if (!reset && (isLoadingMore || !hasMore)) { return; @@ -199,61 +212,157 @@ class LikedTendersViewModel with ChangeNotifier { return tenderApprovals.any((a) => a.tenderId == tenderId); } - // Future submitTenderApproval({ - // required String tenderId, - // required String submissionMode, - // }) async { + /// Get approval status for a tender safely + String? getApprovalStatusForTenderId(String tenderId) { + try { + final approval = tenderApprovals.firstWhere( + (a) => a.tenderId == tenderId, + ); + return approval.status; + } catch (e) { + return null; + } + } - // errorMessage = null; - // notifyListeners(); - // final result = await _tendersRepository.toggleApprovals( - // tenderId: tenderId, - // status: 'submitted', - // submissionMode: submissionMode, - // ); - // switch (result) { - // case Ok(): - // approvalStatus = true; - // _status = result.value.data?.status ?? ''; - // break; - // case Error(): - // _errorMessage = result.error.toString(); - // approvalStatus = false; - // break; - // } - // _isSubmitApprovalLoading = false; - // notifyListeners(); - // _errorMessage = null; - // approvalStatus = false; - // notifyListeners(); - // } + /// Check if tender is submitted + bool isTenderSubmitted(String tenderId) { + return getApprovalStatusForTenderId(tenderId) == 'submitted'; + } - // Future rejectTenderApproval({ - // required String tenderId, - // required String submissionMode, - // }) async { - // _isRejectApprovalLoading = true; - // _errorMessage = null; - // notifyListeners(); - // final result = await _tendersRepository.toggleApprovals( - // tenderId: tenderId, - // status: 'rejected', - // submissionMode: submissionMode, - // ); - // switch (result) { - // case Ok(): - // approvalStatus = true; - // _status = result.value.data?.status ?? ''; - // break; - // case Error(): - // _errorMessage = result.error.toString(); - // approvalStatus = false; - // break; - // } - // _isRejectApprovalLoading = false; - // notifyListeners(); - // _errorMessage = null; - // approvalStatus = false; - // notifyListeners(); - // } + /// Check if tender is rejected + bool isTenderRejected(String tenderId) { + return getApprovalStatusForTenderId(tenderId) == 'rejected'; + } + + Future submitTenderApproval({ + required String tenderId, + required String submissionMode, + }) async { + _isSubmitApprovalLoading = true; + _errorMessage = null; + notifyListeners(); + + // Store original state for rollback + final originalApprovals = List.of(tenderApprovals); + + // Check current approval status and update optimistically + final existingApproval = getApprovalStatusForTender(tenderId); + + if (existingApproval != null) { + if (existingApproval.status == 'submitted') { + // Remove from list if already submitted + tenderApprovals.removeWhere((a) => a.tenderId == tenderId); + } else if (existingApproval.status == 'rejected') { + // Change rejected to submitted + final updatedApproval = existingApproval.copyWith(status: 'submitted'); + final index = tenderApprovals.indexWhere((a) => a.tenderId == tenderId); + if (index != -1) { + tenderApprovals[index] = updatedApproval; + } + } + } else { + // Add new approval with submitted status + final newApproval = TenderApprovalsData( + id: null, + status: 'submitted', + tenderId: tenderId, + companyId: null, + submissionMode: submissionMode, + createdAt: null, + tender: null, + ); + tenderApprovals.add(newApproval); + } + + notifyListeners(); + + // Make server request + final result = await _tendersRepository.toggleApprovals( + tenderId: tenderId, + status: 'submitted', + submissionMode: submissionMode, + ); + + switch (result) { + case Ok(): + approvalStatus = true; + _status = result.value.data?.status ?? ''; + break; + case Error(): + // Rollback changes on error + tenderApprovals = originalApprovals; + _errorMessage = result.error.toString(); + approvalStatus = false; + break; + } + + _isSubmitApprovalLoading = false; + notifyListeners(); + } + + Future rejectTenderApproval({ + required String tenderId, + required String submissionMode, + }) async { + _isRejectApprovalLoading = true; + _errorMessage = null; + notifyListeners(); + + // Store original state for rollback + final originalApprovals = List.of(tenderApprovals); + + // Check current approval status and update optimistically + final existingApproval = getApprovalStatusForTender(tenderId); + + if (existingApproval != null) { + if (existingApproval.status == 'rejected') { + // Remove from list if already rejected + tenderApprovals.removeWhere((a) => a.tenderId == tenderId); + } else if (existingApproval.status == 'submitted') { + // Change submitted to rejected + final updatedApproval = existingApproval.copyWith(status: 'rejected'); + final index = tenderApprovals.indexWhere((a) => a.tenderId == tenderId); + if (index != -1) { + tenderApprovals[index] = updatedApproval; + } + } + } else { + // Add new approval with rejected status + final newApproval = TenderApprovalsData( + id: null, + status: 'rejected', + tenderId: tenderId, + companyId: null, + submissionMode: submissionMode, + createdAt: null, + tender: null, + ); + tenderApprovals.add(newApproval); + } + + notifyListeners(); + + // Make server request + final result = await _tendersRepository.toggleApprovals( + tenderId: tenderId, + status: 'rejected', + submissionMode: submissionMode, + ); + + switch (result) { + case Ok(): + approvalStatus = true; + _status = result.value.data?.status ?? ''; + break; + case Error(): + // Rollback changes on error + tenderApprovals = originalApprovals; + _errorMessage = result.error.toString(); + approvalStatus = false; + break; + } + + _isRejectApprovalLoading = false; + notifyListeners(); + } } diff --git a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart index 9eecb80..0e59862 100644 --- a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart +++ b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart @@ -1,4 +1,5 @@ import 'package:country_flags/country_flags.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; @@ -10,6 +11,8 @@ import 'package:tm_app/view_models/liked_tenders_view_model.dart'; import 'package:tm_app/views/home/strings/home_strings.dart'; import '../../../data/services/model/tender_approvals_data/tender_approvals_data.dart'; +import '../../shared/select_submission_bottom_sheet.dart'; +import '../../shared/select_submission_dialog.dart'; class LikedListItem extends StatelessWidget { const LikedListItem({ @@ -32,7 +35,6 @@ class LikedListItem extends StatelessWidget { borderRadius: BorderRadius.circular(4), border: Border.all(color: AppColors.grey30), ), - margin: EdgeInsetsDirectional.only(end: 16.0.w()), width: double.infinity, height: 250.0.h(), child: Padding( @@ -172,52 +174,99 @@ class LikedListItem extends StatelessWidget { ), ), Spacer(), - Container( - width: 32.0.w(), - height: 32.0.h(), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( + InkWell( + onTap: () { + viewModel.rejectTenderApproval( + tenderId: tender.id!, + submissionMode: 'self-apply', + ); + }, + child: Container( + width: 32.0.w(), + height: 32.0.h(), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: + viewModel.isTenderRejected(tender.id!) + ? AppColors.errorColor + : AppColors.grey60, + width: 1.5.w(), + ), + ), + alignment: Alignment.center, + child: Icon( + Icons.close, color: - approval?.status == 'rejected' + viewModel.isTenderRejected(tender.id!) ? AppColors.errorColor : AppColors.grey60, - width: 1.5.w(), + size: 16, ), ), - alignment: Alignment.center, - child: Icon( - Icons.close, - color: - approval?.status == 'rejected' - ? AppColors.errorColor - : AppColors.grey60, - size: 16, - ), ), SizedBox(width: 30.0.w()), - Container( - width: 32.0.w(), - height: 32.0.h(), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( + InkWell( + onTap: () { + if (viewModel.isTenderSubmitted(tender.id!)) { + viewModel.submitTenderApproval( + tenderId: tender.id!, + submissionMode: 'self-apply', + ); + } else { + kIsWeb + ? showDialog( + context: context, + builder: (context) { + return SelectSubmissionDialog( + onConfirm: (value) { + viewModel.submitTenderApproval( + tenderId: tender.id!, + submissionMode: value, + ); + }, + ); + }, + ) + : showModalBottomSheet( + isDismissible: true, + context: context, + builder: (context) { + return SelectSubmissionBottomSheet( + onConfirm: (value) { + viewModel.submitTenderApproval( + tenderId: tender.id!, + submissionMode: value, + ); + }, + ); + }, + ); + } + }, + child: Container( + width: 32.0.w(), + height: 32.0.h(), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: + viewModel.isTenderSubmitted(tender.id!) + ? AppColors.successColor + : AppColors.grey60, + width: 1.5.w(), + ), + ), + alignment: Alignment.center, + child: Icon( + Icons.check, color: - approval?.status == 'submitted' + viewModel.isTenderSubmitted(tender.id!) ? AppColors.successColor : AppColors.grey60, - width: 1.5.w(), + size: 16, ), ), - alignment: Alignment.center, - child: Icon( - Icons.check, - color: - approval?.status == 'submitted' - ? AppColors.successColor - : AppColors.grey60, - size: 16, - ), ), ], ),