notifications apis integrated
parent
65335dca87
commit
9ab4ed6964
@ -0,0 +1,5 @@
|
||||
<svg width="14" height="9" viewBox="0 0 14 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.2983 1.2631C10.6276 1.09844 10.7611 0.697989 10.5965 0.36867C10.4318 0.0393513 10.0313 -0.0941316 9.70202 0.0705279C8.12823 0.857424 6.54486 2.42431 5.28996 3.87608C5.04918 4.15463 5.07981 4.57563 5.35836 4.8164C5.63691 5.05718 6.05791 5.02656 6.29868 4.74801C7.536 3.31658 8.9753 1.9246 10.2983 1.2631Z" fill="#3DA5E5"/>
|
||||
<path d="M0.937592 4.94649C0.601136 4.79696 0.207161 4.94849 0.0576253 5.28494C-0.0904934 5.61821 0.0567727 6.00791 0.386557 6.16059C0.388304 6.16147 0.391922 6.16332 0.397355 6.16623C0.41272 6.17447 0.442691 6.19128 0.485999 6.21884C0.572533 6.2739 0.712934 6.3723 0.896879 6.53172C1.2645 6.85032 1.80902 7.41527 2.44547 8.36995C2.5726 8.56065 2.78878 8.67267 3.01789 8.66658C3.24698 8.66049 3.4572 8.53661 3.57403 8.33946L3.577 8.3345L3.5909 8.31156C3.60369 8.29058 3.62348 8.25844 3.65012 8.21608C3.70339 8.13135 3.78397 8.00585 3.89053 7.84726C4.09588 7.54165 4.01459 7.12744 3.70898 6.92209C3.49769 6.78012 3.23449 6.77516 3.02429 6.88662C2.5429 6.25821 2.11389 5.82206 1.77012 5.52413C1.5374 5.32244 1.34447 5.18472 1.20183 5.09395C1.0989 5.02536 0.982785 4.96707 0.937592 4.94649Z" fill="#3DA5E5"/>
|
||||
<path d="M13.6316 1.2631C13.961 1.09844 14.0944 0.697989 13.9298 0.36867C13.7651 0.0393513 13.3647 -0.0941316 13.0354 0.0705279C11.2311 0.972653 9.40453 2.90685 8.06812 4.5356C7.38925 5.36297 6.81727 6.13626 6.41498 6.70264C6.38209 6.74895 6.35032 6.79389 6.31971 6.83738C5.8537 6.23534 5.43817 5.81422 5.10345 5.52413C4.87073 5.32244 4.6778 5.18472 4.53517 5.09395C4.45256 5.03938 4.32459 4.97291 4.27093 4.94649C3.93447 4.79696 3.54049 4.94849 3.39096 5.28494C3.24284 5.61821 3.39011 6.00791 3.71989 6.16059C3.72164 6.16147 3.72526 6.16332 3.73069 6.16623C3.74605 6.17447 3.77602 6.19128 3.81933 6.21884C3.90587 6.2739 4.04627 6.3723 4.23021 6.53172C4.59783 6.85032 5.14235 7.41527 5.7788 8.36995C5.90334 8.55676 6.11355 8.66833 6.33807 8.6668C6.56255 8.66526 6.7715 8.55034 6.89348 8.3619L6.89508 8.35944L6.9022 8.34852L6.93185 8.3034C6.95834 8.26328 6.9981 8.20348 7.05015 8.12634C7.15429 7.97205 7.30751 7.74858 7.50202 7.47474C7.8914 6.92653 8.44442 6.17899 9.09888 5.38136C10.4291 3.76011 12.1026 2.02764 13.6316 1.2631Z" fill="#3DA5E5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="21" height="23" viewBox="0 0 21 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0125 1.44721C11.0932 1.60832 10.9314 1.8355 10.6079 2.28986C9.91025 3.26945 9.5 4.46755 9.5 5.76138C9.5 9.07218 12.1863 11.7561 15.5 11.7561C16.0799 11.7561 16.6405 11.6739 17.171 11.5206C17.7329 11.3581 18.0138 11.2769 18.1632 11.328C18.3126 11.3792 18.4902 11.6231 18.8454 12.111L18.8953 12.1794C19.0559 12.3997 19.2328 12.6424 19.3697 12.8561C19.6255 13.2556 19.8754 13.7373 19.9613 14.2985C20.2416 16.1297 18.9494 17.3153 17.6625 17.8466C16.8262 18.1919 15.931 18.4734 14.9988 18.6913C15.0049 18.7949 14.9948 18.9013 14.9667 19.0071C14.3896 21.1786 12.3697 22.7502 10.0002 22.7502C7.63084 22.7502 5.61087 21.1786 5.03379 19.0071C5.00568 18.9013 4.99562 18.795 5.00171 18.6914C4.06928 18.4735 3.17396 18.1919 2.33746 17.8466C1.05056 17.3153 -0.241638 16.1297 0.0386759 14.2985C0.124594 13.7373 0.374521 13.2556 0.630335 12.8561C0.775277 12.6298 0.966356 12.3685 1.13291 12.1407C1.76864 11.2713 1.92776 10.3598 1.92786 9.28573C1.92789 4.85418 5.54419 1.26583 10 1.26583C10.0497 1.26583 10.1105 1.26264 10.1775 1.25912C10.4766 1.24343 10.8993 1.22125 11.0125 1.44721ZM7.19327 19.079C9.04012 19.307 10.9604 19.307 12.8073 19.079C12.3003 20.0591 11.2441 20.7502 10.0002 20.7502C8.75643 20.7502 7.7002 20.0591 7.19327 19.079Z" fill="#767676"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.0001 1C17.0001 0.447715 16.5524 0 16.0001 0C15.4478 0 15.0001 0.447715 15.0001 1V1.39864C14.2597 1.56884 13.5904 1.92409 13.0489 2.41399L12.5409 2.08741C12.0763 1.78874 11.4576 1.92323 11.159 2.38779C10.8603 2.85236 10.9948 3.47107 11.4593 3.76974L11.9236 4.06817C11.7576 4.51495 11.6668 4.99727 11.6668 5.5C11.6668 6.00281 11.7577 6.48521 11.9236 6.93205L11.4598 7.23026C10.9952 7.52893 10.8607 8.14764 11.1594 8.61221C11.458 9.07677 12.0768 9.21126 12.5413 8.91259L13.0491 8.58617C13.5905 9.07598 14.2598 9.43118 15.0001 9.60137V10C15.0001 10.5523 15.4478 11 16.0001 11C16.5524 11 17.0001 10.5523 17.0001 10V9.60137C17.7404 9.43118 18.4097 9.07598 18.9512 8.58617L19.4589 8.91259C19.9235 9.21126 20.5422 9.07677 20.8409 8.61221C21.1395 8.14764 21.005 7.52893 20.5405 7.23026L20.0766 6.93205C20.2426 6.48521 20.3335 6.00281 20.3335 5.5C20.3335 4.99727 20.2426 4.51495 20.0767 4.06817L20.5409 3.76974C21.0055 3.47107 21.1399 2.85236 20.8413 2.38779C20.5426 1.92323 19.9239 1.78874 19.4593 2.08741L18.9513 2.41399C18.4099 1.92409 17.7405 1.56884 17.0001 1.39864V1ZM14.0483 4.28631C14.4615 3.68984 15.1753 3.28571 16.0001 3.28571C16.825 3.28571 17.5387 3.68984 17.9519 4.28631C18.1945 4.6366 18.3335 5.05259 18.3335 5.5C18.3335 5.94747 18.1945 6.36351 17.9518 6.71382C17.5386 7.31022 16.8249 7.71429 16.0001 7.71429C15.1753 7.71429 14.4616 7.31022 14.0484 6.71382C13.8057 6.36351 13.6668 5.94747 13.6668 5.5C13.6668 5.05259 13.8057 4.6366 14.0483 4.28631Z" fill="#767676"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@ -0,0 +1,66 @@
|
||||
import 'package:test_sa/models/base.dart';
|
||||
|
||||
class ModuleWithSubModulesModel extends Base {
|
||||
int? id;
|
||||
String? name;
|
||||
List<SubModuleModel>? subModules;
|
||||
|
||||
ModuleWithSubModulesModel({
|
||||
this.id,
|
||||
this.name,
|
||||
this.subModules,
|
||||
});
|
||||
|
||||
ModuleWithSubModulesModel.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
if (json['subModules'] != null) {
|
||||
subModules = <SubModuleModel>[];
|
||||
json['subModules'].forEach((v) {
|
||||
subModules!.add(SubModuleModel.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['name'] = name;
|
||||
if (subModules != null) {
|
||||
data['subModules'] = subModules!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => name ?? '';
|
||||
}
|
||||
|
||||
class SubModuleModel extends Base {
|
||||
int? id;
|
||||
String? name;
|
||||
String? mainTableName;
|
||||
|
||||
SubModuleModel({
|
||||
this.id,
|
||||
this.name,
|
||||
this.mainTableName,
|
||||
});
|
||||
|
||||
SubModuleModel.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
mainTableName = json['mainTableName'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['name'] = name;
|
||||
data['mainTableName'] = mainTableName;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => name ?? '';
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
class NotificationFilterModel {
|
||||
List<int>? priorityIds;
|
||||
List<int>? channelIds;
|
||||
List<int>? recipientIds;
|
||||
List<int>? moduleIds;
|
||||
List<int>? subModuleIds;
|
||||
DateTime? startDate;
|
||||
DateTime? endDate;
|
||||
String? searchText;
|
||||
bool? isAscending;
|
||||
int? isRead; // 0 = All, 1 = Unread, 2 = Read
|
||||
|
||||
NotificationFilterModel({
|
||||
this.priorityIds,
|
||||
this.channelIds,
|
||||
this.recipientIds,
|
||||
this.moduleIds,
|
||||
this.subModuleIds,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.searchText,
|
||||
this.isAscending,
|
||||
this.isRead,
|
||||
});
|
||||
|
||||
// Check if any filters are active
|
||||
bool get hasActiveFilters {
|
||||
return (priorityIds?.isNotEmpty ?? false) ||
|
||||
(channelIds?.isNotEmpty ?? false) ||
|
||||
(recipientIds?.isNotEmpty ?? false) ||
|
||||
(moduleIds?.isNotEmpty ?? false) ||
|
||||
(subModuleIds?.isNotEmpty ?? false) ||
|
||||
startDate != null ||
|
||||
endDate != null;
|
||||
}
|
||||
|
||||
// Convert to API body (only include non-null/non-empty values)
|
||||
Map<String, dynamic> toApiBody() {
|
||||
final Map<String, dynamic> body = {};
|
||||
|
||||
if (priorityIds?.isNotEmpty ?? false) body['priorityIds'] = priorityIds;
|
||||
if (channelIds?.isNotEmpty ?? false) body['channelIds'] = channelIds;
|
||||
if (recipientIds?.isNotEmpty ?? false) body['recipientIds'] = recipientIds;
|
||||
if (moduleIds?.isNotEmpty ?? false) body['moduleId'] = moduleIds!.first; // API expects single moduleId
|
||||
if (subModuleIds?.isNotEmpty ?? false) body['subModuleIds'] = subModuleIds;
|
||||
if (startDate != null) body['createdDateFrom'] = startDate!.toIso8601String();
|
||||
if (endDate != null) body['createdDateTo'] = endDate!.toIso8601String();
|
||||
if (searchText?.isNotEmpty ?? false) body['searchText'] = searchText;
|
||||
if (isAscending != null) body['isAscending'] = isAscending;
|
||||
if (isRead != null) body['isRead'] = isRead;
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
priorityIds?.clear();
|
||||
channelIds?.clear();
|
||||
recipientIds?.clear();
|
||||
moduleIds?.clear();
|
||||
subModuleIds?.clear();
|
||||
startDate = null;
|
||||
endDate = null;
|
||||
searchText = null;
|
||||
isAscending = null;
|
||||
isRead = null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
class NotificationChannel {
|
||||
final int? id;
|
||||
final String? name;
|
||||
final int? value;
|
||||
|
||||
NotificationChannel({
|
||||
this.id,
|
||||
this.name,
|
||||
this.value,
|
||||
});
|
||||
|
||||
factory NotificationChannel.fromJson(Map<String, dynamic> json) {
|
||||
return NotificationChannel(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
value: json['value'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'value': value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,167 @@
|
||||
import 'package:test_sa/modules/notification_setting/models/notification_channel_model.dart';
|
||||
|
||||
class NotificationPreferenceResponse {
|
||||
final List<NotificationTemplate>? data;
|
||||
final int? totalRows;
|
||||
final int? count;
|
||||
final String? message;
|
||||
final String? title;
|
||||
final String? innerMessage;
|
||||
final int? responseCode;
|
||||
final bool? isSuccess;
|
||||
|
||||
NotificationPreferenceResponse({
|
||||
this.data,
|
||||
this.totalRows,
|
||||
this.count,
|
||||
this.message,
|
||||
this.title,
|
||||
this.innerMessage,
|
||||
this.responseCode,
|
||||
this.isSuccess,
|
||||
});
|
||||
|
||||
factory NotificationPreferenceResponse.fromJson(Map<String, dynamic> json) {
|
||||
return NotificationPreferenceResponse(
|
||||
data: json['data'] != null
|
||||
? (json['data'] as List).map((e) => NotificationTemplate.fromJson(e)).toList()
|
||||
: null,
|
||||
totalRows: json['totalRows'],
|
||||
count: json['count'],
|
||||
message: json['message'],
|
||||
title: json['title'],
|
||||
innerMessage: json['innerMessage'],
|
||||
responseCode: json['responseCode'],
|
||||
isSuccess: json['isSuccess'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'data': data?.map((e) => e.toJson()).toList(),
|
||||
'totalRows': totalRows,
|
||||
'count': count,
|
||||
'message': message,
|
||||
'title': title,
|
||||
'innerMessage': innerMessage,
|
||||
'responseCode': responseCode,
|
||||
'isSuccess': isSuccess,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationTemplate {
|
||||
final int? notificationTemplateId;
|
||||
final String? templateName;
|
||||
final String? moduleName;
|
||||
final String? subModuleName;
|
||||
final bool? mandatory;
|
||||
final bool? isDisabled;
|
||||
final String? channelNames;
|
||||
final bool? isCustom;
|
||||
final List<NotificationChannel>? templateChannels;
|
||||
final List<NotificationChannel>? preferenceChannels;
|
||||
|
||||
// Local state for UI tracking
|
||||
bool isModified;
|
||||
List<int> selectedChannelIds;
|
||||
|
||||
NotificationTemplate({
|
||||
this.notificationTemplateId,
|
||||
this.templateName,
|
||||
this.moduleName,
|
||||
this.subModuleName,
|
||||
this.mandatory,
|
||||
this.isDisabled,
|
||||
this.channelNames,
|
||||
this.isCustom,
|
||||
this.templateChannels,
|
||||
this.preferenceChannels,
|
||||
this.isModified = false,
|
||||
List<int>? selectedChannelIds,
|
||||
}) : selectedChannelIds = selectedChannelIds ?? [];
|
||||
|
||||
factory NotificationTemplate.fromJson(Map<String, dynamic> json) {
|
||||
final List<NotificationChannel>? templateChannelsList = json['templateChannels'] != null
|
||||
? (json['templateChannels'] as List).map((e) => NotificationChannel.fromJson(e)).toList()
|
||||
: null;
|
||||
|
||||
final List<NotificationChannel>? preferenceChannelsList = json['preferenceChannels'] != null
|
||||
? (json['preferenceChannels'] as List).map((e) => NotificationChannel.fromJson(e)).toList()
|
||||
: null;
|
||||
|
||||
// Initialize selectedChannelIds from preferenceChannels if available, otherwise from templateChannels
|
||||
List<int> initialSelectedIds = [];
|
||||
if (preferenceChannelsList != null && preferenceChannelsList.isNotEmpty) {
|
||||
initialSelectedIds = preferenceChannelsList
|
||||
.where((e) => e.id != null)
|
||||
.map((e) => e.id!)
|
||||
.toList();
|
||||
} else if (templateChannelsList != null && templateChannelsList.isNotEmpty) {
|
||||
initialSelectedIds = templateChannelsList
|
||||
.where((e) => e.id != null)
|
||||
.map((e) => e.id!)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return NotificationTemplate(
|
||||
notificationTemplateId: json['notificationTemplateId'],
|
||||
templateName: json['templateName'],
|
||||
moduleName: json['moduleName'],
|
||||
subModuleName: json['subModuleName'],
|
||||
mandatory: json['mandatory'],
|
||||
isDisabled: json['isDisabled'],
|
||||
channelNames: json['channelNames'],
|
||||
isCustom: json['isCustom'],
|
||||
templateChannels: templateChannelsList,
|
||||
preferenceChannels: preferenceChannelsList,
|
||||
selectedChannelIds: initialSelectedIds,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'notificationTemplateId': notificationTemplateId,
|
||||
'templateName': templateName,
|
||||
'moduleName': moduleName,
|
||||
'subModuleName': subModuleName,
|
||||
'mandatory': mandatory,
|
||||
'isDisabled': isDisabled,
|
||||
'channelNames': channelNames,
|
||||
'isCustom': isCustom,
|
||||
'templateChannels': templateChannels?.map((e) => e.toJson()).toList(),
|
||||
'preferenceChannels': preferenceChannels?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy with modified values
|
||||
NotificationTemplate copyWith({
|
||||
int? notificationTemplateId,
|
||||
String? templateName,
|
||||
String? moduleName,
|
||||
String? subModuleName,
|
||||
bool? mandatory,
|
||||
bool? isDisabled,
|
||||
String? channelNames,
|
||||
bool? isCustom,
|
||||
List<NotificationChannel>? templateChannels,
|
||||
List<NotificationChannel>? preferenceChannels,
|
||||
bool? isModified,
|
||||
List<int>? selectedChannelIds,
|
||||
}) {
|
||||
return NotificationTemplate(
|
||||
notificationTemplateId: notificationTemplateId ?? this.notificationTemplateId,
|
||||
templateName: templateName ?? this.templateName,
|
||||
moduleName: moduleName ?? this.moduleName,
|
||||
subModuleName: subModuleName ?? this.subModuleName,
|
||||
mandatory: mandatory ?? this.mandatory,
|
||||
isDisabled: isDisabled ?? this.isDisabled,
|
||||
channelNames: channelNames ?? this.channelNames,
|
||||
isCustom: isCustom ?? this.isCustom,
|
||||
templateChannels: templateChannels ?? this.templateChannels,
|
||||
preferenceChannels: preferenceChannels ?? this.preferenceChannels,
|
||||
isModified: isModified ?? this.isModified,
|
||||
selectedChannelIds: selectedChannelIds ?? this.selectedChannelIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,236 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:test_sa/controllers/api_routes/api_manager.dart';
|
||||
import 'package:test_sa/controllers/api_routes/urls.dart';
|
||||
import 'package:test_sa/modules/notification_setting/models/notification_preference_model.dart';
|
||||
|
||||
class NotificationSettingProvider extends ChangeNotifier {
|
||||
// State management
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingMore = false;
|
||||
bool _isSaving = false;
|
||||
String? _errorMessage;
|
||||
List<NotificationTemplate> _templates = [];
|
||||
|
||||
// Pagination
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
int _totalRecords = 0;
|
||||
bool _hasMoreData = true;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingMore => _isLoadingMore;
|
||||
bool get isSaving => _isSaving;
|
||||
String? get errorMessage => _errorMessage;
|
||||
List<NotificationTemplate> get templates => _templates;
|
||||
bool get hasMoreData => _hasMoreData;
|
||||
bool get isEmpty => _templates.isEmpty && !_isLoading;
|
||||
|
||||
// Check if there are any modified templates
|
||||
bool get hasChanges => _templates.any((template) => template.isModified);
|
||||
|
||||
/// Load initial notification preferences
|
||||
Future<void> loadNotificationPreferences({bool refresh = false}) async {
|
||||
if (refresh) {
|
||||
reset();
|
||||
}
|
||||
|
||||
if (_isLoading || _isLoadingMore) return;
|
||||
|
||||
_isLoading = _currentPage == 1;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final body = {
|
||||
'pageNumber': _currentPage,
|
||||
'pageSize': _pageSize,
|
||||
};
|
||||
|
||||
http.Response response = await ApiManager.instance.post(
|
||||
URLs.getNotificationPreferences,
|
||||
body: body,
|
||||
showToast: false,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final jsonData = json.decode(response.body);
|
||||
final notificationResponse = NotificationPreferenceResponse.fromJson(jsonData);
|
||||
|
||||
if (notificationResponse.isSuccess == true && notificationResponse.data != null) {
|
||||
_totalRecords = notificationResponse.totalRows ?? 0;
|
||||
|
||||
if (notificationResponse.data!.isNotEmpty) {
|
||||
_templates.addAll(notificationResponse.data!);
|
||||
_currentPage++;
|
||||
_hasMoreData = _templates.length < _totalRecords;
|
||||
} else {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} else {
|
||||
_errorMessage = notificationResponse.message ?? 'Failed to load notification preferences';
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} else {
|
||||
_errorMessage = 'Failed to load notification preferences';
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} catch (error) {
|
||||
_errorMessage = 'An error occurred while loading data';
|
||||
print('Error in loadNotificationPreferences: $error');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Load more data for pagination
|
||||
Future<void> loadMoreData() async {
|
||||
if (_isLoadingMore || !_hasMoreData || _isLoading) return;
|
||||
|
||||
_isLoadingMore = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final body = {
|
||||
'pageNumber': _currentPage,
|
||||
'pageSize': _pageSize,
|
||||
};
|
||||
|
||||
http.Response response = await ApiManager.instance.post(
|
||||
URLs.getNotificationPreferences,
|
||||
body: body,
|
||||
showToast: false,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final jsonData = json.decode(response.body);
|
||||
final notificationResponse = NotificationPreferenceResponse.fromJson(jsonData);
|
||||
|
||||
if (notificationResponse.isSuccess == true && notificationResponse.data != null) {
|
||||
if (notificationResponse.data!.isNotEmpty) {
|
||||
_templates.addAll(notificationResponse.data!);
|
||||
_currentPage++;
|
||||
_hasMoreData = _templates.length < _totalRecords;
|
||||
} else {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} else {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} else {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} catch (error) {
|
||||
print('Error in loadMoreData: $error');
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update enable/disable state for a template
|
||||
void updateTemplateState(int index, bool isDisabled) {
|
||||
if (index < 0 || index >= _templates.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
final template = _templates[index];
|
||||
|
||||
// Note: mandatory flag only applies to channel preferences, not to enable/disable toggle
|
||||
// Users can always enable/disable notifications
|
||||
|
||||
_templates[index] = template.copyWith(
|
||||
isDisabled: isDisabled,
|
||||
isModified: true,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Update selected channels for a template
|
||||
void updateTemplateChannels(int index, List<int> channelIds) {
|
||||
if (index < 0 || index >= _templates.length) return;
|
||||
|
||||
final template = _templates[index];
|
||||
|
||||
_templates[index] = template.copyWith(
|
||||
selectedChannelIds: channelIds,
|
||||
isModified: true,
|
||||
);
|
||||
_templates[index].selectedChannelIds = channelIds;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Save all modified preferences
|
||||
Future<bool> savePreferences() async {
|
||||
if (!hasChanges) return true;
|
||||
|
||||
_isSaving = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final preferences = _templates.where((t) => t.isModified).map((template) {
|
||||
return {
|
||||
'notificationTemplateId': template.notificationTemplateId,
|
||||
'disabled': template.isDisabled ?? false,
|
||||
'channels': template.selectedChannelIds,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
if (preferences.isEmpty) {
|
||||
_isSaving = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use ApiManager's postList method for array body
|
||||
http.Response response = await ApiManager.instance.postList(
|
||||
URLs.saveNotificationPreferences,
|
||||
body: preferences,
|
||||
showToast: true,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
// Reset modification flags after successful save
|
||||
for (var i = 0; i < _templates.length; i++) {
|
||||
_templates[i] = _templates[i].copyWith(isModified: false);
|
||||
}
|
||||
|
||||
_isSaving = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
_errorMessage = 'Failed to save preferences';
|
||||
_isSaving = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
_errorMessage = 'An error occurred while saving';
|
||||
_isSaving = false;
|
||||
notifyListeners();
|
||||
print('Error in savePreferences: $error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset provider state
|
||||
void reset() {
|
||||
_templates.clear();
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
_totalRecords = 0;
|
||||
_isLoading = false;
|
||||
_isLoadingMore = false;
|
||||
_errorMessage = null;
|
||||
}
|
||||
|
||||
/// Retry loading after error
|
||||
Future<void> retry() async {
|
||||
reset();
|
||||
await loadNotificationPreferences();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:test_sa/extensions/context_extension.dart';
|
||||
import 'package:test_sa/extensions/int_extensions.dart';
|
||||
import 'package:test_sa/extensions/string_extensions.dart';
|
||||
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
|
||||
import 'package:test_sa/modules/notification_setting/provider/notification_setting_provider.dart';
|
||||
import 'package:test_sa/modules/notification_setting/view/widgets/notification_preference_card.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
|
||||
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
|
||||
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
|
||||
|
||||
class NotificationSettingPage extends StatefulWidget {
|
||||
static const String id = "notification_setting_page";
|
||||
|
||||
const NotificationSettingPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<NotificationSettingPage> createState() => _NotificationSettingPageState();
|
||||
}
|
||||
|
||||
class _NotificationSettingPageState extends State<NotificationSettingPage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
late NotificationSettingProvider _provider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_provider = Provider.of<NotificationSettingProvider>(context, listen: false);
|
||||
_provider.loadNotificationPreferences(refresh: true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.removeListener(_onScroll);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
|
||||
_provider.loadMoreData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: DefaultAppBar(
|
||||
title: 'Notification Settings'.addTranslation,
|
||||
),
|
||||
body: Consumer<NotificationSettingProvider>(
|
||||
builder: (context, provider, child) {
|
||||
_provider = provider;
|
||||
if (provider.isLoading) {
|
||||
return const Center(child: ALoading());
|
||||
}
|
||||
if (provider.isEmpty || (provider.errorMessage != null && provider.templates.isEmpty)) {
|
||||
return const Center(child: NoDataFound());
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await provider.loadNotificationPreferences(refresh: true);
|
||||
},
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
padding: EdgeInsets.all(16.toScreenWidth),
|
||||
itemCount: provider.templates.length + (provider.isLoadingMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == provider.templates.length) {
|
||||
return const Center(
|
||||
child: ALoading());
|
||||
}
|
||||
|
||||
final template = provider.templates[index];
|
||||
return NotificationPreferenceCard(
|
||||
key: ValueKey('${template.notificationTemplateId}_${template.isDisabled}'),
|
||||
template: template,
|
||||
onToggleChange: (isDisabled) {
|
||||
provider.updateTemplateState(index, isDisabled);
|
||||
},
|
||||
onChannelChange: (channelIds) {
|
||||
provider.updateTemplateChannels(index, channelIds);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (cxt, index) => 12.height,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Save button - sticky at bottom
|
||||
if (provider.hasChanges)
|
||||
FooterActionButton.footerContainer(
|
||||
context: context,
|
||||
child: AppFilledButton(
|
||||
onPressed: provider.isSaving ? null : _handleSave,
|
||||
label: context.translation.save,
|
||||
loading: provider.isSaving,
|
||||
disableButton: provider.isSaving,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Future<void> _handleSave() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(child: AppLazyLoading()),
|
||||
);
|
||||
|
||||
final success = await _provider.savePreferences();
|
||||
// Navigator.pop(context); // Close loading dialog
|
||||
|
||||
if (success) {
|
||||
_provider.loadNotificationPreferences(refresh: true);
|
||||
Navigator.pop(context); // Go back to previous screen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,186 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:test_sa/extensions/context_extension.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/widget_extensions.dart';
|
||||
import 'package:test_sa/modules/notification_setting/models/notification_preference_model.dart';
|
||||
import 'package:test_sa/new_views/app_style/app_color.dart';
|
||||
import 'package:test_sa/new_views/app_style/app_text_style.dart';
|
||||
import 'package:test_sa/new_views/pages/land_page/widgets/static_filter_buttons.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';
|
||||
|
||||
class NotificationPreferenceCard extends StatelessWidget {
|
||||
final NotificationTemplate template;
|
||||
final Function(bool isDisabled) onToggleChange;
|
||||
final Function(List<int> channelIds) onChannelChange;
|
||||
|
||||
const NotificationPreferenceCard({
|
||||
Key? key,
|
||||
required this.template,
|
||||
required this.onToggleChange,
|
||||
required this.onChannelChange,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isEnabled = !(template.isDisabled ?? false);
|
||||
final bool isMandatory = template.mandatory ?? false;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(isMandatory) ...[
|
||||
4.height,
|
||||
Text(
|
||||
'You cannot update this notification as it is mandatory.'.addTranslation,
|
||||
style: AppTextStyle.body2.copyWith(
|
||||
color: AppColor.red30,
|
||||
fontSize: 10.toScreenWidth,
|
||||
),
|
||||
),
|
||||
],
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InfoHeader16Widget(
|
||||
template.templateName ?? '',
|
||||
),
|
||||
),
|
||||
8.width,
|
||||
_buildCompactToggle(context, isEnabled, isMandatory),
|
||||
],
|
||||
),
|
||||
4.height,
|
||||
InfoTextWidget(
|
||||
label: context.translation.notificationModule,
|
||||
value: template.moduleName ?? '',
|
||||
),
|
||||
4.height,
|
||||
InfoTextWidget(
|
||||
label: 'Sub Module'.addTranslation,
|
||||
value: template.subModuleName ?? '',
|
||||
),
|
||||
4.height,
|
||||
if (template.templateChannels != null && template.templateChannels!.isNotEmpty) ...[
|
||||
InfoTextLabelWidget(
|
||||
label: '${context.translation.channelPreference} :',
|
||||
),
|
||||
8.height,
|
||||
_buildChannelChips(context, isMandatory),
|
||||
|
||||
],
|
||||
],
|
||||
).toShadowContainer(context,padding: 16);
|
||||
}
|
||||
|
||||
Widget _buildCompactToggle(BuildContext context, bool isEnabled, bool isMandatory) {
|
||||
return Container(
|
||||
width: 99.toScreenWidth,
|
||||
height: 32.toScreenHeight,
|
||||
padding: EdgeInsets.all(4.toScreenHeight),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.toggleContainerBg(context),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: AppColor.border2Color(context)),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
ignoring: isMandatory,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if(!isMandatory){
|
||||
onToggleChange(false);
|
||||
}
|
||||
},
|
||||
child: _buildToggleOption(
|
||||
context: context,
|
||||
label: context.translation.enable,
|
||||
isActive: isEnabled,
|
||||
activeColor: AppColor.toggleEnableBgColor(context),
|
||||
inactiveColor: Colors.transparent,
|
||||
textColor: isEnabled
|
||||
? AppColor.toggleEnableTextColor(context)
|
||||
: AppColor.toggleDisableTextColor(context),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if(!isMandatory){
|
||||
onToggleChange(true);
|
||||
}
|
||||
},
|
||||
child: _buildToggleOption(
|
||||
context: context,
|
||||
label: context.translation.disable,
|
||||
isActive: !isEnabled,
|
||||
activeColor: AppColor.toggleDisableBgColor(context),
|
||||
inactiveColor: Colors.transparent,
|
||||
textColor: !isEnabled
|
||||
? AppColor.toggleDisable2TextColor(context)
|
||||
: AppColor.toggleDisableTextColor(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggleOption({
|
||||
required BuildContext context,
|
||||
required String label,
|
||||
required bool isActive,
|
||||
required Color activeColor,
|
||||
required Color inactiveColor,
|
||||
required Color textColor,
|
||||
}) {
|
||||
return Container(
|
||||
width: 44.toScreenWidth,
|
||||
height: 22.toScreenHeight,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? activeColor : inactiveColor,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
label.toCamelCase,
|
||||
style: AppTextStyle.body2.copyWith(
|
||||
color: textColor,
|
||||
fontSize: 10.toScreenWidth,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChannelChips(BuildContext context, bool isMandatory) {
|
||||
final Map<String, int> channelsMap = {};
|
||||
|
||||
if (template.templateChannels != null) {
|
||||
for (var channel in template.templateChannels!) {
|
||||
if (channel.name != null && channel.id != null) {
|
||||
channelsMap[channel.name!] = channel.id!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: isMandatory,
|
||||
child: RequestsTypeFilter(
|
||||
items: channelsMap,
|
||||
initialValues: template.selectedChannelIds,
|
||||
onSelect: (selectedIds) {
|
||||
if (!isMandatory) {
|
||||
onChannelChange(selectedIds);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:test_sa/controllers/api_routes/api_manager.dart';
|
||||
import 'package:test_sa/controllers/api_routes/urls.dart';
|
||||
import 'package:test_sa/models/base.dart';
|
||||
import 'package:test_sa/models/module_with_submodules_model.dart';
|
||||
import 'package:test_sa/providers/loading_list_notifier.dart';
|
||||
|
||||
class ModulesWithSubModulesProvider extends LoadingListNotifier<ModuleWithSubModulesModel> {
|
||||
@override
|
||||
Future getData({int? id}) async {
|
||||
if (loading == true) return -2;
|
||||
loading = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
Response response = await ApiManager.instance.get(
|
||||
URLs.getModulesWithSubModules,
|
||||
useCache: true,
|
||||
enableToastMessage: false,
|
||||
);
|
||||
stateCode = response.statusCode;
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
List modulesListJson = json.decode(response.body)["data"];
|
||||
items = modulesListJson.map((item) => ModuleWithSubModulesModel.fromJson(item)).toList();
|
||||
}
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return response.statusCode;
|
||||
} catch (error) {
|
||||
loading = false;
|
||||
stateCode = -1;
|
||||
notifyListeners();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:test_sa/controllers/api_routes/api_manager.dart';
|
||||
import 'package:test_sa/controllers/api_routes/urls.dart';
|
||||
import 'package:test_sa/models/lookup.dart';
|
||||
import 'package:test_sa/providers/loading_list_notifier.dart';
|
||||
|
||||
class NotificationChannelLookupProvider extends LoadingListNotifier<Lookup> {
|
||||
@override
|
||||
Future getData({int? id}) async {
|
||||
if (loading == true) return -2;
|
||||
loading = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
Response response = await ApiManager.instance.get(
|
||||
URLs.getNotificationChannelLookup,
|
||||
useCache: true,
|
||||
enableToastMessage: false,
|
||||
);
|
||||
stateCode = response.statusCode;
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
List categoriesListJson = json.decode(response.body)["data"];
|
||||
items = categoriesListJson.map((item) => Lookup.fromJson(item)).toList();
|
||||
}
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return response.statusCode;
|
||||
} catch (error) {
|
||||
loading = false;
|
||||
stateCode = -1;
|
||||
notifyListeners();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:test_sa/controllers/api_routes/api_manager.dart';
|
||||
import 'package:test_sa/controllers/api_routes/urls.dart';
|
||||
import 'package:test_sa/models/lookup.dart';
|
||||
import 'package:test_sa/providers/loading_list_notifier.dart';
|
||||
|
||||
class NotificationPriorityLookupProvider extends LoadingListNotifier<Lookup> {
|
||||
@override
|
||||
Future getData({int? id}) async {
|
||||
if (loading == true) return -2;
|
||||
loading = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
Response response = await ApiManager.instance.get(
|
||||
URLs.getNotificationPriorityLookup,
|
||||
useCache: true,
|
||||
enableToastMessage: false,
|
||||
);
|
||||
stateCode = response.statusCode;
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
List categoriesListJson = json.decode(response.body)["data"];
|
||||
items = categoriesListJson.map((item) => Lookup.fromJson(item)).toList();
|
||||
}
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return response.statusCode;
|
||||
} catch (error) {
|
||||
loading = false;
|
||||
stateCode = -1;
|
||||
notifyListeners();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:test_sa/controllers/api_routes/api_manager.dart';
|
||||
import 'package:test_sa/controllers/api_routes/urls.dart';
|
||||
import 'package:test_sa/models/lookup.dart';
|
||||
import 'package:test_sa/providers/loading_list_notifier.dart';
|
||||
|
||||
class NotificationRecipientLookupProvider extends LoadingListNotifier<Lookup> {
|
||||
@override
|
||||
Future getData({int? id}) async {
|
||||
if (loading == true) return -2;
|
||||
loading = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
Response response = await ApiManager.instance.get(
|
||||
URLs.getNotificationRecipientLookup,
|
||||
useCache: true,
|
||||
enableToastMessage: false,
|
||||
);
|
||||
stateCode = response.statusCode;
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
List categoriesListJson = json.decode(response.body)["data"];
|
||||
items = categoriesListJson.map((item) => Lookup.fromJson(item)).toList();
|
||||
}
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
return response.statusCode;
|
||||
} catch (error) {
|
||||
loading = false;
|
||||
stateCode = -1;
|
||||
notifyListeners();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,340 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:test_sa/extensions/context_extension.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/widget_extensions.dart';
|
||||
import 'package:test_sa/models/lookup.dart';
|
||||
import 'package:test_sa/models/module_with_submodules_model.dart';
|
||||
import 'package:test_sa/models/notification_filter_model.dart';
|
||||
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
|
||||
import 'package:test_sa/new_views/app_style/app_color.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
|
||||
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
|
||||
import 'package:test_sa/providers/loading_list_notifier.dart';
|
||||
import 'package:test_sa/providers/lookups/modules_with_submodules_provider.dart';
|
||||
import 'package:test_sa/providers/lookups/notification_priority_lookup_provider.dart';
|
||||
import 'package:test_sa/providers/lookups/notification_channel_lookup_provider.dart';
|
||||
import 'package:test_sa/providers/lookups/notification_recipient_lookup_provider.dart';
|
||||
import 'package:test_sa/views/widgets/date_and_time/date_picker.dart';
|
||||
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
|
||||
|
||||
class NotificationFilterPage extends StatefulWidget {
|
||||
static const String id = "notification_filter_page";
|
||||
final NotificationFilterModel? data;
|
||||
|
||||
const NotificationFilterPage({Key? key, this.data}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<NotificationFilterPage> createState() => _NotificationFilterPageState();
|
||||
}
|
||||
|
||||
class _NotificationFilterPageState extends State<NotificationFilterPage> {
|
||||
NotificationFilterModel? filter;
|
||||
Lookup? selectedPriority;
|
||||
Lookup? selectedChannel;
|
||||
Lookup? selectedRecipient;
|
||||
ModuleWithSubModulesModel? selectedModule;
|
||||
SubModuleModel? selectedSubModule;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
filter = widget.data ?? NotificationFilterModel();
|
||||
|
||||
// Load providers and restore selected values after first frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadProvidersAndRestoreValues();
|
||||
});
|
||||
}
|
||||
|
||||
/// Load providers and restore previously selected filter values
|
||||
Future<void> _loadProvidersAndRestoreValues() async {
|
||||
// Get providers
|
||||
final priorityProvider = context.read<NotificationPriorityLookupProvider>();
|
||||
final channelProvider = context.read<NotificationChannelLookupProvider>();
|
||||
final recipientProvider = context.read<NotificationRecipientLookupProvider>();
|
||||
final moduleProvider = context.read<ModulesWithSubModulesProvider>();
|
||||
|
||||
// Load data from providers if not already loaded
|
||||
final futures = <Future>[];
|
||||
if (priorityProvider.items.isEmpty) futures.add(priorityProvider.getData());
|
||||
if (channelProvider.items.isEmpty) futures.add(channelProvider.getData());
|
||||
if (recipientProvider.items.isEmpty) futures.add(recipientProvider.getData());
|
||||
if (moduleProvider.items.isEmpty) futures.add(moduleProvider.getData());
|
||||
|
||||
// Wait for all providers to load
|
||||
await Future.wait(futures);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
// Restore priority
|
||||
if (filter?.priorityIds?.isNotEmpty ?? false) {
|
||||
final priorityId = filter!.priorityIds!.first;
|
||||
selectedPriority = priorityProvider.items.firstWhere(
|
||||
(item) => item.id == priorityId,
|
||||
orElse: () => Lookup(),
|
||||
);
|
||||
if (selectedPriority?.id == null) selectedPriority = null;
|
||||
}
|
||||
|
||||
// Restore channel
|
||||
if (filter?.channelIds?.isNotEmpty ?? false) {
|
||||
final channelId = filter!.channelIds!.first;
|
||||
selectedChannel = channelProvider.items.firstWhere(
|
||||
(item) => item.id == channelId,
|
||||
orElse: () => Lookup(),
|
||||
);
|
||||
if (selectedChannel?.id == null) selectedChannel = null;
|
||||
}
|
||||
|
||||
// Restore recipient
|
||||
if (filter?.recipientIds?.isNotEmpty ?? false) {
|
||||
final recipientId = filter!.recipientIds!.first;
|
||||
selectedRecipient = recipientProvider.items.firstWhere(
|
||||
(item) => item.id == recipientId,
|
||||
orElse: () => Lookup(),
|
||||
);
|
||||
if (selectedRecipient?.id == null) selectedRecipient = null;
|
||||
}
|
||||
|
||||
// Restore module
|
||||
if (filter?.moduleIds?.isNotEmpty ?? false) {
|
||||
final moduleId = filter!.moduleIds!.first;
|
||||
selectedModule = moduleProvider.items.firstWhere(
|
||||
(item) => item.id == moduleId,
|
||||
orElse: () => ModuleWithSubModulesModel(),
|
||||
);
|
||||
if (selectedModule?.id == null) selectedModule = null;
|
||||
|
||||
// Restore submodule if module is selected
|
||||
if (selectedModule != null && (filter?.subModuleIds?.isNotEmpty ?? false)) {
|
||||
final subModuleId = filter!.subModuleIds!.first;
|
||||
selectedSubModule = selectedModule!.subModules?.firstWhere(
|
||||
(item) => item.id == subModuleId,
|
||||
orElse: () => SubModuleModel(),
|
||||
);
|
||||
if (selectedSubModule?.id == null) selectedSubModule = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.scaffold2Background(context),
|
||||
appBar: DefaultAppBar(
|
||||
title: context.translation.filter,
|
||||
actions: [
|
||||
if (_showResetButton())
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
filter?.clear();
|
||||
selectedPriority = null;
|
||||
selectedChannel = null;
|
||||
selectedRecipient = null;
|
||||
selectedModule = null;
|
||||
selectedSubModule = null;
|
||||
});
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text(
|
||||
'Clear Filters',
|
||||
style: AppTextStyles.bodyText2.copyWith(color: AppColor.red30),
|
||||
),
|
||||
).paddingOnly(start: 16, end: 16)
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Filter by Priority
|
||||
InfoHeader16Widget("Priority"),
|
||||
8.height,
|
||||
SingleItemDropDownMenu<Lookup, NotificationPriorityLookupProvider>(
|
||||
context: context,
|
||||
showAsBottomSheet: true,
|
||||
showShadow: false,
|
||||
title: "Select Priority",
|
||||
initialValue: selectedPriority,
|
||||
onSelect: (value) {
|
||||
setState(() {
|
||||
selectedPriority = value;
|
||||
if (value != null) {
|
||||
filter?.priorityIds = [value.id!];
|
||||
} else {
|
||||
filter?.priorityIds?.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
16.height,
|
||||
|
||||
// Filter by Channel
|
||||
InfoHeader16Widget("Channel"),
|
||||
8.height,
|
||||
SingleItemDropDownMenu<Lookup, NotificationChannelLookupProvider>(
|
||||
context: context,
|
||||
showAsBottomSheet: true,
|
||||
showShadow: false,
|
||||
title: "Select Channel",
|
||||
initialValue: selectedChannel,
|
||||
onSelect: (value) {
|
||||
setState(() {
|
||||
selectedChannel = value;
|
||||
if (value != null) {
|
||||
filter?.channelIds = [value.id!];
|
||||
} else {
|
||||
filter?.channelIds?.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
16.height,
|
||||
InfoHeader16Widget("Recipient"),
|
||||
8.height,
|
||||
SingleItemDropDownMenu<Lookup, NotificationRecipientLookupProvider>(
|
||||
context: context,
|
||||
showAsBottomSheet: true,
|
||||
backgroundColor: AppColor.neutral100,
|
||||
showShadow: false,
|
||||
title: "Select Recipient",
|
||||
initialValue: selectedRecipient,
|
||||
onSelect: (value) {
|
||||
setState(() {
|
||||
selectedRecipient = value;
|
||||
if (value != null) {
|
||||
filter?.recipientIds = [value.id!];
|
||||
} else {
|
||||
filter?.recipientIds?.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
16.height,
|
||||
|
||||
// Filter by Module
|
||||
InfoHeader16Widget("Module"),
|
||||
8.height,
|
||||
SingleItemDropDownMenu<ModuleWithSubModulesModel, ModulesWithSubModulesProvider>(
|
||||
context: context,
|
||||
showAsBottomSheet: true,
|
||||
showShadow: false,
|
||||
title: "Select Module",
|
||||
initialValue: selectedModule,
|
||||
onSelect: (value) {
|
||||
setState(() {
|
||||
selectedModule = value;
|
||||
selectedSubModule = null;
|
||||
if (value != null) {
|
||||
filter?.moduleIds = [value.id!];
|
||||
filter?.subModuleIds?.clear();
|
||||
} else {
|
||||
filter?.moduleIds?.clear();
|
||||
filter?.subModuleIds?.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
16.height,
|
||||
|
||||
// Filter by SubModule
|
||||
InfoHeader16Widget( "SubModule"),
|
||||
8.height,
|
||||
SingleItemDropDownMenu<SubModuleModel, NullableLoadingProvider>(
|
||||
context: context,
|
||||
showAsBottomSheet: true,
|
||||
showShadow: false,
|
||||
title: "Select SubModule",
|
||||
initialValue: selectedSubModule,
|
||||
enabled: selectedModule?.subModules?.isNotEmpty ?? false,
|
||||
staticData: selectedModule?.subModules ?? [],
|
||||
onSelect: (value) {
|
||||
setState(() {
|
||||
selectedSubModule = value;
|
||||
if (value != null) {
|
||||
filter?.subModuleIds = [value.id!];
|
||||
} else {
|
||||
filter?.subModuleIds?.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
16.height,
|
||||
|
||||
// Date Range Filter
|
||||
context.translation.pickDate.heading5(context).custom(color: AppColor.textStyleColor(context)),
|
||||
8.height,
|
||||
Row(
|
||||
children: [
|
||||
ADatePicker(
|
||||
label: context.translation.from,
|
||||
from: DateTime(2000),
|
||||
date: filter!.startDate,
|
||||
onDatePicker: (date) {
|
||||
setState(() {
|
||||
filter!.startDate = date;
|
||||
});
|
||||
},
|
||||
).expanded,
|
||||
8.width,
|
||||
ADatePicker(
|
||||
label: context.translation.to,
|
||||
from: DateTime(2000),
|
||||
date: filter!.endDate,
|
||||
onDatePicker: (date) {
|
||||
if (filter!.startDate != null && date.isBefore(filter!.startDate!)) {
|
||||
"End Date must be greater than start date".showToast;
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
filter!.endDate = date;
|
||||
});
|
||||
},
|
||||
).expanded,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
).expanded,
|
||||
16.height,
|
||||
FooterActionButton.footerContainer(
|
||||
context: context,
|
||||
child: AppFilledButton(
|
||||
onPressed: _applyFilter,
|
||||
label: context.translation.applyFilter,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _applyFilter() {
|
||||
if (filter?.startDate != null && filter?.endDate == null) {
|
||||
filter?.endDate = DateTime.now();
|
||||
}
|
||||
Navigator.of(context).pop(filter);
|
||||
}
|
||||
|
||||
bool _showResetButton() {
|
||||
return filter?.hasActiveFilters ?? false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:test_sa/extensions/int_extensions.dart';
|
||||
import 'package:test_sa/extensions/text_extensions.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/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_text_widget.dart';
|
||||
|
||||
class NotificationCardRedesigned extends StatelessWidget {
|
||||
final SystemNotificationModel notification;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const NotificationCardRedesigned({
|
||||
Key? key,
|
||||
required this.notification,
|
||||
required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isUnread = !(notification.readed ?? false);
|
||||
return Container(
|
||||
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,
|
||||
children: [
|
||||
// Priority Badge & Date Row
|
||||
Row(
|
||||
children: [
|
||||
// Priority Badge
|
||||
if (notification.priorityName != null && notification.priorityName!.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.getRequestStatusColorByName(
|
||||
context,
|
||||
notification.priorityName!,
|
||||
),
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Unread indicator (blue dot) - aligned with title
|
||||
if (isUnread)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6, right: 8),
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColor.primary10,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Title
|
||||
Expanded(
|
||||
child: InfoHeader16Widget(
|
||||
notification.title ?? '',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
8.height,
|
||||
if (notification.sourceName?.isNotEmpty ?? false)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
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)
|
||||
InfoTextLabelWidget(
|
||||
label: notification.text!,
|
||||
// style: AppTextStyles.bodyText2.copyWith(
|
||||
// color: AppColor.textColor(context),
|
||||
// height: 1.4,
|
||||
// ),
|
||||
// maxLines: 3,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:test_sa/extensions/text_extensions.dart';
|
||||
import 'package:test_sa/new_views/app_style/app_color.dart';
|
||||
|
||||
class NotificationTabButton extends StatelessWidget {
|
||||
final String label;
|
||||
final int? count;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const NotificationTabButton({
|
||||
Key? key,
|
||||
required this.label,
|
||||
this.count,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: ShapeDecoration(
|
||||
color: AppColor.background(context),
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
width: 1,
|
||||
color: isSelected ? AppColor.primary10 : AppColor.borderColor(context),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
shadows: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 0),
|
||||
spreadRadius: 0,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
count != null ? '$label ($count)' : label,
|
||||
style: AppTextStyles.tinyFont.copyWith(
|
||||
height: 1,
|
||||
color: isSelected ? AppColor.primary10 : AppColor.filterButtonTextColor(context),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue