alerts added and move asset tracking to asset page
parent
caee6edf1b
commit
9c4fd1088c
@ -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<AlertDetailPage> createState() => _AlertDetailPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlertDetailPageState extends State<AlertDetailPage> {
|
||||||
|
AlertModel _model = AlertModel();
|
||||||
|
late AlertProvider alertProvider;
|
||||||
|
bool isDataUpdated = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
alertProvider = Provider.of<AlertProvider>(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<String>()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -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!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,132 @@
|
|||||||
|
class AlertZone {
|
||||||
|
int? zoneId;
|
||||||
|
String? zoneName;
|
||||||
|
|
||||||
|
AlertZone({this.zoneId, this.zoneName});
|
||||||
|
|
||||||
|
AlertZone.fromJson(Map<String, dynamic> json) {
|
||||||
|
zoneId = json['zoneId'];
|
||||||
|
zoneName = json['zoneName'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
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<AlertZone>? 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<String, dynamic> 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 = <AlertZone>[];
|
||||||
|
(json['zones'] as List).forEach((v) {
|
||||||
|
zones!.add(AlertZone.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
@ -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<AlertModel>? items;
|
||||||
|
|
||||||
|
bool isLoading = false;
|
||||||
|
|
||||||
|
AlertModel? alertModel;
|
||||||
|
|
||||||
|
Future<int> getAlerts() async {
|
||||||
|
if (isLoading == true) return -2;
|
||||||
|
isLoading = true;
|
||||||
|
if (items?.isEmpty ?? true) notifyListeners();
|
||||||
|
late Response response;
|
||||||
|
|
||||||
|
Map<String, dynamic> 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<AlertModel> 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<AlertModel?> 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<String, dynamic> dataNode = (decoded is Map && decoded.containsKey('data')) ? decoded['data'] : decoded;
|
||||||
|
alertModel = AlertModel.fromJson(dataNode);
|
||||||
|
notifyListeners();
|
||||||
|
return alertModel;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -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<AlertsListPage> createState() => _AlertsListPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlertsListPageState extends State<AlertsListPage> {
|
||||||
|
AlertProvider? _provider;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
_provider = Provider.of<AlertProvider>(context);
|
||||||
|
if (_provider!.items == null && !_provider!.isLoading) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_provider!.getAlerts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: DefaultAppBar(title: "Alerts", showBackButton: !widget.fromBottomBar),
|
||||||
|
body: Consumer<AlertProvider>(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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue