merge with conflict
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user