merge with conflict

This commit is contained in:
amirrezaghabeli
2025-09-25 12:48:32 +03:30
parent 9d22bccef0
commit e49c12b6cc
8 changed files with 130 additions and 39 deletions
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/views/profile/strings/profile_strings.dart'; import 'package:tm_app/views/profile/strings/profile_strings.dart';
import '../../../views/home/strings/home_strings.dart'; import '../../../views/home/strings/home_strings.dart';
@@ -63,6 +65,7 @@ class MobileShellPage extends StatelessWidget {
onTap: () => onTap(3), onTap: () => onTap(3),
iconPath: AssetsManager.notify, iconPath: AssetsManager.notify,
activeIconPath: AssetsManager.notifyActive, activeIconPath: AssetsManager.notifyActive,
showBadge: context.watch<HomeViewModel>().hasUnreadNotification,
), ),
_bottomNavigationItem( _bottomNavigationItem(
context: context, context: context,
@@ -85,6 +88,7 @@ class MobileShellPage extends StatelessWidget {
required VoidCallback onTap, required VoidCallback onTap,
required String iconPath, required String iconPath,
required String activeIconPath, required String activeIconPath,
bool showBadge = false,
}) { }) {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
@@ -96,8 +100,25 @@ class MobileShellPage extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SvgPicture.asset(isActive ? activeIconPath : iconPath), Stack(
clipBehavior: Clip.none,
children: [
SvgPicture.asset(isActive ? activeIconPath : iconPath),
if (showBadge)
Positioned(
right: -5,
top: -2,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
),
Text( Text(
text, text,
maxLines: 1, maxLines: 1,
@@ -37,4 +37,8 @@ class HomeRepository {
Future<Result<FeedbackStatResponse>> getFeedbackStats() { Future<Result<FeedbackStatResponse>> getFeedbackStats() {
return _homeService.getFeedbackStats(); return _homeService.getFeedbackStats();
} }
Future<Result<Map<String, dynamic>>> checkUnreadNotifications() async {
return _homeService.checkUnreadNotifications();
}
} }
+2 -4
View File
@@ -9,13 +9,11 @@ class HomeApi {
static const String tenderApprovalsBase = '/api/v1/tender-approvals'; static const String tenderApprovalsBase = '/api/v1/tender-approvals';
static String getYourTenders({ static String getYourTenders({required int limit, required int offset}) {
required int limit,
required int offset,
}) {
return '$tenderApprovalsBase?limit=$limit&offset=$offset'; return '$tenderApprovalsBase?limit=$limit&offset=$offset';
} }
static const String tenderApprovalStats = '/api/v1/tender-approvals/stats'; static const String tenderApprovalStats = '/api/v1/tender-approvals/stats';
static const String statsCompany = '/api/v1/feedback/stats/company'; static const String statsCompany = '/api/v1/feedback/stats/company';
static const String checkUnreadNotifications = '/api/v1/notifications?seen=false&limit=1';
} }
+9
View File
@@ -41,4 +41,13 @@ class HomeService {
); );
return result; return result;
} }
Future<Result<Map<String, dynamic>>> checkUnreadNotifications() async {
final result = await _networkManager.makeRequest(
HomeApi.checkUnreadNotifications,
(json) => json,
method: 'GET',
);
return result;
}
} }
+18
View File
@@ -44,6 +44,9 @@ class HomeViewModel with ChangeNotifier {
bool get hasMore => _hasMore; bool get hasMore => _hasMore;
bool get isRecommendedMode => _isRecommendedMode; bool get isRecommendedMode => _isRecommendedMode;
bool _hasUnreadNotification = false;
bool get hasUnreadNotification => _hasUnreadNotification;
void init() async { void init() async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
@@ -51,6 +54,7 @@ class HomeViewModel with ChangeNotifier {
await getApprovedStates(); await getApprovedStates();
await getYourTenders(reset: true); await getYourTenders(reset: true);
await getFeedbackStats(); await getFeedbackStats();
await checkUnreadNotifications();
if (_errorMessage != null) { if (_errorMessage != null) {
_isLoading = false; _isLoading = false;
@@ -206,4 +210,18 @@ class HomeViewModel with ChangeNotifier {
double get selfApplyPercent { double get selfApplyPercent {
return totalCount > 0 ? (selfApplyCount / totalCount) * 100 : 0; return totalCount > 0 ? (selfApplyCount / totalCount) * 100 : 0;
} }
Future<void> checkUnreadNotifications() async {
final result = await _homeRepository.checkUnreadNotifications();
switch (result) {
case Ok<Map<String, dynamic>>():
final response = result.value;
_hasUnreadNotification = response['success'] == true;
break;
case Error<Map<String, dynamic>>():
_hasUnreadNotification = false;
break;
}
notifyListeners();
}
} }
+2 -2
View File
@@ -45,9 +45,9 @@ class NotificationViewModel with ChangeNotifier {
/// Fetch notifications /// Fetch notifications
Future<void> getNotifications({int page = 1, bool isMobile = false}) async { Future<void> getNotifications({int page = 1, bool isMobile = false}) async {
if (isMobile && page > 1) { if (isMobile && page > 1) {
_isMoreLoading = true; // 👈 لودینگ فقط برای پیج بعدی _isMoreLoading = true;
} else { } else {
_isLoading = true; // 👈 لودینگ عادی (اولین بار یا رفرش) _isLoading = true;
} }
notifyListeners(); notifyListeners();
+51 -30
View File
@@ -148,36 +148,57 @@ class DesktopNavigationWidget extends StatelessWidget {
); );
} }
Widget _navigationItem({ Widget _navigationItem({
required BuildContext context, required BuildContext context,
required String text, required String text,
required bool isActive, required bool isActive,
required VoidCallback onTap, required VoidCallback onTap,
required String iconPath, required String iconPath,
required String activeIconPath, required String activeIconPath,
}) { }) {
return InkWell( final hasUnread = context.watch<HomeViewModel>().hasUnreadNotification;
onTap: onTap,
child: SizedBox( return InkWell(
width: 115.0, onTap: onTap,
height: 64, child: SizedBox(
child: Row( width: 115.0,
mainAxisAlignment: MainAxisAlignment.center, height: 64,
crossAxisAlignment: CrossAxisAlignment.center, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
SvgPicture.asset(isActive ? activeIconPath : iconPath), crossAxisAlignment: CrossAxisAlignment.center,
const SizedBox(width: 8), children: [
Text( Stack(
text, clipBehavior: Clip.none,
style: TextStyle( children: [
fontSize: 12.0.sp(), SvgPicture.asset(isActive ? activeIconPath : iconPath),
fontWeight: FontWeight.w400, if (text == TendersStrings.notifications && hasUnread)
color: isActive ? AppColors.primaryColor : AppColors.grey60, Positioned(
), right: -5,
top: -2,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
),
const SizedBox(width: 8),
Text(
text,
style: TextStyle(
fontSize: 12.0.sp(),
fontWeight: FontWeight.w400,
color: isActive ? AppColors.primaryColor : AppColors.grey60,
), ),
], ),
), ],
), ),
); ),
} );
}
} }
+21 -1
View File
@@ -129,6 +129,8 @@ class TabletNavigationWidget extends StatelessWidget {
required String iconPath, required String iconPath,
required String activeIconPath, required String activeIconPath,
}) { }) {
final hasUnread = context.watch<HomeViewModel>().hasUnreadNotification;
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
@@ -139,7 +141,25 @@ class TabletNavigationWidget extends StatelessWidget {
padding: EdgeInsetsDirectional.only(start: 24.0.w()), padding: EdgeInsetsDirectional.only(start: 24.0.w()),
child: Row( child: Row(
children: [ children: [
SvgPicture.asset(isActive ? activeIconPath : iconPath), Stack(
clipBehavior: Clip.none,
children: [
SvgPicture.asset(isActive ? activeIconPath : iconPath),
if (text == ProfileStrings.notifications && hasUnread)
Positioned(
right: -5,
top: -2,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
),
SizedBox(width: 12.0.w()), SizedBox(width: 12.0.w()),
Text( Text(
text, text,