improvements

ui_ux_rollout_merge_notification_settings
WaseemAbbasi22 13 hours ago
parent afc1e9c77a
commit 37a2be7b78

@ -163,7 +163,7 @@ class NotificationsProvider extends ChangeNotifier {
/// Returns true if successful, false otherwise /// Returns true if successful, false otherwise
Future<bool> markOneAsRead({required String notificationId}) async { Future<bool> markOneAsRead({required String notificationId}) async {
try { try {
final response = await ApiManager.instance.get("${URLs.markOneNotificationAsRead}?NotificationId=$notificationId"); final response = await ApiManager.instance.get("${URLs.markOneNotificationAsRead}?NotificationId=$notificationId", enableToastMessage: false);
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {
// Update the specific notification in the local list to read status // Update the specific notification in the local list to read status

@ -12,6 +12,7 @@ class SystemNotificationModel {
String? createdOn; String? createdOn;
String? modifiedOn; String? modifiedOn;
String? priorityName; String? priorityName;
int? priorityValue;
String? statusName; String? statusName;
int? transactionType; int? transactionType;
String? moduleName; String? moduleName;
@ -38,6 +39,7 @@ class SystemNotificationModel {
this.createdOn, this.createdOn,
this.modifiedOn, this.modifiedOn,
this.priorityName, this.priorityName,
this.priorityValue,
this.transactionType, this.transactionType,
this.statusName, this.statusName,
this.moduleName, this.moduleName,
@ -65,6 +67,7 @@ class SystemNotificationModel {
createdOn = json['createdDate'] ?? json['createdOn']; createdOn = json['createdDate'] ?? json['createdOn'];
modifiedOn = json['modifiedOn']; modifiedOn = json['modifiedOn'];
priorityName = json['priorityName']; priorityName = json['priorityName'];
priorityValue = json['priorityValue'];
statusName = json['statusName']; statusName = json['statusName'];
transactionType = json['transactionType']; transactionType = json['transactionType'];
moduleName = json['moduleName']; moduleName = json['moduleName'];
@ -94,6 +97,7 @@ class SystemNotificationModel {
data['createdOn'] = createdOn; data['createdOn'] = createdOn;
data['modifiedOn'] = modifiedOn; data['modifiedOn'] = modifiedOn;
data['priorityName'] = priorityName; data['priorityName'] = priorityName;
data['priorityValue'] = priorityValue;
data['priority'] = priorityName; data['priority'] = priorityName;
data['statusName'] = statusName; data['statusName'] = statusName;
data['transactionType'] = transactionType; data['transactionType'] = transactionType;
@ -120,6 +124,7 @@ class SystemNotificationModel {
data['createdOn'] = createdOn; data['createdOn'] = createdOn;
data['modifiedOn'] = modifiedOn; data['modifiedOn'] = modifiedOn;
data['priorityName'] = priorityName; data['priorityName'] = priorityName;
data['priorityValue'] = priorityValue;
data['statusName'] = statusName; data['statusName'] = statusName;
data['transactionType'] = transactionType; data['transactionType'] = transactionType;
data['moduleName'] = moduleName; data['moduleName'] = moduleName;

@ -30,14 +30,14 @@ class NotificationPreferenceCard extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4.toScreenHeight,
children: [ children: [
if(isMandatory) ...[ if(isMandatory) ...[
4.height, 4.height,
Text( Text(
'You cannot update this notification as it is mandatory.'.addTranslation, 'You cannot update this notification as it is mandatory.'.addTranslation,
style: AppTextStyle.body2.copyWith( style: AppTextStyle.tiny.copyWith(
color: AppColor.red30, color: AppColor.red30,
fontSize: 10.toScreenWidth,
), ),
), ),
], ],
@ -54,27 +54,23 @@ class NotificationPreferenceCard extends StatelessWidget {
_buildCompactToggle(context, isEnabled, isMandatory), _buildCompactToggle(context, isEnabled, isMandatory),
], ],
), ),
4.height,
InfoTextWidget( InfoTextWidget(
label: context.translation.notificationModule, label: context.translation.notificationModule,
value: template.moduleName ?? '', value: template.moduleName ?? '',
), ),
4.height,
InfoTextWidget( InfoTextWidget(
label: 'Sub Module'.addTranslation, label: 'Sub Module'.addTranslation,
value: template.subModuleName ?? '', value: template.subModuleName ?? '',
), ),
4.height,
if (template.templateChannels != null && template.templateChannels!.isNotEmpty) ...[ if (template.templateChannels != null && template.templateChannels!.isNotEmpty) ...[
InfoTextLabelWidget( InfoTextLabelWidget(
label: '${context.translation.channelPreference} :', label: '${context.translation.channelPreference} :',
), ),
8.height,
_buildChannelChips(context, isMandatory), _buildChannelChips(context, isMandatory),
], ],
], ],
).toShadowContainer(context,padding: 16); ).toShadowContainer(context);
} }
Widget _buildCompactToggle(BuildContext context, bool isEnabled, bool isMandatory) { Widget _buildCompactToggle(BuildContext context, bool isEnabled, bool isMandatory) {

@ -418,6 +418,42 @@ class AppColor {
} }
} }
//0 critical, 1 high, 2 low, 3 medium,
static Color getNotificationPriorityStatusTextColor(BuildContext context, int id) {
switch (id) {
//low priority
case 2:
return greenStatusColor;
case 3:
return greenStatusTextColor;
case 0:
return redStatusTextColor;
case 1:
return redStatusTextColor;
default:
return defaultStatusTextColor(context);
}
}
static Color getNotificationPriorityStatusColor(BuildContext context, int id) {
switch (id) {
//low priority
case 2:
return greenStatusColor;
case 0:
return redStatusColor;
//high priority
case 1:
return redStatusColor;
case 3:
return greenStatusColor;
default:
return defaultStatusColor(context);
}
}
static Color getPriorityStatusColor(BuildContext context, int id) { static Color getPriorityStatusColor(BuildContext context, int id) {
switch (id) { switch (id) {
//low priority //low priority

@ -98,7 +98,7 @@ class _NotificationFilterPageState extends State<NotificationFilterPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColor.scaffold2Background(context), // backgroundColor: AppColor.scaffold2Background(context),
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: context.translation.filter, title: context.translation.filter,
actions: [ actions: [
@ -126,17 +126,16 @@ class _NotificationFilterPageState extends State<NotificationFilterPage> {
body: Column( body: Column(
children: [ children: [
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Filter by Priority
InfoHeader16Widget("Priority"), InfoHeader16Widget("Priority"),
8.height, 8.height,
SingleItemDropDownMenu<Lookup, NotificationPriorityLookupProvider>( SingleItemDropDownMenu<Lookup, NotificationPriorityLookupProvider>(
context: context, context: context,
showAsBottomSheet: true, showAsFullScreenDialog: true,
showShadow: false, showShadow: true,
title: "Select Priority", title: "Select Priority",
initialValue: selectedPriority, initialValue: selectedPriority,
onSelect: (value) { onSelect: (value) {
@ -151,15 +150,15 @@ class _NotificationFilterPageState extends State<NotificationFilterPage> {
}, },
), ),
16.height, 12.height,
// Filter by Module // Filter by Module
InfoHeader16Widget("Module"), InfoHeader16Widget("Module"),
8.height, 8.height,
SingleItemDropDownMenu<ModuleWithSubModulesModel, ModulesWithSubModulesProvider>( SingleItemDropDownMenu<ModuleWithSubModulesModel, ModulesWithSubModulesProvider>(
context: context, context: context,
showAsBottomSheet: true, showAsFullScreenDialog: true,
showShadow: false, showShadow: true,
title: "Select Module", title: "Select Module",
initialValue: selectedModule, initialValue: selectedModule,
onSelect: (value) { onSelect: (value) {
@ -177,15 +176,15 @@ class _NotificationFilterPageState extends State<NotificationFilterPage> {
}, },
), ),
16.height, 12.height,
// Filter by SubModule // Filter by SubModule
InfoHeader16Widget( "SubModule"), InfoHeader16Widget("SubModule"),
8.height, 8.height,
SingleItemDropDownMenu<SubModuleModel, NullableLoadingProvider>( SingleItemDropDownMenu<SubModuleModel, NullableLoadingProvider>(
context: context, context: context,
showAsBottomSheet: true, showAsFullScreenDialog: true,
showShadow: false, showShadow: true,
title: "Select SubModule", title: "Select SubModule",
initialValue: selectedSubModule, initialValue: selectedSubModule,
enabled: selectedModule?.subModules?.isNotEmpty ?? false, enabled: selectedModule?.subModules?.isNotEmpty ?? false,
@ -202,10 +201,10 @@ class _NotificationFilterPageState extends State<NotificationFilterPage> {
}, },
), ),
16.height, 12.height,
// Date Range Filter // Date Range Filter
context.translation.pickDate.heading5(context).custom(color: AppColor.textStyleColor(context)), InfoHeader16Widget(context.translation.pickDate),
8.height, 8.height,
Row( Row(
children: [ children: [
@ -239,7 +238,6 @@ class _NotificationFilterPageState extends State<NotificationFilterPage> {
], ],
), ),
).expanded, ).expanded,
16.height,
FooterActionButton.footerContainer( FooterActionButton.footerContainer(
context: context, context: context,
child: AppFilledButton( child: AppFilledButton(

@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import 'package:test_sa/controllers/notification/firebase_notification_manger.dart'; import 'package:test_sa/controllers/notification/firebase_notification_manger.dart';
import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; import 'package:test_sa/controllers/providers/api/notifications_provider.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/system_notification_model.dart'; import 'package:test_sa/models/system_notification_model.dart';
import 'package:test_sa/views/pages/user/notifications/widgets/notification_card_redesigned.dart'; import 'package:test_sa/views/pages/user/notifications/widgets/notification_card_redesigned.dart';
@ -27,22 +28,17 @@ class NotificationsList extends StatelessWidget {
nextPage: nextPage, nextPage: nextPage,
onLazyLoad: onLazyLoad, onLazyLoad: onLazyLoad,
child: ListView.separated( child: ListView.separated(
// padding: const EdgeInsets.all(16),
itemCount: notifications.length, itemCount: notifications.length,
separatorBuilder: (context, itemIndex) => const SizedBox(height: 12), separatorBuilder: (context, itemIndex) => 12.height,
itemBuilder: (context, itemIndex) { itemBuilder: (context, itemIndex) {
final notification = notifications[itemIndex]; final notification = notifications[itemIndex];
return NotificationCardRedesigned( return NotificationCardRedesigned(
notification: notification, notification: notification,
onTap: () async { onTap: () async {
// Mark notification as read when tapped
log('i am tapped with id ${notification.toJson()}');
if (notification.id != null && !(notification.readed ?? false)) { if (notification.id != null && !(notification.readed ?? false)) {
final provider = Provider.of<NotificationsProvider>(context, listen: false); final provider = Provider.of<NotificationsProvider>(context, listen: false);
await provider.markOneAsRead(notificationId: notification.id!); await provider.markOneAsRead(notificationId: notification.id!);
} }
// Navigate to notification details
FirebaseNotificationManger.handleMessage(context, notification.toNotificationJson()); FirebaseNotificationManger.handleMessage(context, notification.toNotificationJson());
}, },
); );

@ -16,7 +16,6 @@ import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/pages/user/notifications/notification_filter_page.dart'; import 'package:test_sa/views/pages/user/notifications/notification_filter_page.dart';
import 'package:test_sa/views/pages/user/notifications/notifications_list.dart'; import 'package:test_sa/views/pages/user/notifications/notifications_list.dart';
import 'package:test_sa/views/pages/user/notifications/widgets/notification_tab_button.dart'; import 'package:test_sa/views/pages/user/notifications/widgets/notification_tab_button.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'dart:async'; import 'dart:async';
import '../../../widgets/loaders/loading_manager.dart'; import '../../../widgets/loaders/loading_manager.dart';
@ -80,7 +79,6 @@ class _NotificationsPageState extends State<NotificationsPage> with TickerProvid
); );
} }
/// Handle search with debounce
void _onSearchChanged(String value) { void _onSearchChanged(String value) {
setState(() { setState(() {
_searchQuery = value; _searchQuery = value;
@ -141,7 +139,6 @@ class _NotificationsPageState extends State<NotificationsPage> with TickerProvid
_userProvider = Provider.of<UserProvider>(context); _userProvider = Provider.of<UserProvider>(context);
return Scaffold( return Scaffold(
backgroundColor: AppColor.scaffold2Background(context),
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: context.translation.notifications, title: context.translation.notifications,
actions: [ actions: [
@ -181,37 +178,26 @@ class _NotificationsPageState extends State<NotificationsPage> with TickerProvid
), ),
body: SafeArea( body: SafeArea(
child: Column( child: Column(
spacing: 12.toScreenHeight,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
// Search Section 12.height,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
8.height,
AppTextFormField( AppTextFormField(
controller: _searchController, controller: _searchController,
labelText: context.translation.search, labelText: context.translation.search,
showShadow: false, showShadow: false,
showBorder: true, showBorder: true,
prefixIconData: Icons.search, prefixIconData: Icons.search,
hintText: 'Search by Title or content'.addTranslation,
hintStyle: AppTextStyles.tinyFont.copyWith(color: AppColor.labelTextStyleColor(context)),
contentPadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
onChange: (value) { onChange: (value) {
_onSearchChanged(value); _onSearchChanged(value);
}, },
), ),
], // 8.height,
),
16.height,
// Notification Tabs
Row( Row(
children: [ children: [
Expanded( Expanded(
child: SizedBox( child: SizedBox(
height: 35.toScreenHeight, height: 30.toScreenHeight,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: 3, itemCount: 3,
@ -238,9 +224,10 @@ class _NotificationsPageState extends State<NotificationsPage> with TickerProvid
8.width, 8.width,
// Sort & Filter Container // Sort & Filter Container
Container( Container(
height: 35.toScreenHeight, height: 30.toScreenHeight,
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 14, offset: const Offset(0, 0), spreadRadius: 0)],
color: AppColor.background(context), color: AppColor.background(context),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppColor.borderColor(context)), border: Border.all(color: AppColor.borderColor(context)),
@ -287,7 +274,7 @@ class _NotificationsPageState extends State<NotificationsPage> with TickerProvid
_loadNotifications(); _loadNotifications();
} }
}), }),
// Active Filters Badge // Active Filters Badge we are not using custom badge widget here as it need more customizations
if (_activeFilterCount > 0) if (_activeFilterCount > 0)
Positioned( Positioned(
right: -8, right: -8,
@ -321,15 +308,12 @@ class _NotificationsPageState extends State<NotificationsPage> with TickerProvid
), ),
], ],
), ),
// 8.height,
16.height,
// Notifications List // Notifications List
Expanded( Expanded(
child: LoadingManager( child: LoadingManager(
isLoading: (_notificationsProvider.isLoading) && _notificationsProvider.notifications.isEmpty, isLoading: (_notificationsProvider.isLoading) && _notificationsProvider.notifications.isEmpty,
isFailedLoading: false, isFailedLoading: false,
// Don't show error state, show no data found instead
stateCode: _notificationsProvider.stateCode, stateCode: _notificationsProvider.stateCode,
onRefresh: () async { onRefresh: () async {
_notificationsProvider.reset(); _notificationsProvider.reset();
@ -352,7 +336,7 @@ class _NotificationsPageState extends State<NotificationsPage> with TickerProvid
), ),
), ),
], ],
).paddingOnly(start: 16, end: 16), ).paddingOnly(start: 12, end: 12),
), ),
); );
} }

@ -1,11 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/system_notification_model.dart'; import 'package:test_sa/models/system_notification_model.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/item_views/info_date_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_date_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class NotificationCardRedesigned extends StatelessWidget { class NotificationCardRedesigned extends StatelessWidget {
final SystemNotificationModel notification; final SystemNotificationModel notification;
@ -20,58 +23,26 @@ class NotificationCardRedesigned extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isUnread = !(notification.readed ?? false); final isUnread = !(notification.readed ?? false);
return Container( return Column(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.background(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.borderColor(context)),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4.toScreenHeight,
children: [ children: [
// Priority Badge & Date Row
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// Priority Badge if (notification.priorityName != null && notification.priorityValue != null)
if (notification.priorityName != null && notification.priorityName!.isNotEmpty) StatusLabel(
Container( label: notification.priorityName!,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), radius: 4,
decoration: BoxDecoration( textColor: AppColor.getNotificationPriorityStatusTextColor(context, notification.priorityValue!),
color: AppColor.getRequestStatusColorByName( backgroundColor: AppColor.getNotificationPriorityStatusColor(context, notification.priorityValue!),
context, ),
notification.priorityName!, if (notification.createdOn?.isNotEmpty ?? false) InfoDateWidget(notification.createdOn ?? ""),
),
borderRadius: BorderRadius.circular(4),
),
child: Text(
notification.priorityName!,
style: AppTextStyles.tinyFont2.copyWith(
color: AppColor.getRequestStatusTextColorByName(
context,
notification.priorityName!,
),
fontWeight: FontWeight.w600,
),
),
),
const Spacer(),
// Date & Time - Top Right
if (notification.createdOn?.isNotEmpty ?? false)
InfoDateWidget(notification.createdOn ?? ""),
], ],
), ),
8.height,
// Title with Unread Dot
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Unread indicator (blue dot) - aligned with title
if (isUnread) if (isUnread)
Padding( Padding(
padding: const EdgeInsets.only(top: 6, right: 8), padding: const EdgeInsets.only(top: 6, right: 8),
@ -84,8 +55,6 @@ class NotificationCardRedesigned extends StatelessWidget {
), ),
), ),
), ),
// Title
Expanded( Expanded(
child: InfoHeader16Widget( child: InfoHeader16Widget(
notification.title ?? '', notification.title ?? '',
@ -93,39 +62,16 @@ class NotificationCardRedesigned extends StatelessWidget {
), ),
], ],
), ),
8.height,
if (notification.sourceName?.isNotEmpty ?? false) if (notification.sourceName?.isNotEmpty ?? false)
Container( InfoTextWidget(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), label: 'Source'.addTranslation,
decoration: BoxDecoration( value: notification.sourceName ?? '',
color: AppColor.primary10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
notification.sourceName!,
style: AppTextStyles.tinyFont2.copyWith(
color: AppColor.primary10,
fontWeight: FontWeight.w500,
),
),
), ),
if (notification.sourceName?.isNotEmpty ?? false) 8.height,
// Message Preview
if (notification.text?.isNotEmpty ?? false) if (notification.text?.isNotEmpty ?? false)
InfoTextLabelWidget( InfoTextLabelWidget(
label: notification.text!, label: notification.text!,
// style: AppTextStyles.bodyText2.copyWith(
// color: AppColor.textColor(context),
// height: 1.4,
// ),
// maxLines: 3,
// overflow: TextOverflow.ellipsis,
), ),
], ],
), ).toShadowContainer(context).onPress(onTap);
),
);
} }
} }

@ -22,7 +22,7 @@ class NotificationTabButton extends StatelessWidget {
onTap: onTap, onTap: onTap,
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: ShapeDecoration( decoration: ShapeDecoration(
color: AppColor.background(context), color: AppColor.background(context),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(

Loading…
Cancel
Save