Notification system enhanced
- Base for notifcation center - Show text in map if no data is available
This commit is contained in:
@@ -10,6 +10,7 @@ import * as winston from 'winston';
|
|||||||
|
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
import { GetBeat, GetBeatStats } from './endpoints/beat';
|
import { GetBeat, GetBeatStats } from './endpoints/beat';
|
||||||
|
import { getNotification } from './endpoints/notification';
|
||||||
import { GetPhone, PostPhone } from './endpoints/phone';
|
import { GetPhone, PostPhone } from './endpoints/phone';
|
||||||
import { DeleteUser, GetUser, LoginRabbitUser, LoginUser, MW_User, PatchUser, PostUser, Resource, Topic, VHost } from './endpoints/user';
|
import { DeleteUser, GetUser, LoginRabbitUser, LoginUser, MW_User, PatchUser, PostUser, Resource, Topic, VHost } from './endpoints/user';
|
||||||
import { hashPassword, randomPepper, randomString } from './lib/crypto';
|
import { hashPassword, randomPepper, randomString } from './lib/crypto';
|
||||||
@@ -145,17 +146,20 @@ async function run() {
|
|||||||
app.get('/user/resource', (req, res) => Resource(req, res));
|
app.get('/user/resource', (req, res) => Resource(req, res));
|
||||||
app.get('/user/topic', (req, res) => Topic(req, res));
|
app.get('/user/topic', (req, res) => Topic(req, res));
|
||||||
|
|
||||||
// Basic user actions
|
// CRUD user
|
||||||
|
app.get('/user/notification', MW_User, (req, res) => getNotification(req, res)); // Notifications
|
||||||
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
|
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
|
||||||
app.post('/user/', MW_User, (req, res) => PostUser(req, res));
|
app.post('/user/', MW_User, (req, res) => PostUser(req, res));
|
||||||
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
||||||
app.patch('/user/:id', MW_User, (req, res) => PatchUser(req, res));
|
app.patch('/user/:id', MW_User, (req, res) => PatchUser(req, res));
|
||||||
app.delete('/user/:id', MW_User, (req, res) => DeleteUser(req, res));
|
app.delete('/user/:id', MW_User, (req, res) => DeleteUser(req, res));
|
||||||
|
|
||||||
|
// Phones
|
||||||
app.get('/phone/:id', MW_User, (req, res) => GetPhone(req, res));
|
app.get('/phone/:id', MW_User, (req, res) => GetPhone(req, res));
|
||||||
app.get('/phone', MW_User, (req, res) => GetPhone(req, res));
|
app.get('/phone', MW_User, (req, res) => GetPhone(req, res));
|
||||||
app.post('/phone', MW_User, (req, res) => PostPhone(req, res));
|
app.post('/phone', MW_User, (req, res) => PostPhone(req, res));
|
||||||
|
|
||||||
|
// Beats
|
||||||
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
|
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
|
||||||
app.get('/beat/stats', MW_User, (req, res) => GetBeatStats(req, res));
|
app.get('/beat/stats', MW_User, (req, res) => GetBeatStats(req, res));
|
||||||
|
|
||||||
|
|||||||
22
backend/endpoints/notification.ts
Normal file
22
backend/endpoints/notification.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
import { LivebeatRequest } from "../lib/request";
|
||||||
|
import { Notification } from "../models/notifications/notification.model";
|
||||||
|
|
||||||
|
export async function getNotification(req: LivebeatRequest, res: Response) {
|
||||||
|
let limit = req.query.limit;
|
||||||
|
let skip = req.query.skip;
|
||||||
|
|
||||||
|
if (limit === undefined) { limit = '100' };
|
||||||
|
if (skip === undefined) { skip = '0' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notifications = await Notification.find({ user: req.user?._id }).limit(Number(limit)).sort({ _id: -1 });
|
||||||
|
res.status(200).send(notifications);
|
||||||
|
} catch(error: any) {
|
||||||
|
if (error instanceof TypeError) {
|
||||||
|
res.status(400).send({ message: "'Limit' has to be a number" });
|
||||||
|
} else {
|
||||||
|
res.status(500).send({ message: "An error occured while processing your request" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import * as amqp from 'amqplib';
|
import * as amqp from 'amqplib';
|
||||||
|
import { Schema, SchemaType } from 'mongoose';
|
||||||
import { logger, RABBITMQ_URI } from '../app';
|
import { logger, RABBITMQ_URI } from '../app';
|
||||||
import { Beat } from '../models/beat/beat.model.';
|
import { Beat } from '../models/beat/beat.model.';
|
||||||
|
import { ISeverity, NotificationType } from '../models/notifications/notification.interface';
|
||||||
|
import { addNotification, Notification } from '../models/notifications/notification.model';
|
||||||
|
import { IPhone } from '../models/phone/phone.interface';
|
||||||
import { Phone } from '../models/phone/phone.model';
|
import { Phone } from '../models/phone/phone.model';
|
||||||
|
import { User } from '../models/user/user.model';
|
||||||
|
|
||||||
interface IBeat {
|
interface IBeat {
|
||||||
token: string,
|
token: string,
|
||||||
@@ -14,7 +19,7 @@ export class RabbitMQ {
|
|||||||
connection: amqp.Connection | null = null;
|
connection: amqp.Connection | null = null;
|
||||||
channel: amqp.Channel | null = null;
|
channel: amqp.Channel | null = null;
|
||||||
|
|
||||||
timeouts: Map<string, Timeout> = new Map<string, Timeout>();
|
timeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
this.connection = await amqp.connect(RABBITMQ_URI);
|
this.connection = await amqp.connect(RABBITMQ_URI);
|
||||||
@@ -46,16 +51,15 @@ export class RabbitMQ {
|
|||||||
|
|
||||||
// Broadcast if device became active
|
// Broadcast if device became active
|
||||||
if (this.timeouts.has(phone.id)) {
|
if (this.timeouts.has(phone.id)) {
|
||||||
clearTimeout(this.timeouts.get(phone.id));
|
clearTimeout(this.timeouts.get(phone.id)!!);
|
||||||
} else {
|
} else {
|
||||||
logger.debug('Set phone active');
|
|
||||||
phone.active = true;
|
phone.active = true;
|
||||||
await phone.save();
|
await phone.save();
|
||||||
this.publish(phone.user.toString(), phone.toJSON(), 'phone_alive');
|
this.publish(phone.user.toString(), phone.toJSON(), 'phone_alive', ISeverity.SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutTimer = setTimeout(async () => {
|
const timeoutTimer = setTimeout(async () => {
|
||||||
this.publish(phone.user.toString(), phone.toJSON(), 'phone_dead');
|
this.publish(phone.user.toString(), phone.toJSON(), 'phone_dead', ISeverity.WARN);
|
||||||
this.timeouts.delete(phone.id);
|
this.timeouts.delete(phone.id);
|
||||||
phone.active = false;
|
phone.active = false;
|
||||||
await phone.save();
|
await phone.save();
|
||||||
@@ -66,10 +70,22 @@ export class RabbitMQ {
|
|||||||
}, { noAck: true });
|
}, { noAck: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async publish(userId: string, data: any, type: 'beat' | 'phone_alive' | 'phone_dead' | 'phone_register' | 'panic' = 'beat') {
|
async publish(userId: string, data: any, type: NotificationType, severity = ISeverity.INFO) {
|
||||||
if (this.connection == undefined) await this.init()
|
if (this.connection == undefined) await this.init();
|
||||||
|
|
||||||
data = { type, ...data };
|
const user = await User.findById(userId);
|
||||||
|
if (user === null) return;
|
||||||
|
|
||||||
|
/* Manage notifications */
|
||||||
|
if (type != 'beat') {
|
||||||
|
if (type == 'phone_alive' || type == 'phone_dead') {
|
||||||
|
addNotification(type, severity, ((data as IPhone)._id), user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data = { type, severity, ...data };
|
||||||
|
console.log('Send:', data);
|
||||||
|
|
||||||
this.channel?.publish('amq.topic', userId, Buffer.from(JSON.stringify(data)));
|
this.channel?.publish('amq.topic', userId, Buffer.from(JSON.stringify(data)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ const schemaBeat = new Schema({
|
|||||||
accuracy: { type: Number, required: false },
|
accuracy: { type: Number, required: false },
|
||||||
speed: { type: Number, required: false },
|
speed: { type: Number, required: false },
|
||||||
battery: { type: Number, required: false },
|
battery: { type: Number, required: false },
|
||||||
phone: { type: SchemaTypes.ObjectId, required: true, default: 'user' },
|
phone: { type: SchemaTypes.ObjectId, required: true },
|
||||||
createdAt: { type: SchemaTypes.Date, required: false }
|
createdAt: { type: SchemaTypes.Date, required: false }
|
||||||
}, {
|
}, {
|
||||||
timestamps: {
|
timestamps: {
|
||||||
|
|||||||
18
backend/models/notifications/notification.interface.ts
Normal file
18
backend/models/notifications/notification.interface.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Document } from "mongoose";
|
||||||
|
import { IUser } from "../user/user.interface";
|
||||||
|
|
||||||
|
export enum ISeverity {
|
||||||
|
INFO = 0,
|
||||||
|
SUCCESS = 1,
|
||||||
|
WARN = 2,
|
||||||
|
ERROR = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationType = 'beat' | 'phone_alive' | 'phone_dead' | 'phone_register' | 'panic';
|
||||||
|
|
||||||
|
export interface INotification extends Document {
|
||||||
|
type: NotificationType;
|
||||||
|
severity: ISeverity;
|
||||||
|
message: any;
|
||||||
|
user: IUser;
|
||||||
|
}
|
||||||
11
backend/models/notifications/notification.model.ts
Normal file
11
backend/models/notifications/notification.model.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Model, model } from 'mongoose';
|
||||||
|
import { IUser } from '../user/user.interface';
|
||||||
|
import { INotification, ISeverity, NotificationType } from './notification.interface';
|
||||||
|
import { schemaNotification } from './notification.schema';
|
||||||
|
|
||||||
|
const modelNotification: Model<INotification> = model<INotification>('Notification', schemaNotification, 'Notification');
|
||||||
|
export { modelNotification as Notification };
|
||||||
|
|
||||||
|
export function addNotification(type: NotificationType, severity: ISeverity, message: any, user: IUser) {
|
||||||
|
return modelNotification.create({ type, severity, message, user });
|
||||||
|
}
|
||||||
15
backend/models/notifications/notification.schema.ts
Normal file
15
backend/models/notifications/notification.schema.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Schema, SchemaTypes } from 'mongoose';
|
||||||
|
const schemaNotification = new Schema({
|
||||||
|
type: { type: String, required: true },
|
||||||
|
severity: { type: Number, required: true },
|
||||||
|
message: { type: Object, required: true },
|
||||||
|
user: { type: SchemaTypes.ObjectId }
|
||||||
|
}, {
|
||||||
|
timestamps: {
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: false
|
||||||
|
},
|
||||||
|
versionKey: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export { schemaNotification };
|
||||||
7
backend/package-lock.json
generated
7
backend/package-lock.json
generated
@@ -188,10 +188,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "14.11.10",
|
"version": "14.14.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz",
|
||||||
"integrity": "sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA==",
|
"integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.9.5",
|
"version": "6.9.5",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"author": "Mondei1",
|
"author": "Mondei1",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/node": "^14.14.9",
|
||||||
"amqplib": "^0.6.0",
|
"amqplib": "^0.6.0",
|
||||||
"argon2": "^0.27.0",
|
"argon2": "^0.27.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES5",
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020"],
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./",
|
"rootDir": "./",
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ export class Alert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum AlertType {
|
export enum AlertType {
|
||||||
Success,
|
INFO = 0,
|
||||||
Error,
|
SUCCESS = 1,
|
||||||
Info,
|
WARN = 2,
|
||||||
Warning
|
ERROR = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -36,23 +36,30 @@ export class AlertService {
|
|||||||
|
|
||||||
// convenience methods
|
// convenience methods
|
||||||
success(message: string, title = 'Success', options?: any): void {
|
success(message: string, title = 'Success', options?: any): void {
|
||||||
this.alert(new Alert({ ...options, type: AlertType.Success, message, title }));
|
this.alert(new Alert({ ...options, type: AlertType.SUCCESS, message, title }));
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message: string, title = 'Error!', options?: any): void {
|
error(message: string, title = 'Error!', options?: any): void {
|
||||||
const audio = new Audio(this.errorSoundPath);
|
const audio = new Audio(this.errorSoundPath);
|
||||||
audio.load();
|
audio.load();
|
||||||
audio.play();
|
audio.play().catch(error => {});
|
||||||
|
|
||||||
this.alert(new Alert({ ...options, type: AlertType.Error, message, title }));
|
this.alert(new Alert({ ...options, type: AlertType.ERROR, message, title }));
|
||||||
}
|
}
|
||||||
|
|
||||||
info(message: string, title = 'Info', options?: any): void {
|
info(message: string, title = 'Info', options?: any): void {
|
||||||
this.alert(new Alert({ ...options, type: AlertType.Info, message, title }));
|
this.alert(new Alert({ ...options, type: AlertType.INFO, message, title }));
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(message: string, title = 'Warning!', options?: any): void {
|
warn(message: string, title = 'Warning!', options?: any): void {
|
||||||
this.alert(new Alert({ ...options, type: AlertType.Warning, message, title }));
|
this.alert(new Alert({ ...options, type: AlertType.WARN, message, title }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to use a variable as alert type.
|
||||||
|
*/
|
||||||
|
dynamic(message: string, type: AlertType, title = 'Dynamic', options?: any) {
|
||||||
|
this.alert(new Alert({ ...options, type, message, title }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// main alert method
|
// main alert method
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ export class AlertComponent implements OnInit, OnDestroy {
|
|||||||
const classes = ['alert', 'alert-dismissable'];
|
const classes = ['alert', 'alert-dismissable'];
|
||||||
|
|
||||||
const alertTypeClass = {
|
const alertTypeClass = {
|
||||||
[AlertType.Success]: 'alert alert-success',
|
[AlertType.SUCCESS]: 'alert alert-success',
|
||||||
[AlertType.Error]: 'alert alert-danger',
|
[AlertType.ERROR]: 'alert alert-danger',
|
||||||
[AlertType.Info]: 'alert alert-info',
|
[AlertType.INFO]: 'alert alert-info',
|
||||||
[AlertType.Warning]: 'alert alert-warning'
|
[AlertType.WARN]: 'alert alert-warning'
|
||||||
};
|
};
|
||||||
|
|
||||||
classes.push(alertTypeClass[alert.type]);
|
classes.push(alertTypeClass[alert.type]);
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import * as moment from 'moment';
|
|||||||
import { error } from 'protractor';
|
import { error } from 'protractor';
|
||||||
import { AlertService } from './_alert/alert.service';
|
import { AlertService } from './_alert/alert.service';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DEFINITION OF TYPE
|
||||||
|
*/
|
||||||
export interface ILogin {
|
export interface ILogin {
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
@@ -65,6 +68,26 @@ export interface ITimespan {
|
|||||||
to?: number;
|
to?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ISeverity {
|
||||||
|
INFO = 0,
|
||||||
|
SUCCESS = 1,
|
||||||
|
WARN = 2,
|
||||||
|
ERROR = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationType = 'beat' | 'phone_alive' | 'phone_dead' | 'phone_register' | 'panic';
|
||||||
|
|
||||||
|
export interface INotification extends Document {
|
||||||
|
type: NotificationType;
|
||||||
|
severity: ISeverity;
|
||||||
|
message: any;
|
||||||
|
user: IUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* END OF THE DEFINITION OF TYPE
|
||||||
|
*/
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
@@ -74,10 +97,6 @@ export class APIService {
|
|||||||
|
|
||||||
username: string;
|
username: string;
|
||||||
rabbitmq: any;
|
rabbitmq: any;
|
||||||
time: ITimespan | undefined = {
|
|
||||||
from: moment().subtract(1, 'day').unix(),
|
|
||||||
to: moment().unix()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Passthough data (not useful for api but a way for components to share data)
|
// Passthough data (not useful for api but a way for components to share data)
|
||||||
showFilter = true;
|
showFilter = true;
|
||||||
@@ -99,6 +118,7 @@ export class APIService {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
twoFASecret: ''
|
twoFASecret: ''
|
||||||
};
|
};
|
||||||
|
notifications: INotification[];
|
||||||
|
|
||||||
// Events when new data got fetched
|
// Events when new data got fetched
|
||||||
beatsEvent: BehaviorSubject<IBeat[]> = new BehaviorSubject([]);
|
beatsEvent: BehaviorSubject<IBeat[]> = new BehaviorSubject([]);
|
||||||
@@ -121,30 +141,26 @@ export class APIService {
|
|||||||
password: this.user.brokerToken
|
password: this.user.brokerToken
|
||||||
});
|
});
|
||||||
|
|
||||||
this.mqtt.observe(this.user._id).subscribe(message => {
|
this.mqtt.observe(this.user._id).subscribe(async message => {
|
||||||
if (message !== undefined || message !== null) {
|
if (message !== undefined || message !== null) {
|
||||||
const obj = JSON.parse(message.payload.toString());
|
const obj = JSON.parse(message.payload.toString());
|
||||||
|
console.log('Received message:', obj);
|
||||||
|
|
||||||
switch (obj.type) {
|
if (obj.type === 'beat') {
|
||||||
case 'beat':
|
if (this.beats !== undefined) {
|
||||||
if (this.beats !== undefined) {
|
this.beats.push(obj);
|
||||||
this.beats.push(obj);
|
this.beatsEvent.next([obj]); // We just push one, so the map doesn't has to rebuild everything from scratch.
|
||||||
this.beatsEvent.next([obj]); // We just push one, so the map doesn't has to rebuild everything from scratch.
|
this.beatStats.totalBeats++;
|
||||||
this.beatStats.totalBeats++;
|
}
|
||||||
}
|
} else if (obj.type === 'phone_available') {
|
||||||
break;
|
this.alert.dynamic(`Device ${obj.displayName} is now online`, obj.severity, 'Device');
|
||||||
case 'phone_available':
|
} else if (obj.type === 'phone_register') {
|
||||||
this.alert.info(`Device ${obj.name} is now online`, 'Device');
|
await this.getPhones();
|
||||||
break;
|
this.alert.dynamic(`New device "${obj.displayName}"`, obj.severity, 'New device');
|
||||||
case 'phone_register':
|
} else if (obj.type === 'phone_alive') {
|
||||||
this.alert.success(`New device "${obj.displayName}"`, 'New device');
|
this.alert.dynamic('Device is now active', obj.severity, obj.displayName);
|
||||||
break;
|
} else if (obj.type === 'phone_dead') {
|
||||||
case 'phone_alive':
|
this.alert.dynamic('Device is now offline', obj.severity, obj.displayName);
|
||||||
this.alert.info('Device is now active', obj.displayName.toString());
|
|
||||||
break;
|
|
||||||
case 'phone_dead':
|
|
||||||
this.alert.warn('Device is now offline', obj.displayName.toString());
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -163,6 +179,7 @@ export class APIService {
|
|||||||
this.username = username;
|
this.username = username;
|
||||||
await this.getPhones();
|
await this.getPhones();
|
||||||
await this.getUserInfo();
|
await this.getUserInfo();
|
||||||
|
await this.getNotifications();
|
||||||
|
|
||||||
this.mqttInit();
|
this.mqttInit();
|
||||||
|
|
||||||
@@ -208,20 +225,25 @@ export class APIService {
|
|||||||
/*
|
/*
|
||||||
BEATS
|
BEATS
|
||||||
*/
|
*/
|
||||||
async getBeats(): Promise<IBeat[]> {
|
async getBeats(time?: ITimespan): Promise<IBeat[]> {
|
||||||
return new Promise<IBeat[]>((resolve, reject) => {
|
return new Promise<IBeat[]>((resolve, reject) => {
|
||||||
if (this.token === undefined) { reject([]); }
|
if (this.token === undefined) { reject([]); }
|
||||||
|
|
||||||
this.fetchingDataEvent.next(true);
|
this.fetchingDataEvent.next(true);
|
||||||
|
|
||||||
|
// If time is not specified, default to 'today'
|
||||||
|
if (time === undefined) {
|
||||||
|
time = { from: moment().startOf('day').unix(), to: moment().unix() };
|
||||||
|
}
|
||||||
|
|
||||||
const headers = new HttpHeaders({ token: this.token });
|
const headers = new HttpHeaders({ token: this.token });
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
|
|
||||||
if (this.time !== undefined) {
|
if (time !== undefined) {
|
||||||
params = params.set('from', this.time.from.toString());
|
params = params.set('from', time.from.toString());
|
||||||
|
|
||||||
if (this.time.to !== 0) {
|
if (time.to !== 0) {
|
||||||
params = params.set('to', this.time.to.toString());
|
params = params.set('to', time.to.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +336,10 @@ export class APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch information about one phone from API and cache them locally.
|
||||||
|
* @param phoneId Object id of target phone
|
||||||
|
*/
|
||||||
async getPhone(phoneId: string): Promise<{ IPhone, IBeat }> {
|
async getPhone(phoneId: string): Promise<{ IPhone, IBeat }> {
|
||||||
return new Promise<{ IPhone, IBeat }>((resolve, reject) => {
|
return new Promise<{ IPhone, IBeat }>((resolve, reject) => {
|
||||||
if (this.token === undefined) { reject([]); }
|
if (this.token === undefined) { reject([]); }
|
||||||
@@ -330,6 +356,35 @@ export class APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function searchs for the target phone in local array.
|
||||||
|
*
|
||||||
|
* **Notice:** If you want to fetch data again from the server consider using `getPhone(...)`.
|
||||||
|
* @param phoneId Object id of target phone
|
||||||
|
*/
|
||||||
|
getPhoneFromCache(phoneId: string): IPhone | undefined {
|
||||||
|
return this.phones.find((phone, i, array) => {
|
||||||
|
if (phone._id === phoneId) { return true; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotifications(): Promise<INotification[]> {
|
||||||
|
return new Promise<INotification[]>((resolve, reject) => {
|
||||||
|
if (!this.hasSession()) { resolve([]); }
|
||||||
|
|
||||||
|
const headers = new HttpHeaders({ token: this.token });
|
||||||
|
|
||||||
|
this.fetchingDataEvent.next(true);
|
||||||
|
|
||||||
|
this.httpClient.get(this.API_ENDPOINT + '/user/notification', { responseType: 'json', headers })
|
||||||
|
.subscribe((notifications: INotification[]) => {
|
||||||
|
this.notifications = notifications;
|
||||||
|
this.fetchingDataEvent.next(false);
|
||||||
|
resolve(notifications);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* HELPER CLASSES */
|
/* HELPER CLASSES */
|
||||||
degreesToRadians(degrees: number): number {
|
degreesToRadians(degrees: number): number {
|
||||||
return degrees * Math.PI / 180;
|
return degrees * Math.PI / 180;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AppComponent } from './app.component';
|
|||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
import { LoginComponent } from './login/login.component';
|
import { LoginComponent } from './login/login.component';
|
||||||
import { MapComponent } from './map/map.component';
|
import { MapComponent } from './map/map.component';
|
||||||
|
import { NotificationsComponent } from './notifications/notifications.component';
|
||||||
import { UserComponent } from './user/user.component';
|
import { UserComponent } from './user/user.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -24,6 +25,10 @@ const routes: Routes = [
|
|||||||
path: 'map',
|
path: 'map',
|
||||||
component: MapComponent
|
component: MapComponent
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'notifications',
|
||||||
|
component: NotificationsComponent
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'user/:id',
|
path: 'user/:id',
|
||||||
component: UserComponent
|
component: UserComponent
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
<li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active">Dashboard</a></li>
|
<li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active">Dashboard</a></li>
|
||||||
<li><a [routerLink]="['/map']" routerLinkActive="router-link-active">Map</a></li>
|
<li><a [routerLink]="['/map']" routerLinkActive="router-link-active">Map</a></li>
|
||||||
|
|
||||||
|
<li class="navbar-right"><a [routerLink]="['/notifications']">
|
||||||
|
<img src="assets/message.svg">
|
||||||
|
</a></li>
|
||||||
<li class="navbar-right"><a [routerLink]="['/user', this.api.user._id]"
|
<li class="navbar-right"><a [routerLink]="['/user', this.api.user._id]"
|
||||||
routerLinkActive="router-link-active">
|
routerLinkActive="router-link-active">
|
||||||
<fa-icon [icon]="faUser"></fa-icon>
|
<fa-icon [icon]="faUser"></fa-icon>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { environment } from '../environments/environment';
|
|||||||
import { AdminComponent } from './admin/admin.component';
|
import { AdminComponent } from './admin/admin.component';
|
||||||
import { AlertComponent } from './_alert/alert/alert.component';
|
import { AlertComponent } from './_alert/alert/alert.component';
|
||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { NotificationsComponent } from './notifications/notifications.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -31,7 +32,8 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
|||||||
UserComponent,
|
UserComponent,
|
||||||
DashboardWidgetComponent,
|
DashboardWidgetComponent,
|
||||||
AdminComponent,
|
AdminComponent,
|
||||||
AlertComponent
|
AlertComponent,
|
||||||
|
NotificationsComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
|||||||
@@ -12,40 +12,47 @@ export class FilterComponent implements OnInit {
|
|||||||
presetHours = -1;
|
presetHours = -1;
|
||||||
customRange: any;
|
customRange: any;
|
||||||
customUnit: moment.unitOfTime.DurationConstructor;
|
customUnit: moment.unitOfTime.DurationConstructor;
|
||||||
|
timeRange: ITimespan; // Differs to customRange since this is the actual object used for query.
|
||||||
|
|
||||||
constructor(public api: APIService) { }
|
constructor(public api: APIService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
update(value: number): void {
|
/**
|
||||||
let result: ITimespan | undefined = { to: 0, from: 0 };
|
* @param value Time in hours.
|
||||||
|
* Except: `-1`: Today,
|
||||||
|
* `-2`: Custom time range,
|
||||||
|
* `0`: All time
|
||||||
|
*/
|
||||||
|
update(): void {
|
||||||
|
let time: ITimespan = { from: 0, to: 0 };
|
||||||
console.log(this.customRange, this.customUnit, this.presetHours);
|
console.log(this.customRange, this.customUnit, this.presetHours);
|
||||||
|
|
||||||
if (this.presetHours == -2) {
|
if (this.presetHours == -2) {
|
||||||
if (this.customRange !== undefined && this.customUnit !== undefined) {
|
if (this.customRange !== undefined && this.customUnit !== undefined) {
|
||||||
result.from = moment().subtract(this.customRange, this.customUnit).unix();
|
time.from = moment().subtract(this.customRange, this.customUnit).unix();
|
||||||
console.log(result.from);
|
|
||||||
}
|
}
|
||||||
} else if (this.presetHours == -1) {
|
} else if (this.presetHours == -1) {
|
||||||
result.from = moment().startOf('day').unix();
|
time.from = moment().startOf('day').unix();
|
||||||
} else if (this.presetHours == 0) {
|
} else if (this.presetHours == 0) {
|
||||||
result = undefined;
|
// Do nothing since we want `from` to stay zero.
|
||||||
} else {
|
} else {
|
||||||
result.from = moment().subtract(this.presetHours, 'hours').unix();
|
time.from = moment().subtract(this.presetHours, 'hours').unix();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(result);
|
time.to = moment().unix();
|
||||||
this.api.time = result;
|
this.timeRange = time;
|
||||||
this.refresh();
|
|
||||||
|
this.refresh(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAccuracy(val: any): void {
|
updateAccuracy(val: any): void {
|
||||||
this.api.maxAccuracy.next(Number(val.target.value));
|
this.api.maxAccuracy.next(Number(val.target.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh(): Promise<void> {
|
async refresh(time: ITimespan): Promise<void> {
|
||||||
await this.api.getBeats();
|
await this.api.getBeats(time);
|
||||||
await this.api.getBeatStats();
|
await this.api.getBeatStats();
|
||||||
await this.api.getPhones();
|
await this.api.getPhones();
|
||||||
await this.api.getUserInfo();
|
await this.api.getUserInfo();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ addEventListener('message', ({ data }) => {
|
|||||||
progress++;
|
progress++;
|
||||||
|
|
||||||
// Report progress every fifth loop
|
// Report progress every fifth loop
|
||||||
if (Math.trunc(progress / data.length * 100) % 5 === 0) {
|
if (Math.trunc(progress / data.length * 100) % 3 === 0) {
|
||||||
postMessage({ progress: (progress / data.length) * 100 });
|
postMessage({ progress: (progress / data.length) * 100 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +49,5 @@ addEventListener('message', ({ data }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`worker response to`, mostVisit);
|
|
||||||
postMessage(mostVisit);
|
postMessage(mostVisit);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,4 +36,8 @@
|
|||||||
position="top-right"
|
position="top-right"
|
||||||
></mgl-control>
|
></mgl-control>
|
||||||
</div>
|
</div>
|
||||||
</mgl-map>
|
</mgl-map>
|
||||||
|
<div id="noData" *ngIf="!this.showMap">
|
||||||
|
<h1>There is no data</h1><br>
|
||||||
|
<p>Selected time span does not contain any coordiantes.</p>
|
||||||
|
</div>
|
||||||
@@ -5,4 +5,13 @@ mgl-map {
|
|||||||
left: 0;
|
left: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
#noData {
|
||||||
|
position: fixed;
|
||||||
|
top: 35vh;
|
||||||
|
text-align: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
@@ -161,8 +161,12 @@ export class MapComponent {
|
|||||||
buildMap(isUpdate: boolean, maxAccuracy: number = 30): void {
|
buildMap(isUpdate: boolean, maxAccuracy: number = 30): void {
|
||||||
// If this is an update don't rebuild entire map.
|
// If this is an update don't rebuild entire map.
|
||||||
if (!isUpdate) {
|
if (!isUpdate) {
|
||||||
if (this.api.beats.length === 0) {
|
if (this.api.beats !== undefined) {
|
||||||
console.warn('Build map method was called while there are no beats!');
|
if (this.api.beats.length === 0) {
|
||||||
|
console.warn('Build map method was called while there are no beats!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div id="notifications">
|
||||||
|
<h1>Notifications</h1>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#notifications {
|
||||||
|
margin-left: 3rem;
|
||||||
|
margin-right: 3rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NotificationsComponent } from './notifications.component';
|
||||||
|
|
||||||
|
describe('NotificationsComponent', () => {
|
||||||
|
let component: NotificationsComponent;
|
||||||
|
let fixture: ComponentFixture<NotificationsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ NotificationsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NotificationsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
25
frontend/src/app/notifications/notifications.component.ts
Normal file
25
frontend/src/app/notifications/notifications.component.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { APIService } from '../api.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-notifications',
|
||||||
|
templateUrl: './notifications.component.html',
|
||||||
|
styleUrls: ['./notifications.component.scss']
|
||||||
|
})
|
||||||
|
export class NotificationsComponent implements OnInit, AfterContentInit, OnDestroy {
|
||||||
|
|
||||||
|
constructor(private api: APIService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterContentInit(): void {
|
||||||
|
this.api.showFilter = false;
|
||||||
|
console.log(this.api.showFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.api.showFilter = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
8
frontend/src/assets/message.svg
Normal file
8
frontend/src/assets/message.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message" width="19" height="19" viewBox="0 0 24 24" stroke-width="2" stroke="#fff" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4" />
|
||||||
|
<line x1="8" y1="9" x2="16" y2="9" />
|
||||||
|
<line x1="8" y1="13" x2="14" y2="13" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 446 B |
Reference in New Issue
Block a user