From 9c4fd1088c7f71eed8f406e1f21ea0657f40afbb Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:37:48 +0300 Subject: [PATCH] alerts added and move asset tracking to asset page --- lib/controllers/api_routes/urls.dart | 5 + lib/main.dart | 4 + lib/modules/alerts/alert_detail_page.dart | 129 +++++++++++++++++ lib/modules/alerts/alert_item_view.dart | 50 +++++++ lib/modules/alerts/alert_model.dart | 132 ++++++++++++++++++ lib/modules/alerts/alert_provider.dart | 96 +++++++++++++ lib/modules/alerts/alerts_list_page.dart | 58 ++++++++ .../common_widgets/app_bottom_nav_bar.dart | 3 +- lib/new_views/common_widgets/app_drawer.dart | 4 +- lib/new_views/pages/land_page/land_page.dart | 9 +- .../widgets/equipment/my_assets_page.dart | 14 +- 11 files changed, 499 insertions(+), 5 deletions(-) create mode 100644 lib/modules/alerts/alert_detail_page.dart create mode 100644 lib/modules/alerts/alert_item_view.dart create mode 100644 lib/modules/alerts/alert_model.dart create mode 100644 lib/modules/alerts/alert_provider.dart create mode 100644 lib/modules/alerts/alerts_list_page.dart diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 9f4f12c3..6f8fbb27 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -644,4 +644,9 @@ class URLs { static get getPenguinTagStatusLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=3037"; static get getPenguinTagAssignmentLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=3033"; + //Alerts module + static get getAlerts => "$_baseUrl/Alerts/GetAlerts"; // post - paginated alerts list + static get getAlertById => "$_baseUrl/Alerts/GetById"; // get - append /{id} + static get resolveAlert => "$_baseUrl/Alerts/Resolve"; // post - resolve an alert + } diff --git a/lib/main.dart b/lib/main.dart index 091514cd..f178da9e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ import 'package:test_sa/controllers/providers/api/departments_provider.dart'; import 'package:test_sa/controllers/providers/api/device_transfer_provider.dart'; import 'package:test_sa/controllers/providers/api/devices_provider.dart'; import 'package:test_sa/controllers/providers/api/gas_refill_provider.dart'; +import 'package:test_sa/modules/alerts/alert_provider.dart'; import 'package:test_sa/controllers/providers/api/hospitals_provider.dart'; import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; import 'package:test_sa/controllers/providers/api/oracle_code_provider.dart'; @@ -273,6 +274,9 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => NotificationRecipientLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => ModulesWithSubModulesProvider(), lazy: true), + // ALERTS MODULE PROVIDER + ChangeNotifierProvider(create: (_) => AlertProvider(), lazy: true), + // GAS MODULE PROVIDERS (6) ChangeNotifierProvider(create: (_) => GasTypesProvider(), lazy: true), ChangeNotifierProvider(create: (_) => CylinderTypesProvider(), lazy: true), diff --git a/lib/modules/alerts/alert_detail_page.dart b/lib/modules/alerts/alert_detail_page.dart new file mode 100644 index 00000000..581cb42d --- /dev/null +++ b/lib/modules/alerts/alert_detail_page.dart @@ -0,0 +1,129 @@ +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/widget_extensions.dart'; +import 'package:test_sa/helper/utils.dart'; +import 'package:test_sa/modules/alerts/alert_model.dart'; +import 'package:test_sa/modules/alerts/alert_provider.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/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'; +import 'package:test_sa/views/widgets/loaders/app_loading.dart'; +import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; +import 'package:test_sa/views/widgets/requests/request_status.dart'; + +class AlertDetailPage extends StatefulWidget { + static const String id = "/alert-detail-page"; + final int alertId; + + const AlertDetailPage({Key? key, required this.alertId}) : super(key: key); + + @override + State createState() => _AlertDetailPageState(); +} + +class _AlertDetailPageState extends State { + AlertModel _model = AlertModel(); + late AlertProvider alertProvider; + bool isDataUpdated = false; + + @override + Widget build(BuildContext context) { + alertProvider = Provider.of(context, listen: false); + + return Scaffold( + appBar: const DefaultAppBar(title: "Alert Details"), + body: FutureBuilder( + future: alertProvider.getAlertById(widget.alertId, refresh: isDataUpdated), + builder: (context, snap) { + if (snap.connectionState == ConnectionState.waiting) { + return const ALoading(); + } else if (snap.hasData && snap.data != null) { + _model = snap.data as AlertModel; + isDataUpdated = false; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: informationCard(_model), + ).expanded, + if (!_model.isResolved) + FooterActionButton.footerContainer( + context: context, + child: AppFilledButton( + label: "Resolve", + onPressed: () async { + Utils.showLoading(context); + bool success = await alertProvider.resolveAlert(_model.id!); + isDataUpdated = !success; + Utils.hideLoading(context); + setState(() {}); + }, + ), + ), + ], + ); + } + return NoDataFound(message: context.translation.noDataFound).center; + }, + ), + ); + } + + Widget informationCard(AlertModel alertModel) { + String zonesText = (alertModel.zones ?? []) + .map((z) => z.zoneName ?? (z.zoneId != null ? 'Zone ${z.zoneId}' : null)) + .whereType() + .join(', '); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + StatusLabel( + label: alertModel.statusLabel, + textColor: AppColor.getRequestStatusTextColorByName(context, alertModel.statusLabel), + backgroundColor: AppColor.getRequestStatusColorByName(context, alertModel.statusLabel), + ), + ], + ), + 8.height, + InfoHeader16Widget("Alert Details"), + InfoTextWidget(label: "Alert No", value: alertModel.alertId ?? '-', copyValue: true), + InfoTextWidget(label: "Rule Name", value: alertModel.ruleName ?? '-'), + InfoTextWidget(label: "Trigger Type", value: alertModel.ruleTriggerType ?? '-'), + InfoTextWidget(label: "Severity", value: alertModel.severity ?? '-'), + if (alertModel.enter != null) InfoTextWidget(label: "Direction", value: alertModel.enter == true ? "Enter" : "Exit"), + const Divider().defaultStyle(context), + InfoHeader16Widget("Location"), + InfoTextWidget(label: context.translation.site, value: alertModel.campusName ?? '-'), + if (alertModel.venueName?.isNotEmpty ?? false) InfoTextWidget(label: "Venue", value: alertModel.venueName ?? '-'), + if (alertModel.floorName?.isNotEmpty ?? false) InfoTextWidget(label: context.translation.floor, value: alertModel.floorName ?? '-'), + if (zonesText.isNotEmpty) InfoTextWidget(label: "Zones", value: zonesText), + ], + ).expanded, + InfoDateWidget(alertModel.createdDate ?? alertModel.dateTime ?? ""), + ], + ), + ], + ).toShadowContainer(context, padding: 16); + } +} + + + + diff --git a/lib/modules/alerts/alert_item_view.dart b/lib/modules/alerts/alert_item_view.dart new file mode 100644 index 00000000..16e61298 --- /dev/null +++ b/lib/modules/alerts/alert_item_view.dart @@ -0,0 +1,50 @@ +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/widget_extensions.dart'; +import 'package:test_sa/modules/alerts/alert_detail_page.dart'; +import 'package:test_sa/modules/alerts/alert_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'; +import 'package:test_sa/views/widgets/requests/request_status.dart'; + +class AlertItemView extends StatelessWidget { + final AlertModel alert; + final bool showShadow; + + const AlertItemView({Key? key, required this.alert, this.showShadow = true}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StatusLabel( + label: alert.statusLabel, + textColor: AppColor.getRequestStatusTextColorByName(context, alert.statusLabel), + backgroundColor: AppColor.getRequestStatusColorByName(context, alert.statusLabel), + ), + 1.width.expanded, + InfoDateWidget(alert.createdDate ?? alert.dateTime ?? ""), + ], + ), + InfoHeaderWidget(alert.ruleName ?? "Alert"), + InfoTextWidget(label: "Severity", value: alert.severity), + InfoTextWidget(label: context.translation.site, value: alert.campusName), + InfoTextWidget(label: "Alert No", value: alert.alertId, showArrow: true), + ], + ).toShadowContainer(context, withShadow: showShadow).onPress(() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => AlertDetailPage(alertId: alert.id!), + ), + ); + }); + } +} + diff --git a/lib/modules/alerts/alert_model.dart b/lib/modules/alerts/alert_model.dart new file mode 100644 index 00000000..a959400f --- /dev/null +++ b/lib/modules/alerts/alert_model.dart @@ -0,0 +1,132 @@ +class AlertZone { + int? zoneId; + String? zoneName; + + AlertZone({this.zoneId, this.zoneName}); + + AlertZone.fromJson(Map json) { + zoneId = json['zoneId']; + zoneName = json['zoneName']; + } + + Map toJson() { + final Map data = {}; + data['zoneId'] = zoneId; + data['zoneName'] = zoneName; + return data; + } +} + +class AlertModel { + int? id; + int? ruleId; + String? alertId; + String? ruleName; + String? ruleTriggerType; + String? severity; + String? dateTime; + bool? resolved; + bool? enter; + int? entityId; + int? campusId; + String? campusName; + int? venueId; + String? venueName; + int? floorId; + String? floorName; + List? zones; + int? geofenceId; + dynamic idleThreshold; + dynamic count; + dynamic externalJson; + String? createdDate; + + AlertModel({ + this.id, + this.ruleId, + this.alertId, + this.ruleName, + this.ruleTriggerType, + this.severity, + this.dateTime, + this.resolved, + this.enter, + this.entityId, + this.campusId, + this.campusName, + this.venueId, + this.venueName, + this.floorId, + this.floorName, + this.zones, + this.geofenceId, + this.idleThreshold, + this.count, + this.externalJson, + this.createdDate, + }); + + AlertModel.fromJson(Map json) { + id = json['id']; + ruleId = json['ruleId']; + alertId = json['alertId']; + ruleName = json['ruleName']; + ruleTriggerType = json['ruleTriggerType']; + severity = json['severity']; + dateTime = json['dateTime']; + resolved = json['resolved']; + enter = json['enter']; + entityId = json['entityId']; + campusId = json['campusId']; + campusName = json['campusName']; + venueId = json['venueId']; + venueName = json['venueName']; + floorId = json['floorId']; + floorName = json['floorName']; + geofenceId = json['geofenceId']; + idleThreshold = json['idleThreshold']; + count = json['count']; + externalJson = json['externalJson']; + createdDate = json['createdDate']; + if (json['zones'] != null) { + zones = []; + (json['zones'] as List).forEach((v) { + zones!.add(AlertZone.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['ruleId'] = ruleId; + data['alertId'] = alertId; + data['ruleName'] = ruleName; + data['ruleTriggerType'] = ruleTriggerType; + data['severity'] = severity; + data['dateTime'] = dateTime; + data['resolved'] = resolved; + data['enter'] = enter; + data['entityId'] = entityId; + data['campusId'] = campusId; + data['campusName'] = campusName; + data['venueId'] = venueId; + data['venueName'] = venueName; + data['floorId'] = floorId; + data['floorName'] = floorName; + data['geofenceId'] = geofenceId; + data['idleThreshold'] = idleThreshold; + data['count'] = count; + data['externalJson'] = externalJson; + data['createdDate'] = createdDate; + if (zones != null) { + data['zones'] = zones!.map((v) => v.toJson()).toList(); + } + return data; + } + + bool get isResolved => resolved == true; + + String get statusLabel => isResolved ? "Resolved" : "Open"; +} + diff --git a/lib/modules/alerts/alert_provider.dart b/lib/modules/alerts/alert_provider.dart new file mode 100644 index 00000000..3d8609bc --- /dev/null +++ b/lib/modules/alerts/alert_provider.dart @@ -0,0 +1,96 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +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/modules/alerts/alert_model.dart'; + +class AlertProvider extends ChangeNotifier { + final pageItemNumber = 12; + + void reset() { + items = null; + nextPage = true; + stateCode = null; + } + + int? stateCode; + + bool nextPage = true; + + List? items; + + bool isLoading = false; + + AlertModel? alertModel; + + Future getAlerts() async { + if (isLoading == true) return -2; + isLoading = true; + if (items?.isEmpty ?? true) notifyListeners(); + late Response response; + + Map body = {}; + body["pageNumber"] = (items?.length ?? 0) ~/ pageItemNumber + 1; + body["pageSize"] = pageItemNumber; + + try { + response = await ApiManager.instance.post(URLs.getAlerts, body: body); + stateCode = response.statusCode; + if (stateCode! >= 200 && stateCode! < 300) { + final decoded = json.decode(response.body); + final List requestsListJson = (decoded is Map && decoded.containsKey('data')) ? decoded['data'] : decoded; + List itemsPage = requestsListJson.map((request) => AlertModel.fromJson(request)).toList(); + items ??= []; + items!.addAll(itemsPage); + nextPage = itemsPage.length == pageItemNumber; + } + isLoading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + isLoading = false; + stateCode = -1; + notifyListeners(); + return -1; + } + } + + Future getAlertById(num id, {bool refresh = false}) async { + if (refresh) { + return alertModel; + } + try { + alertModel = null; + Response response = await ApiManager.instance.get("${URLs.getAlertById}/$id"); + if (response.statusCode >= 200 && response.statusCode < 300) { + final decoded = json.decode(response.body); + final Map dataNode = (decoded is Map && decoded.containsKey('data')) ? decoded['data'] : decoded; + alertModel = AlertModel.fromJson(dataNode); + notifyListeners(); + return alertModel; + } + return null; + } catch (error) { + return null; + } + } + + Future resolveAlert(num id) async { + try { + Response response = await ApiManager.instance.post(URLs.resolveAlert, body: {"id": id}); + if (response.statusCode >= 200 && response.statusCode < 300) { + if (items != null) { + reset(); + } + return true; + } + return false; + } catch (error) { + return false; + } + } +} + + diff --git a/lib/modules/alerts/alerts_list_page.dart b/lib/modules/alerts/alerts_list_page.dart new file mode 100644 index 00000000..fc7b4020 --- /dev/null +++ b/lib/modules/alerts/alerts_list_page.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/modules/alerts/alert_item_view.dart'; +import 'package:test_sa/modules/alerts/alert_provider.dart'; +import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; +import 'package:test_sa/views/widgets/loaders/lazy_loading.dart'; +import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; + +class AlertsListPage extends StatefulWidget { + final bool fromBottomBar; + + const AlertsListPage({Key? key, this.fromBottomBar = false}) : super(key: key); + + @override + State createState() => _AlertsListPageState(); +} + +class _AlertsListPageState extends State { + AlertProvider? _provider; + + @override + Widget build(BuildContext context) { + _provider = Provider.of(context); + if (_provider!.items == null && !_provider!.isLoading) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _provider!.getAlerts(); + }); + } + + return Scaffold( + appBar: DefaultAppBar(title: "Alerts", showBackButton: !widget.fromBottomBar), + body: Consumer(builder: (context, snapshot, _) { + final list = snapshot.items ?? []; + if (list.isEmpty && !snapshot.isLoading) { + return NoDataFound(message: context.translation.noDataFound).center; + } + return LazyLoading( + nextPage: snapshot.nextPage, + onLazyLoad: () async { + await snapshot.getAlerts(); + }, + child: ListView.separated( + padding: const EdgeInsets.all(16), + itemBuilder: (cxt, index) { + if (index >= list.length) return const SizedBox(); + return AlertItemView(alert: list[index]); + }, + separatorBuilder: (cxt, index) => const SizedBox(height: 12), + itemCount: list.length, + ), + ); + }), + ); + } +} + diff --git a/lib/new_views/common_widgets/app_bottom_nav_bar.dart b/lib/new_views/common_widgets/app_bottom_nav_bar.dart index 001cb8b0..a4346455 100644 --- a/lib/new_views/common_widgets/app_bottom_nav_bar.dart +++ b/lib/new_views/common_widgets/app_bottom_nav_bar.dart @@ -26,7 +26,8 @@ class AppBottomNavigationBar extends StatelessWidget { if(!isHouseKeeper) NavItemModel(1, "request_icon", context.translation.workOrder), if (!commissioningEng && !isHouseKeeper) NavItemModel(2, "add_icon", context.translation.calendar, showLabel: false), if(!isHouseKeeper) NavItemModel(3, "assets", context.translation.assets), - NavItemModel(4, "contact", context.translation.contact), + NavItemModel(4, "warning", "Alerts"), + NavItemModel(5, "contact", context.translation.contact), ]; return Container( diff --git a/lib/new_views/common_widgets/app_drawer.dart b/lib/new_views/common_widgets/app_drawer.dart index 2b17ddcd..6510850c 100644 --- a/lib/new_views/common_widgets/app_drawer.dart +++ b/lib/new_views/common_widgets/app_drawer.dart @@ -75,8 +75,8 @@ class AppDrawer extends StatelessWidget { // 18.height, // drawerItem("rate_us", context.translation.rateUs, context), // 18.height, - drawerItem("asset_tracking", "Asset Tracking", context).onPress(() => Navigator.push(context, MaterialPageRoute(builder: (context) => AssetTrackingPage()))), - 18.height, + // drawerItem("asset_tracking", "Asset Tracking", context).onPress(() => Navigator.push(context, MaterialPageRoute(builder: (context) => AssetTrackingPage()))), + // 18.height, drawerItem("setting", context.translation.settings, context).onPress(() => Navigator.of(context).pushNamed(SettingsPage.id)), 18.height, drawerItem("site_request_icon", "Site Request", context).onPress(() => Navigator.of(context).pushNamed(SiteRequestHistoryScreen.routeName)), diff --git a/lib/new_views/pages/land_page/land_page.dart b/lib/new_views/pages/land_page/land_page.dart index bd475186..dd9f235d 100644 --- a/lib/new_views/pages/land_page/land_page.dart +++ b/lib/new_views/pages/land_page/land_page.dart @@ -15,6 +15,8 @@ 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/models/enums/user_types.dart'; +import 'package:test_sa/modules/alerts/alert_provider.dart'; +import 'package:test_sa/modules/alerts/alerts_list_page.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/pages/land_page/create_request_type_bottom_sheet.dart'; @@ -130,6 +132,7 @@ class _LandPageState extends State { const SizedBox(), // if (_userProvider!.user!.type != UsersTypes.engineer) const CalendarPage(), const MyAssetsPage(fromBottomBar: true), + const AlertsListPage(fromBottomBar: true), ]; if (isFCM) { FirebaseNotificationManger.initialized(context); @@ -183,7 +186,7 @@ class _LandPageState extends State { backgroundColor: Colors.white, builder: (context) => CreateRequestTypeBottomSheet(), ); - } else if (index == 4) { + } else if (index == 5) { showModalBottomSheet( context: context, useSafeArea: true, @@ -191,6 +194,10 @@ class _LandPageState extends State { builder: (context) => const ContactUsBottomSheet(), ); } else { + if (index == 4) { + Provider.of(context, listen: false).reset(); + Provider.of(context, listen: false).getAlerts(); + } setState(() { currentPageIndex = index; }); diff --git a/lib/views/widgets/equipment/my_assets_page.dart b/lib/views/widgets/equipment/my_assets_page.dart index 37df435b..e5a0965a 100644 --- a/lib/views/widgets/equipment/my_assets_page.dart +++ b/lib/views/widgets/equipment/my_assets_page.dart @@ -10,6 +10,7 @@ import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/modules/asset_tracking/asset_tracking_page.dart'; import 'package:test_sa/modules/tm_module/device_transfer/device_filter_screen.dart'; import 'package:test_sa/modules/tm_module/device_transfer/device_search_screen.dart'; import 'package:test_sa/modules/export_module/pages/export_form_view.dart'; @@ -131,7 +132,18 @@ class _MyAssetsPageState extends State with SingleTickerProviderSt _searchableList.clear(); _searchableList.addAll(_devicesProvider.devices); }).expanded, - 12.width, + // 12.width, + IconButton( + icon:'asset_tracking'.toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)), + padding: EdgeInsets.zero, + iconSize: 20, + visualDensity: VisualDensity.compact, + tooltip: "Asset Tracking", + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => AssetTrackingPage())); + }, + ), + // 12.width, IconButton( icon: Icon(!showListView ? Icons.view_list_sharp : Icons.grid_view_outlined, color: AppColor.icon2Color(context)), padding: EdgeInsets.zero,