Merge pull request 'delete_like' (#94) from delete_like into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/94
This commit is contained in:
a.ghabeli
2025-09-03 16:07:48 +03:30
10 changed files with 211 additions and 103 deletions
+4 -2
View File
@@ -119,8 +119,10 @@ List<SingleChildWidget> get viewModels {
), ),
ChangeNotifierProvider( ChangeNotifierProvider(
create: create:
(context) => (context) => LikedTendersViewModel(
LikedTendersViewModel(likedTendersRepository: context.read()), likedTendersRepository: context.read(),
tendersRepository: context.read(),
),
), ),
ChangeNotifierProvider( ChangeNotifierProvider(
create: create:
@@ -5,6 +5,7 @@ import 'package:tm_app/core/routes/app_shell/desktop_shell_page.dart';
import 'package:tm_app/core/routes/app_shell/mobile_shell_page.dart'; import 'package:tm_app/core/routes/app_shell/mobile_shell_page.dart';
import 'package:tm_app/core/routes/app_shell/tablet_shell_page.dart'; import 'package:tm_app/core/routes/app_shell/tablet_shell_page.dart';
import 'package:tm_app/view_models/home_view_model.dart'; import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/view_models/tenders_view_model.dart';
import 'package:tm_app/views/shared/responsive_builder.dart'; import 'package:tm_app/views/shared/responsive_builder.dart';
class AppShellScreen extends StatelessWidget { class AppShellScreen extends StatelessWidget {
@@ -16,13 +17,22 @@ class AppShellScreen extends StatelessWidget {
if (index == navigationShell.currentIndex) { if (index == navigationShell.currentIndex) {
return; return;
} }
// Navigate first
navigationShell.goBranch( navigationShell.goBranch(
index, index,
initialLocation: index == navigationShell.currentIndex, initialLocation: index == navigationShell.currentIndex,
); );
if (index == 0) {
context.read<HomeViewModel>().init(); // Then trigger data loading after navigation is complete
} WidgetsBinding.instance.addPostFrameCallback((_) {
if (index == 0) {
context.read<HomeViewModel>().init();
}
if (index == 1) {
context.read<TendersViewModel>().getTenders();
}
});
} }
@override @override
@@ -3,13 +3,14 @@ import 'package:tm_app/data/services/liked_tenders_service.dart';
import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart'; import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart';
class LikedTendersRepository { class LikedTendersRepository {
final LikedTendersService likedTendersService; final LikedTendersService _likedTendersService;
LikedTendersRepository({required this.likedTendersService}); LikedTendersRepository({required LikedTendersService likedTendersService})
: _likedTendersService = likedTendersService;
Future<Result<LikedTendersResponse>> getLikedTenders({ Future<Result<LikedTendersResponse>> getLikedTenders({
int offset = 0, int offset = 0,
int limit = 20, int limit = 20,
}) { }) {
return likedTendersService.getLikedTenders(offset: offset, limit: limit); return _likedTendersService.getLikedTenders(offset: offset, limit: limit);
} }
} }
+103 -73
View File
@@ -1,14 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/repositories/liked_tenders_repository.dart'; 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_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/liked_tenders_response/liked_tenders_response.dart';
import '../data/services/model/feedback_response/feedback_response.dart';
class LikedTendersViewModel with ChangeNotifier { class LikedTendersViewModel with ChangeNotifier {
final LikedTendersRepository likedTendersRepository; final LikedTendersRepository _likedTendersRepository;
LikedTendersViewModel({required this.likedTendersRepository}) { final TendersRepository _tendersRepository;
getLikedTenders(reset: true); LikedTendersViewModel({
} required LikedTendersRepository likedTendersRepository,
required TendersRepository tendersRepository,
}) : _likedTendersRepository = likedTendersRepository,
_tendersRepository = tendersRepository;
bool isLoading = false; bool isLoading = false;
bool isLoadingMore = false; bool isLoadingMore = false;
@@ -21,71 +27,70 @@ class LikedTendersViewModel with ChangeNotifier {
int currentPage = 1; int currentPage = 1;
Future<void> getLikedTenders({bool reset = false}) async { Future<void> getLikedTenders({bool reset = false}) async {
if (!reset && (isLoadingMore || !hasMore)) { if (!reset && (isLoadingMore || !hasMore)) {
return; return;
}
if (reset) {
_offset = (currentPage - 1) * _limit;
hasMore = true;
data = null;
isLoading = true;
notifyListeners();
} else {
if (_offset == 0) {
isLoading = true;
} else {
isLoadingMore = true;
} }
if (reset) {
_offset = (currentPage - 1) * _limit;
hasMore = true;
data = null;
isLoading = true;
notifyListeners();
} else {
if (_offset == 0) {
isLoading = true;
} else {
isLoadingMore = true;
}
notifyListeners();
}
errorMessage = null;
final result = await _likedTendersRepository.getLikedTenders(
offset: _offset,
limit: _limit,
);
switch (result) {
case Ok<LikedTendersResponse>():
final incoming = result.value;
final incomingList = incoming.data?.feedback ?? [];
if (reset) {
data = incoming;
} else {
final existingList = data?.data?.feedback ?? [];
final combined = List<LikedTenderFeedback>.of(existingList)
..addAll(incomingList);
data = data?.copyWith(
data: data?.data?.copyWith(
feedback: combined,
meta: incoming.data?.meta ?? data!.data!.meta,
),
);
}
hasMore = incomingList.length == _limit;
_offset += _limit;
errorMessage = null;
break;
case Error<LikedTendersResponse>():
errorMessage = result.error.toString();
break;
}
isLoading = false;
isLoadingMore = false;
notifyListeners(); notifyListeners();
} }
errorMessage = null;
final result = await likedTendersRepository.getLikedTenders(
offset: _offset,
limit: _limit,
);
switch (result) {
case Ok<LikedTendersResponse>():
final incoming = result.value;
final incomingList = incoming.data?.feedback ?? [];
if (reset) {
data = incoming;
} else {
final existingList = data?.data?.feedback ?? [];
final combined = List<LikedTenderFeedback>.of(existingList)
..addAll(incomingList);
data = data?.copyWith(
data: data?.data?.copyWith(
feedback: combined,
meta: incoming.data?.meta ?? data!.data!.meta,
),
);
}
hasMore = incomingList.length == _limit;
_offset += _limit;
errorMessage = null;
break;
case Error<LikedTendersResponse>():
errorMessage = result.error.toString();
break;
}
isLoading = false;
isLoadingMore = false;
notifyListeners();
}
Future<void> loadMoreIfNeeded() async { Future<void> loadMoreIfNeeded() async {
if (isLoadingMore || !hasMore) { if (isLoadingMore || !hasMore) {
return; return;
@@ -100,24 +105,49 @@ Future<void> getLikedTenders({bool reset = false}) async {
await getLikedTenders(reset: true); await getLikedTenders(reset: true);
} }
void removeTenderById(String id) { void removeTenderById(String id) async {
final current = data; final current = data;
final list = current?.data?.feedback; final list = current?.data?.feedback;
if (current == null || list == null) { if (current == null || list == null) {
return; return;
} }
// Store original list to restore if error occurs
final originalList = List<LikedTenderFeedback>.of(list);
final updatedList = List<LikedTenderFeedback>.of(list) final updatedList = List<LikedTenderFeedback>.of(list)
..removeWhere((e) => e.id == id); ..removeWhere((e) => e.tenderId == id);
final updatedMeta = current.data?.meta?.copyWith(total: updatedList.length);
// Temporarily update UI
data = current.copyWith( data = current.copyWith(
data: current.data?.copyWith( data: current.data?.copyWith(feedback: updatedList),
feedback: updatedList,
meta: updatedMeta,
),
); );
notifyListeners();
final result = await _tendersRepository.toggleFeedback(
tenderId: id,
feedbackType: 'like',
);
switch (result) {
case Ok<FeedbackResponse>():
final updatedMeta = current.data?.meta?.copyWith(
total: updatedList.length,
);
data = current.copyWith(
data: current.data?.copyWith(
feedback: updatedList,
meta: updatedMeta,
),
);
break;
case Error<FeedbackResponse>():
// Restore original list if error occurs
data = current.copyWith(
data: current.data?.copyWith(feedback: originalList),
);
errorMessage = result.error.toString();
break;
}
notifyListeners(); notifyListeners();
} }
+30 -1
View File
@@ -93,8 +93,8 @@ class TendersViewModel with ChangeNotifier {
} }
Future<void> getTenders() async { Future<void> getTenders() async {
clearAll(); // Clear all data before fetching new tenders
_isLoading = true; _isLoading = true;
_errorMessage = null;
_currentOffset = 0; _currentOffset = 0;
_hasMoreData = true; _hasMoreData = true;
notifyListeners(); notifyListeners();
@@ -466,4 +466,33 @@ class TendersViewModel with ChangeNotifier {
void updateHome() { void updateHome() {
_homeViewModel.init(); _homeViewModel.init();
} }
/// Clear all feedbacks
void clearFeedbacks() {
feedbacks.clear();
_feedbackData = null;
_feedbackErrorMessage = null;
notifyListeners();
}
/// Clear all tenders
void clearTenders() {
_tendersResponse = null;
_errorMessage = null;
_currentOffset = 0;
_hasMoreData = true;
currentPage = 1;
notifyListeners();
}
/// Clear all data (tenders, feedbacks, and approvals)
void clearAll() {
clearTenders();
clearFeedbacks();
tenderApprovals.clear();
_tenderApprovalData = null;
_tenderApprovalToggleData = null;
_tenderApprovalErrorMessage = null;
notifyListeners();
}
} }
+3 -1
View File
@@ -152,7 +152,9 @@ class MobileHomePage extends StatelessWidget {
textColor: AppColors.grey80, textColor: AppColors.grey80,
enableTap: true, enableTap: true,
onTap: () { onTap: () {
LikedTendersRouteData().push(context); LikedTendersRouteData().push(context).then((value) {
homeViewModel.init();
});
}, },
), ),
], ],
@@ -61,7 +61,9 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
final feedback = viewModel.data?.data?.feedback ?? []; final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) { if (feedback.isEmpty) {
return const Center(child: Text('Nothing to show')); return const Center(
child: Text('You havent liked any tenders yet.'),
);
} }
final currentPage = viewModel.currentPage; final currentPage = viewModel.currentPage;
@@ -85,7 +87,7 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: 20.0.h()), padding: EdgeInsets.only(bottom: 20.0.h()),
child: Dismissible( child: Dismissible(
key: ValueKey(item.id ?? index), key: ValueKey(item.tenderId ?? index),
direction: DismissDirection.endToStart, direction: DismissDirection.endToStart,
background: Container( background: Container(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@@ -97,8 +99,10 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
), ),
), ),
onDismissed: (_) { onDismissed: (_) {
if (item.id != null) { if (item.tenderId != null) {
viewModel.removeTenderById(item.id!); viewModel.removeTenderById(
item.tenderId!,
);
} }
}, },
child: LikedListItem(tender: item.tender!), child: LikedListItem(tender: item.tender!),
@@ -140,8 +144,11 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
final pageNumber = index + 1; final pageNumber = index + 1;
return ListTile( return ListTile(
title: Text('Page $pageNumber'), title: Text('Page $pageNumber'),
onTap: () => onTap:
Navigator.pop(context, pageNumber), () => Navigator.pop(
context,
pageNumber,
),
); );
}, },
), ),
@@ -31,7 +31,7 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
} }
final max = _scrollController.position.maxScrollExtent; final max = _scrollController.position.maxScrollExtent;
final pos = _scrollController.position.pixels; final pos = _scrollController.position.pixels;
const threshold = 200.0; const threshold = 200.0;
if (max - pos <= threshold) { if (max - pos <= threshold) {
final vm = Provider.of<LikedTendersViewModel>(context, listen: false); final vm = Provider.of<LikedTendersViewModel>(context, listen: false);
vm.loadMoreIfNeeded(); vm.loadMoreIfNeeded();
@@ -71,10 +71,13 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
final feedback = viewModel.data?.data?.feedback ?? []; final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) { if (feedback.isEmpty) {
return const Center(child: Text('Nothing to show')); return const Center(
child: Text('You havent liked any tenders yet.'),
);
} }
final itemCount = feedback.length + (viewModel.isLoadingMore ? 1 : 0); final itemCount =
feedback.length + (viewModel.isLoadingMore ? 1 : 0);
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
@@ -93,16 +96,18 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: 20.0.h()), padding: EdgeInsets.only(bottom: 20.0.h()),
child: Dismissible( child: Dismissible(
key: ValueKey(item.id ?? index), key: ValueKey(item.tenderId ?? index),
direction: DismissDirection.endToStart, direction: DismissDirection.endToStart,
background: Container( background: Container(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 20.0.w()), padding: EdgeInsets.symmetric(
horizontal: 20.0.w(),
),
child: SvgPicture.asset(AssetsManager.trash), child: SvgPicture.asset(AssetsManager.trash),
), ),
onDismissed: (_) { onDismissed: (_) {
if (item.id != null) { if (item.tenderId != null) {
viewModel.removeTenderById(item.id!); viewModel.removeTenderById(item.tenderId!);
} }
}, },
child: LikedListItem(tender: item.tender!), child: LikedListItem(tender: item.tender!),
@@ -1,13 +1,31 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/views/shared/responsive_builder.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/liked_tenders_view_model.dart';
import 'package:tm_app/views/liked_tenders/pages/liked_tenders_desktop_page.dart'; import 'package:tm_app/views/liked_tenders/pages/liked_tenders_desktop_page.dart';
import 'package:tm_app/views/liked_tenders/pages/liked_tenders_mobile_page.dart'; import 'package:tm_app/views/liked_tenders/pages/liked_tenders_mobile_page.dart';
import 'package:tm_app/views/liked_tenders/pages/liked_tenders_tablet_page.dart'; import 'package:tm_app/views/liked_tenders/pages/liked_tenders_tablet_page.dart';
import 'package:tm_app/views/shared/responsive_builder.dart';
class LikedTendersScreen extends StatelessWidget { class LikedTendersScreen extends StatefulWidget {
const LikedTendersScreen({super.key}); const LikedTendersScreen({super.key});
@override
State<LikedTendersScreen> createState() => _LikedTendersScreenState();
}
class _LikedTendersScreenState extends State<LikedTendersScreen> {
late final LikedTendersViewModel viewModel;
@override
void initState() {
viewModel = context.read<LikedTendersViewModel>();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
viewModel.getLikedTenders(reset: true);
});
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SizeConfig.init(context); SizeConfig.init(context);
@@ -68,7 +68,9 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
final feedback = viewModel.data?.data?.feedback ?? []; final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) { if (feedback.isEmpty) {
return const Center(child: Text('Nothing to show')); return const Center(
child: Text('You havent liked any tenders yet.'),
);
} }
final currentPage = viewModel.currentPage; final currentPage = viewModel.currentPage;
@@ -92,7 +94,7 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: 20.0.h()), padding: EdgeInsets.only(bottom: 20.0.h()),
child: Dismissible( child: Dismissible(
key: ValueKey(item.id ?? index), key: ValueKey(item.tenderId ?? index),
direction: DismissDirection.endToStart, direction: DismissDirection.endToStart,
background: Container( background: Container(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@@ -104,8 +106,10 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
), ),
), ),
onDismissed: (_) { onDismissed: (_) {
if (item.id != null) { if (item.tenderId != null) {
viewModel.removeTenderById(item.id!); viewModel.removeTenderById(
item.tenderId!,
);
} }
}, },
child: LikedListItem(tender: item.tender!), child: LikedListItem(tender: item.tender!),