Merge pull request 'merge with conflict' (#166) from merge into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/166
This commit is contained in:
a.ghabeli
2025-09-25 12:49:17 +03:30
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,
@@ -95,9 +99,26 @@ class MobileShellPage extends StatelessWidget {
padding: EdgeInsets.only(top: 12.0.h(), bottom: 14.0.h()), padding: EdgeInsets.only(top: 12.0.h(), bottom: 14.0.h()),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Stack(
clipBehavior: Clip.none,
children: [ children: [
SvgPicture.asset(isActive ? activeIconPath : iconPath), 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();
@@ -156,6 +156,8 @@ class DesktopNavigationWidget 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: SizedBox( child: SizedBox(
@@ -164,8 +166,26 @@ class DesktopNavigationWidget extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(
clipBehavior: Clip.none,
children: [ children: [
SvgPicture.asset(isActive ? activeIconPath : iconPath), SvgPicture.asset(isActive ? activeIconPath : iconPath),
if (text == TendersStrings.notifications && hasUnread)
Positioned(
right: -5,
top: -2,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
text, text,
@@ -180,4 +200,5 @@ class DesktopNavigationWidget extends StatelessWidget {
), ),
); );
} }
} }
@@ -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(
@@ -138,8 +140,26 @@ class TabletNavigationWidget extends StatelessWidget {
child: Padding( child: Padding(
padding: EdgeInsetsDirectional.only(start: 24.0.w()), padding: EdgeInsetsDirectional.only(start: 24.0.w()),
child: Row( child: Row(
children: [
Stack(
clipBehavior: Clip.none,
children: [ children: [
SvgPicture.asset(isActive ? activeIconPath : iconPath), 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,