Current state

This commit is contained in:
Leon Meier 2025-01-17 23:02:39 +01:00
parent fa06c402e6
commit 534e240eff
24 changed files with 18717 additions and 6974 deletions

File diff suppressed because one or more lines are too long

View File

@ -7,20 +7,43 @@ Project "AssetFlow" {
Note: ''
}
Table AlarmContacts {
Table alerts {
id Int [pk, increment]
type alertType [not null]
message String
actionplan actionPlan
actionplanId Int
date DateTime [not null]
state alertState [not null]
acknowledged_by alertContacts [not null]
acknowledged_at DateTime
}
Table alertContacts {
id Int [pk, increment]
name String [not null]
phone String [unique, not null]
comment String
Prios Priorities [not null]
prios priorities [not null]
alerts alerts [not null]
}
Table Priorities {
Table actionPlan {
id Int [pk, increment]
Contact AlarmContacts [not null]
name String [not null]
comment String
alert_hook String [unique, not null]
prio priorities [not null]
content content [not null]
alerts alerts [not null]
}
Table priorities {
id Int [pk, increment]
Contact alertContacts [not null]
contactId Int [not null]
priority Int [not null]
actionplan ActionPlan [not null]
actionplan actionPlan [not null]
actionplanId Int [not null]
indexes {
@ -28,25 +51,22 @@ Table Priorities {
}
}
Table ActionPlan {
id Int [pk, increment]
name String [not null]
comment String
prio Priorities [not null]
content Content [not null]
}
Table Content {
Table content {
id Int [pk, increment]
type contentType [not null]
name String [not null]
filename String [not null]
actionplan ActionPlan [not null]
actionplan actionPlan [not null]
}
Table ActionPlanToContent {
contentId Int [ref: > Content.id]
actionplanId Int [ref: > ActionPlan.id]
Table alertContactsToalerts {
acknowledged_byId Int [ref: > alertContacts.id]
alertsId Int [ref: > alerts.id]
}
Table actionPlanTocontent {
contentId Int [ref: > content.id]
actionplanId Int [ref: > actionPlan.id]
}
Enum contentType {
@ -56,6 +76,23 @@ Enum contentType {
voice_ending
}
Ref: Priorities.contactId > AlarmContacts.id
Enum alertType {
generic
fire
fault
intrusion
clear
}
Ref: Priorities.actionplanId > ActionPlan.id
Enum alertState {
incomming
running
failed
acknowledged
}
Ref: alerts.actionplanId > actionPlan.id
Ref: priorities.contactId > alertContacts.id
Ref: priorities.actionplanId > actionPlan.id

17
package-lock.json generated
View File

@ -10,6 +10,7 @@
"license": "GPL-3.0",
"dependencies": {
"@prisma/client": "^6.2.1",
"bootstrap-icons": "^1.11.3",
"bulma": "^1.0.3",
"eta": "^3.5.0",
"express": "^4.21.2",
@ -1644,6 +1645,22 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/bootstrap-icons": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",

View File

@ -39,6 +39,7 @@
},
"dependencies": {
"@prisma/client": "^6.2.1",
"bootstrap-icons": "^1.11.3",
"bulma": "^1.0.3",
"eta": "^3.5.0",
"express": "^4.21.2",

View File

@ -45,54 +45,71 @@ enum alertType {
clear
}
enum alertState {
incomming // Incomming alerts
running // Started calling
failed // Failed to get acknowledgement of any alertContacts
acknowledged // Some user acknowledged alert
}
model AlarmContacts {
model alerts {
id Int @id @unique @default(autoincrement())
type alertType
message String?
actionplan actionPlan? @relation(fields: [actionplanId], references: [id])
actionplanId Int?
date DateTime
state alertState
acknowledged_by alertContacts[]
acknowledged_at DateTime?
@@fulltext([message])
}
model alertContacts {
id Int @id @unique @default(autoincrement())
name String
phone String @unique
comment String?
Prios Priorities[]
prios priorities[]
alerts alerts[]
@@fulltext([name, phone, comment])
}
model Priorities {
id Int @id @unique @default(autoincrement())
Contact AlarmContacts @relation(fields: [contactId], references: [id])
contactId Int
priority Int
actionplan ActionPlan @relation(fields: [actionplanId], references: [id])
actionplanId Int
@@unique([priority, actionplanId]) // TODO: Does this work with sqlite???
}
model ActionPlan {
model actionPlan {
id Int @id @unique @default(autoincrement())
name String
comment String?
prio Priorities[]
content Content[] // aka. all voice files
Alerts Alerts[]
alert_hook String @unique
prio priorities[]
content content[] // aka. all voice files
alerts alerts[]
@@fulltext([name, comment])
}
model Content {
model priorities {
id Int @id @unique @default(autoincrement())
Contact alertContacts @relation(fields: [contactId], references: [id])
contactId Int
priority Int
actionplan actionPlan @relation(fields: [actionplanId], references: [id])
actionplanId Int
@@unique([priority, actionplanId])
}
model content {
id Int @id @unique @default(autoincrement())
type contentType
name String
filename String
actionplan ActionPlan[]
actionplan actionPlan[]
@@fulltext([name, filename])
}
model Alerts {
id Int @id @unique @default(autoincrement())
type alertType
message String?
actionplan ActionPlan? @relation(fields: [actionplanId], references: [id])
actionplanId Int?
// TODO: TBD
// Quelle: BMA / EMA
// Date
// Date (Clear) ???
}
// https://spacecdn.de/file/bma_stoe_v1.mp3
// https://spacecdn.de/file/quittiert_v1.mp3

View File

@ -1,8 +1,10 @@
import { PrismaClient } from '@prisma/client'; // Database
import config from "./config.js";
import { PrismaClient, Prisma } from '@prisma/client'; // Database
import { Response } from 'express';
import config from './config.js';
import log from './log.js';
// TODO: Add errorhandling with some sort of message.
export const prisma = new PrismaClient({
const prisma = new PrismaClient({
datasources: {
db: {
url: config.global.db_connection_string
@ -10,3 +12,75 @@ export const prisma = new PrismaClient({
}
});
// FIXME: any
export function handlePrismaError(errorObj: any, res: Response) {
log.core.debug(errorObj);
res.status(500).json({ status: 'ERROR', meta: errorObj.meta, errorcode: errorObj.code, message: errorObj.message });
// if(errorObj instanceof Prisma.PrismaClientKnownRequestError)
// switch (errorObj.code) {
// // P2002 -> "Unique constraint failed on the {constraint}"
// // https://www.prisma.io/docs/reference/api-reference/error-reference
// case 'P2002': //
// log.db.error('');
// break;
// // P2003 -> "Foreign key constraint failed on the field: {field_name}"
// // https://www.prisma.io/docs/reference/api-reference/error-reference
// // FIXME: Is this errormessage right?
// case 'P2003': //
// log.db.error('');
// break;
// case 'xxx': //
// log.db.error('');
// break;
// case 'xxx':
// log.db.error('');
// break;
// case 'xxx':
// log.db.error('');
// break;
// case 'xxx':
// log.db.error('');
// break;
// case 'xxx':
// log.db.error('');
// break;
// case 'xxx':
// log.db.error('');
// break;
// case 'xxx':
// log.db.error('');
// break;
// default:
// break;
// }
// // Check if an entry already exists.
// if (errorcode === 'P2002') {
// // P2002 -> "Unique constraint failed on the {constraint}"
// // https://www.prisma.io/docs/reference/api-reference/error-reference
// res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Item already exists' });
// } else if (errorcode == 'P2003') {
// // P2003 -> "Foreign key constraint failed on the field: {field_name}"
// // https://www.prisma.io/docs/reference/api-reference/error-reference
// // FIXME: Is this errormessage right?
// res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
// } else if (errorcode == 'P2000') {
// // P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
// // https://www.prisma.io/docs/reference/api-reference/error-reference
// res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
// } else {
// log.db.error(err);
// res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
// }
}
export default prisma;

View File

@ -1,24 +1,31 @@
import { Logger } from "tslog";
import { Logger,ISettingsParam } from "tslog";
const loggerConfig: any = {
function loggerConfig(name: string): ISettingsParam<unknown> {
return {
type: "pretty", // pretty, json, hidden
name: "Core",
name: name,
hideLogPositionForProduction: true,
prettyLogTemplate: "{{dateIsoStr}} {{logLevelName}} {{nameWithDelimiterPrefix}} "
}
}
const coreLogger = new Logger(loggerConfig);
export const log = {
core: coreLogger,
db: coreLogger.getSubLogger({ name: "DB" }),
web: coreLogger.getSubLogger({ name: "WEB" }),
auth: coreLogger.getSubLogger({ name: "AUTH" }),
helper: coreLogger.getSubLogger({ name: "HELPER" }),
// FIXME: any type
let log: any = {
core: new Logger(loggerConfig("Core")),
db: new Logger(loggerConfig("DB")),
web: new Logger(loggerConfig("Web")),
auth: new Logger(loggerConfig("Auth")),
// helper: new Logger(loggerConfig("HELPER")),
};
log["api"] = log.web.getSubLogger({ name: "API" });
log["frontend"] = log.web.getSubLogger({ name: "Frontend" });
// log.core.silly("Hello from core");
// log.core.trace("Hello from core");
//log.api.trace("Hello from api");
//log.frontend.trace("Hello from frontend");
// log.core.debug("Hello from core");
// log.core.info("Hello from core");
// log.core.warn("Hello from core");

View File

@ -2,6 +2,7 @@
import path from 'node:path';
import __path from "./handlers/path.js";
import log from "./handlers/log.js";
import db from "./handlers/db.js";
import config from './handlers/config.js';
// Express & more
@ -13,28 +14,56 @@ import bodyParser, { Options } from 'body-parser';
import { Eta } from "eta";
import passport from 'passport';
import ChildProcess from 'child_process';
import routes from './routes/index.js';
import fs from 'node:fs';
log.core.trace("Running from path: " + __path);
db.$disconnect();
// MARK: Express
const app = express();
// TODO: Version check need to be rewritten.
//app.locals.versionRevLong = require('child_process').execSync('git rev-parse HEAD').toString().trim();
//app.locals.versionRev = require('child_process').execSync('git rev-parse --short HEAD').toString().trim();
//app.locals.versionRevLatest = require('child_process').execSync('git ls-remote --refs -q').toString().trim().split('\t')[0];
app.locals.versionRev = '0';
app.locals.versionRevLong = '0';
app.locals.versionRevLatest = '0';
if (app.locals.versionRevLong === app.locals.versionRevLatest) {
log.core.info(`Running Latest Version (${app.locals.versionRevLong})`);
} else {
log.core.info(`Running Version: ${app.locals.versionRevLong} (Latest: ${app.locals.versionRevLatest})`);
// Versioning
try {
const rawPkg = fs.readFileSync("package.json", 'utf8');
const pkgJson = JSON.parse(rawPkg);
app.locals.version = pkgJson.version;
} catch (error) {
log.core.error("Failed to get version from package.json.");
app.locals.version = "0.0.0";
}
try {
app.locals.versionRevLong = ChildProcess.execSync('git rev-parse HEAD').toString().trim();
app.locals.versionRev = app.locals.versionRevLong.substring(0, 7);
} catch (error) {
log.core.error("Failed to get git revision hash.");
app.locals.versionRev = '0';
app.locals.versionRevLong = '0';
}
try {
app.locals.versionRevLatest = ChildProcess.execSync('git ls-remote --refs -q').toString().trim().split('\t')[0];
} catch (error) {
log.core.error("Failed to get latest git revision hash.");
app.locals.versionRevLatest = '0';
}
app.locals.versionUpdateAvailable = false;
if (app.locals.versionRevLong === app.locals.versionRevLatest) {
log.core.info(`Running Latest Version (${app.locals.versionRevLong}; ${app.locals.version})`);
} else {
log.core.info(`Running Version: ${app.locals.versionRevLong}; ${app.locals.version} (Latest: ${app.locals.versionRevLatest})`);
app.locals.versionUpdateAvailable = true;
}
// ETA Init
const eta = new Eta({ views: path.join(__path, "views") })
app.engine("eta", buildEtaEngine())

View File

@ -0,0 +1,116 @@
import { Request, Response } from 'express';
import db, { handlePrismaError } from '../../../handlers/db.js'; // Database
import log from '../../../handlers/log.js';
///api/v1/alertContacts?action=count&filter=...
// GET without args -> Get all alertContacts
/**
* A function to create a sortBy compatible object from a string
*
* @export
* @param {string} SortField
* @param {string} Order
* @returns {object}
*/
export function parseDynamicSortBy(SortField: string, Order: string) {
return JSON.parse(`{ "${SortField}": "${Order}" }`);
}
/**
* Function to parse a string into a number or return undefined if it is not a number
*
* @export
* @param {string || any} data
* @returns {object}
*/
export function parseIntOrUndefined(data: any) {
return isNaN(parseInt(data)) ? undefined : parseInt(data);
}
// GET AlertContact
async function get(req: Request, res: Response) {
// Set sane defaults if undefined for sort
if (req.query.sort === undefined) {
req.query.sort = 'id';
}
if (req.query.order === undefined) {
req.query.order = 'asc';
}
// Prio 1 -> Get count (with or without filter)
// Prio 2 -> Get by id
// Prio 3 -> Get with filter
if ((req.query.search !== undefined && req.query.search.length > 0) || (req.query.id !== undefined && req.query.id.length > 0)) {
if (req.query.search !== undefined && req.query.search === '*') {
log.db.debug('Single * does not work with FullTextSearch');
req.query.search = '';
}
// When an ID is set, remove(disable) the search query
if (req.query.id !== undefined && req.query.id.length > 0) {
req.query.search = undefined;
}
const query = {
where: {
OR: [{ id: parseIntOrUndefined(req.query.id) }, { name: { search: req.query.search } }, { phone: { search: req.query.search } }, { comment: { search: req.query.search } }]
},
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
};
if (req.query.count === undefined) {
// get all entrys
await db.alertContacts.findMany(query).then((result) => {
res.status(200).json(result);
});
} else {
// count all entrys (filtered or not)
await db.alertContacts.count(query).then((result) => {
res.status(200).json(result);
});
}
} else {
if (req.query.count === undefined) {
await db.alertContacts.findMany({ orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()) }).then((result) => {
res.status(200).json(result);
});
} else {
await db.alertContacts.count().then((result) => {
res.status(200).json(result);
});
}
}
}
// CREATE AlertContact
async function post(req: Request, res: Response) {
// Check if undefined or null
if (req.body.name != null && req.body.phone != null) {
await db.alertContacts
.create({
data: {
name: req.body.name,
phone: req.body.phone,
comment: req.body.comment,
},
select: {
id: true
}
}).then((result) => {
res.status(201).json({ status: 'CREATED', message: 'Successfully created alertContact', id: result.id });
})
} else {
res.status(400).json({ status: 'ERROR', errorcode: "VALIDATION_ERROR", message: 'One or more required fields are missing or invalid' });
}
}
// UPDATE AlertContact
async function patch(req: Request, res: Response) {}
// DELETE AlertContact
async function del(req: Request, res: Response) {}
export default { get, post, patch, del };

View File

@ -3,12 +3,12 @@ import passport from 'passport';
// Route imports
import testRoute from './test.js';
//import itemRoute from './items.js';
import alertContactsRoute from './alertContacts.js';
//import categoryRoute from './categories.js';
//import storageUnitRoute from './storageUnits.js';
//import storageLocationRoute from './storageLocations.js';
//import contactInfo from './contactInfo.js';
//import versionRoute from './version.js'
import versionRoute from './version.js'
//import search_routes from './search/index.js';
@ -25,14 +25,15 @@ Router.use('*', function (req, res, next) {
next();
});
//Router.route('/items').get(itemRoute.get).post(itemRoute.post).patch(itemRoute.patch).delete(itemRoute.del);
// All api routes lowercase! Yea I know but when strict: true it matters.
Router.route('/alertcontacts').get(alertContactsRoute.get).post(alertContactsRoute.post).patch(alertContactsRoute.patch).delete(alertContactsRoute.del);
//Router.route('/categories').get(categoryRoute.get).post(categoryRoute.post).patch(categoryRoute.patch).delete(categoryRoute.del);
// TODO: Migrate routes to lowercase.
//Router.route('/storageUnits').get(storageUnitRoute.get).post(storageUnitRoute.post).patch(storageUnitRoute.patch).delete(storageUnitRoute.del);
//Router.route('/storageLocations').get(storageLocationRoute.get).post(storageLocationRoute.post).patch(storageLocationRoute.patch).delete(storageLocationRoute.del);
//Router.route('/contactInfo').get(contactInfo.get).post(contactInfo.post).patch(contactInfo.patch).delete(contactInfo.del);
//Router.route('/version').get(versionRoute.get);
Router.route('/version').get(versionRoute.get);
//Router.use('/search', search_routes);
Router.route('/test').get(testRoute.get);

View File

@ -0,0 +1,9 @@
import { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.status(200).send({ version: '1.0.0', commit: req.app.locals.versionRev, updateAvailable: req.app.locals.versionUpdateAvailable });
};
export default { get };
// TODO: FIXME!!!!!!

View File

@ -5,6 +5,7 @@ import express from 'express';
// import skuRouteDash from './itemInfo.js'
// import testRoute from './test.js';
import dashboardRoute from './dashboard.js';
import testRoute from './test.js';
// import itemsRoute from './items.js';
// import manage_routes from './manage/index.js';
@ -20,5 +21,6 @@ const Router = express.Router({ strict: false });
// Router.use('/manage', manage_routes);
Router.route('/').get(dashboardRoute.get);
Router.route('/dbTest').get(testRoute.get);
export default Router;

View File

@ -0,0 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.render("test", { message: "Hello world from eta!" })
}
export default { get };

View File

@ -17,6 +17,7 @@ const Router = express.Router({ strict: false });
Router.use('/static', express.static(__path + '/static'));
Router.use('/libs/bulma', express.static(path.join(__path, 'node_modules', 'bulma', 'css'))); // http://192.168.221.10:3000/libs/bulma/bulma.css
Router.use('/libs/jquery', express.static(path.join(__path, 'node_modules', 'jquery', 'dist')));
Router.use('/libs/bootstrap-icons', express.static(path.join(__path, 'node_modules', 'bootstrap-icons')));
// Other routers
Router.use('/api', checkAuthentication, api_routes);

109
static/apiWrapper.js Normal file
View File

@ -0,0 +1,109 @@
_wrapperVersion = "1.0.0"
_minApiVersion = "1.0.0"
_maxApiVersion = "1.0.0"
_apiConfig = {
"basePath": "/api/v1/"
}
// Generic driver functions
let _api = {
"get": async function(path) {
const options = {
headers: new Headers({'content-type': 'application/json'})
};
const response = await fetch(_apiConfig.basePath + path, options)
// Handle the response
if (!response.ok) {
_testPageFail(response.statusText)
return
}
const result = await response.json()
// Handle the result, was json valid?
if (!result) {
_testPageFail("Invalid JSON response")
return
}
return result;
},
"post": async function(path, data) {
const options = {
method: 'POST',
headers: new Headers({'content-type': 'application/json'}),
body: JSON.stringify(data)
};
const response = await fetch(_apiConfig.basePath + path, options)
// Handle the response
if (!response.ok) {
_testPageFail(response.statusText)
return
}
const result = await response.json()
// Handle the result, was json valid?
if (!result) {
_testPageFail("Invalid JSON response")
return
}
return result;
},
"getAsync": function(path, callback) {
const options = {
headers: new Headers({'content-type': 'application/json'})
};
fetch(_apiConfig.basePath + path, options).then(response => response.json()).then(data => callback(data)).catch(error => _testPageFail(error))
}
}
function returnTableDataByTableName(tableName) {
return _api.get(tableName)
}
function returnTableDataByTableNameWithSearch(tableName, search) {
return _api.get(tableName + "?search=" + search)
}
function returnTableDataByTableNameAsync(tableName, callback) {
_api.getAsync(tableName, callback)
}
async function getCountByTable(tableName) {
let result = await(_api.get(tableName + "?count"))
if(typeof result !== 'number') {
_testPageWarn("Count was not a number, was: " + result)
console.warn("Count was not a number, was: " + result)
return -1
}
return result
}
function getRowsByTableAndColumnList(tableName, columnList) {
//return _api.get(tableName + '/rows/' + columnList.join(','))
return undefined
}
function _testPageFail(reason) {
document.getElementById("heroStatus").classList.remove("is-success")
document.getElementById("heroStatus").classList.add("is-danger")
document.getElementById("heroExplainer").innerHTML = "API Wrapper Test Failed, reason: " + reason
}
function _testPageWarn(reason) {
document.getElementById("heroStatus").classList.remove("is-success")
document.getElementById("heroStatus").classList.add("is-warning")
document.getElementById("heroExplainer").innerHTML = "API Wrapper Test Warning, reason: " + reason
}
function getServerVersion() {
return _api.get('version')
}
function createEntry(tableName, data) {
return _api.post(tableName, data)
}

3
static/favicon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg id="favicon" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="138.654" height="146.519" viewBox="0 0 36.685 38.767">
<path d="M18.775 0A24.388 24.388 0 0 0 6.82 3.115C3.15 5.165-1.91 9.252.736 13.985c.37.66.9 1.221 1.47 1.713 1.532 1.322 2.98.222 4.554-.457.975-.42 1.95-.842 2.922-1.27.434-.19 1.01-.33 1.328-.698.858-.99.494-2.994.05-4.095a27.25 27.25 0 0 1 3.65-1.24v30.828h7.215V7.671c1.05.184 2.438.432 3.266 1.041.387.284.113.908.076 1.297-.08.827-.027 1.817.344 2.581.308.632 1.16.784 1.765 1.008l4.564 1.704c.628.232 1.33.643 1.979.297 2.822-1.507 3.574-5.39 1.843-8.023-1.165-1.77-3.255-3.13-5.035-4.216C27.037 1.107 22.906.014 18.775 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 678 B

3
static/logo.svg Normal file
View File

@ -0,0 +1,3 @@
<svg id="logo" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="498.424" height="148.888" viewBox="0 0 131.875 39.393">
<path d="M118.368 51.177c-3.682 0-6.537.32-8.566.958-1.99.6-3.419 1.635-4.283 3.099-.827 1.466-1.239 3.533-1.239 6.2 0 2.03.356 3.7 1.07 5.016.714 1.315 1.916 2.46 3.607 3.438 1.728.939 4.17 1.878 7.326 2.817 2.517.752 4.452 1.466 5.805 2.142 1.39.64 2.386 1.352 2.987 2.142.601.79.9 1.747.9 2.874 0 1.24-.224 2.198-.675 2.874-.451.64-1.202 1.09-2.254 1.353-1.052.263-2.536.375-4.452.338-1.916-.038-3.4-.226-4.453-.564-1.051-.376-1.822-.977-2.31-1.804-.451-.826-.733-2.01-.845-3.55h-7.045c-.113 3.157.263 5.598 1.127 7.327.864 1.728 2.348 2.95 4.452 3.663 2.142.714 5.166 1.07 9.074 1.07 3.795 0 6.706-.318 8.735-.958 2.066-.638 3.532-1.728 4.396-3.268.864-1.54 1.296-3.72 1.296-6.538 0-2.254-.357-4.095-1.07-5.522-.715-1.466-1.917-2.706-3.608-3.72-1.653-1.015-4.02-2.01-7.1-2.987-2.518-.79-4.49-1.485-5.918-2.085-1.39-.6-2.404-1.202-3.043-1.804-.639-.6-.959-1.277-.959-2.028 0-1.165.207-2.049.62-2.649.414-.601 1.09-1.033 2.03-1.296.976-.263 2.366-.395 4.17-.395 1.728 0 3.061.15 4.001.45.977.264 1.672.734 2.085 1.41.451.638.733 1.578.846 2.818h7.157c.038-2.856-.376-5.054-1.24-6.594-.863-1.54-2.292-2.63-4.283-3.27-1.954-.637-4.734-.957-8.34-.957zm-67.058.12a24.388 24.388 0 0 0-11.954 3.114c-3.67 2.051-8.73 6.137-6.085 10.87.37.66.9 1.222 1.47 1.714 1.53 1.322 2.98.222 4.554-.458.975-.42 1.95-.842 2.922-1.268.433-.19 1.01-.331 1.328-.7.858-.99.494-2.994.05-4.094a27.22 27.22 0 0 1 3.651-1.24v30.828h7.214V58.968c1.05.182 2.439.43 3.266 1.04.387.285.113.91.075 1.298-.08.827-.027 1.816.345 2.58.307.632 1.16.785 1.765 1.009l4.564 1.703c.628.233 1.33.644 1.979.298 2.822-1.508 3.574-5.39 1.842-8.023-1.164-1.771-3.254-3.13-5.034-4.216-3.69-2.254-7.822-3.347-11.952-3.36zm-39.287.443L1.146 90.063h7.045l2.423-8.453h12.962l2.48 8.453h7.101L22.055 51.74H12.023zm67.628.001L68.773 90.063h7.045l2.423-8.453h12.964l2.48 8.453h7.1L89.683 51.74H79.65zm-62.668 6.537h.056l4.903 17.076h-9.637l4.678-17.076zm67.628 0h.056l4.903 17.076h-9.637l4.678-17.076z" style="display:inline;fill:current;fill-opacity:1;stroke:none;stroke-width:.408654;stroke-opacity:1" transform="translate(-1.146 -51.177)"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

301
static/pageDriver.js Normal file
View File

@ -0,0 +1,301 @@
_pageDriverVersion = "1.0.1";
// Handle color for icon svg with id="logo" based on the current theme
const logo = document.getElementById("logo");
if(logo) {
logo.style.fill = getComputedStyle(document.documentElement).getPropertyValue("--bulma-text");
}
if(_wrapperVersion === undefined) {
console.error("API Wrapper not found; Please include the API Wrapper before including the Page Driver");
exit();
} else {
console.log("API Wrapper found; Page Driver is ready to use");
}
// Find all tables on the page which have data-dataSource attribute
var tables = document.querySelectorAll("table[data-dataSource]");
//var tables = []
// Get all single values with data-dataSource, data-dataCol and data-dataAction
var singleValues = document.querySelectorAll("span[data-dataSource]");
// Find all search fields with data-searchTargetId
var searchFields = document.querySelectorAll("input[data-searchTargetId]");
// Find all modalForms
var modalForms = document.querySelectorAll("form[data-targetTable]");
// Iterate over all tables
tables.forEach(async table => {
console.log("Table found: ", table);
// Get THEAD and TBODY elements
const thead = table.querySelector("thead");
const tbody = table.querySelector("tbody");
// get index per column
const columns = thead.querySelectorAll("th");
const columnIndices = [];
columns.forEach((column, index) => {
columnIndices[column.getAttribute("data-dataCol")] = index;
});
// All required cols
let requiredCols = [];
columns.forEach(column => {
requiredCols.push(column.getAttribute("data-dataCol"));
});
console.log("Required columns: ", requiredCols);
// Get data from API
//let result = getRowsByTableAndColumnList(table.getAttribute("data-dataSource"), requiredCols);
let result = await returnTableDataByTableName(table.getAttribute("data-dataSource"))
// for (resultIndex in result) {
// const row = result[resultIndex];
// const tr = document.createElement("tr");
// requiredCols.forEach(column => {
// const td = document.createElement("td");
// td.innerHTML = row[column];
// tr.appendChild(td);
// });
// tbody.appendChild(tr);
// }
writeDataToTable(table, result);
console.log("Column indices: ", columnIndices);
});
console.info("Processing single values");
console.info(singleValues);
// Iterate over all single values
singleValues.forEach(async singleValue => {
writeSingelton(singleValue);
});
async function writeSingelton(element) {
const table = element.getAttribute("data-dataSource");
console.log("Table: ", table, " Action: ", element.getAttribute("data-dataAction"), " Element: ", element);
switch(element.getAttribute("data-dataAction")) {
case "COUNT": {
console.log("Count action found");
element.innerHTML = (await getCountByTable(table))
break;
}
case "SPECIAL": {
if(table == "version") {
element.innerHTML = (await getServerVersion())["version"];
break
}
}
default: {
console.error("Unknown action found: ", element.getAttribute("data-dataAction"));
break;
}
}
element.classList.remove("is-skeleton");
}
// Attach listeners to search fields
searchFields.forEach(searchField => {
searchField.addEventListener("input", async function() {
console.log("Search field changed: ", searchField);
const targetId = searchField.getAttribute("data-searchTargetId");
const target = document.getElementById(targetId);
const table = target.getAttribute("data-dataSource");
const column = target.getAttribute("data-dataCol");
const value = searchField.value;
console.log("Searching for ", value, " in ", table, " column ", column);
const result = await returnTableDataByTableNameWithSearch(table, value);
console.log("Result: ", result);
clearTable(target);
writeDataToTable(target, result);
});
});
// Attach listeners to modal forms
modalForms.forEach(modalForm => {
modalForm.addEventListener("submit", async function(event) {
event.preventDefault();
// Check what button submitted the form and if it has data-actionBtn = save
// If not, close modal
const pressedBtn = event.submitter;
if(pressedBtn.getAttribute("data-actionBtn") != "save") {
modalForm.closest(".modal").classList.remove('is-active');
return;
}
// Find .entryPhase and hide it
const entryPhase = modalForm.querySelector(".entryPhase");
const loadPhase = modalForm.querySelector(".loadPhase");
if(entryPhase) {
entryPhase.classList.add("is-hidden");
}
if(loadPhase) {
loadPhase.classList.remove("is-hidden");
}
console.log("Form submitted: ", modalForm);
const table = modalForm.getAttribute("data-targetTable");
const data = new FormData(modalForm);
// Convert to JSON object
let jsonData = {};
data.forEach((value, key) => {
jsonData[key] = value;
});
console.log("JSON Data: ", jsonData);
let resp = await createEntry(table, jsonData);
console.log("Response: ", resp);
if(resp["status"] == "CREATED") {
console.log("Entry created successfully");
modalForm.closest(".modal").classList.remove('is-active');
modalForm.reset();
// Hide loadPhase
if(loadPhase) {
loadPhase.classList.add("is-hidden");
}
// Show entryPhase
if(entryPhase) {
entryPhase.classList.remove("is-hidden");
}
} else {
// Hide loadPhase
if(loadPhase) {
loadPhase.classList.add("is-hidden");
}
// Show entryPhase
if(entryPhase) {
entryPhase.classList.remove("is-hidden");
}
// TODO: Show error message
}
// const target = document.getElementById(table);
// writeDataToTable(target, result);
// Find all tables with data-searchTargetId set to table
setTimeout(() => {
refreshTableByName(table);
updateSingeltonsByTableName(table);
}, 500);
});
});
// Helper
async function refreshTable(table) {
// Refresh a table while keeping (optionally set) search value
const searchField = document.querySelector("input[data-searchTargetId='" + table.id + "']");
if(searchField) {
const value = searchField.value;
const dbTable = table.getAttribute("data-dataSource");
const result = await returnTableDataByTableNameWithSearch(dbTable, value);
clearTable(table);
writeDataToTable(table, result);
} else {
const result = await returnTableDataByTableName(table.getAttribute("data-dataSource"));
clearTable(table);
writeDataToTable(table, result);
}
}
async function refreshTableByName(name) {
const dirtyTables = document.querySelectorAll("table[data-dataSource='" + name + "']");
for(dirty of dirtyTables) {
refreshTable(dirty);
}
}
async function updateSingeltonsByTableName(name) {
const dirtySingles = document.querySelectorAll("span[data-dataSource='" + name + "']");
for(dirty of dirtySingles) {
writeSingelton(dirty);
}
}
function clearTable(table) {
const tbody = table.querySelector("tbody");
tbody.innerHTML = "";
}
function writeDataToTable(table, data) {
console.log("Writing data to table: ", table, data);
// Get THEAD and TBODY elements
const thead = table.querySelector("thead");
const tbody = table.querySelector("tbody");
// get index per column
const columns = thead.querySelectorAll("th");
const columnIndices = [];
columns.forEach((column, index) => {
columnIndices[column.getAttribute("data-dataCol")] = index;
});
// All required cols
let requiredCols = [];
columns.forEach(column => {
requiredCols.push(column.getAttribute("data-dataCol"));
});
for (resultIndex in data) {
const row = data[resultIndex];
const tr = document.createElement("tr");
requiredCols.forEach(column => {
const td = document.createElement("td");
td.innerHTML = row[column];
tr.appendChild(td);
});
tbody.appendChild(tr);
}
}
// Handle modal
document.addEventListener('DOMContentLoaded', () => {
// Functions to open and close a modal
function openModal($el) {
$el.classList.add('is-active');
}
function closeModal($el) {
$el.classList.remove('is-active');
}
function closeAllModals() {
(document.querySelectorAll('.modal') || []).forEach(($modal) => {
closeModal($modal);
});
}
// Add a click event on buttons to open a specific modal
(document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => {
const modal = $trigger.dataset.target;
const $target = document.getElementById(modal);
$trigger.addEventListener('click', () => {
openModal($target);
});
});
// Add a click event on various child elements to close the parent modal
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => {
const $target = $close.closest('.modal');
$close.addEventListener('click', () => {
closeModal($target);
});
});
// Add a keyboard event to close all modals
document.addEventListener('keydown', (event) => {
if(event.key === "Escape") {
closeAllModals();
}
});
});

View File

@ -19,7 +19,7 @@
<div class="level-item has-text-centered">
<div>
<p class="heading">Alarmkonakte</p>
<p class="title">5</p>
<p class="title"><span data-dataSource="AlertContacts" data-dataAction="COUNT" class="is-skeleton">Load.</span></p>
</div>
</div>
<div class="level-item has-text-centered">
@ -69,7 +69,5 @@
</table>
</section>
<h2><%= it.message %></h2>
<%~ include("partials/footer.eta") %>
<%~ include("partials/base_foot.eta") %>

View File

@ -1,2 +1,4 @@
<script src="/static/apiWrapper.js"></script>
<script src="/static/pageDriver.js"></script>
</body>
</html>

View File

@ -8,12 +8,13 @@
<title>ATAS - <%= it.title %></title>
<meta name="author" content="[Project-Name-Here]"/>
<link rel="icon" href="/logo/Design_icon.svg" type="image/svg+xml" />
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
<script src="/libs/jquery/jquery.min.js"></script>
<link rel="stylesheet" href="/libs/bulma/bulma.min.css">
<link rel="stylesheet" href="/static/main.css">
<link rel="stylesheet" href="/libs/bootstrap-icons/font/bootstrap-icons.min.css">
</head>
<body>
<!-- The body and html tag need to be left open! -->

View File

@ -1,9 +1,9 @@
<footer class="footer">
<div class="content has-text-centered">
<p>
<strong>ATAS</strong> by <a href="https://tgd.fyi">Meee</a>.The source code is not licensed
<a href="https://opensource.org/license/mit">MIT</a>. The website content is licensed
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0//">CC BY NC SA 4.0</a>.
<i class="bi bi-universal-access"></i>
<strong>ATAS</strong> by <a href="https://pnh.fyi">[Project-name-here]</a>.<br>
Running Version <span data-dataSource="version" data-dataAction="SPECIAL" class="is-skeleton">Load.</span>
</p>
</div>
</footer>

View File

@ -1,10 +1,11 @@
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="https://bulma.io">
<svg width="640" height="160" viewBox="0 0 640 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M170 132.571V27.5908C170 25.5451 170.915 23.93 172.746 22.7456C174.576 21.5612 176.729 20.969 179.206 20.969H210.377C232.019 20.969 242.84 30.4441 242.84 49.3943C242.84 62.5303 238.264 71.0902 229.112 75.074C234.603 77.2275 238.748 80.2692 241.548 84.1992C244.347 88.1292 245.747 93.8627 245.747 101.4V104.791C245.747 116.743 242.84 125.437 237.026 130.875C231.211 136.312 223.351 139.031 213.445 139.031H179.206C176.514 139.031 174.307 138.385 172.584 137.093C170.861 135.801 170 134.293 170 132.571ZM190.834 120.619H209.085C219.529 120.619 224.751 114.751 224.751 103.015V100.431C224.751 94.401 223.432 90.0404 220.794 87.3486C218.156 84.6568 214.253 83.3109 209.085 83.3109H190.834V120.619ZM190.834 66.8371H208.923C213.122 66.8371 216.326 65.5989 218.533 63.1225C220.74 60.646 221.844 57.2544 221.844 52.9475C221.844 48.7483 220.686 45.4374 218.371 43.0148C216.057 40.5922 212.853 39.3809 208.762 39.3809H190.834V66.8371ZM260.283 103.015V27.4293C260.283 25.2759 261.306 23.6608 263.351 22.5841C265.397 21.5074 267.873 20.969 270.781 20.969C273.688 20.969 276.164 21.5074 278.21 22.5841C280.256 23.6608 281.279 25.2759 281.279 27.4293V103.015C281.279 115.397 287.2 121.588 299.044 121.588C310.888 121.588 316.81 115.397 316.81 103.015V27.4293C316.81 25.2759 317.833 23.6608 319.879 22.5841C321.925 21.5074 324.401 20.969 327.308 20.969C330.215 20.969 332.692 21.5074 334.738 22.5841C336.783 23.6608 337.806 25.2759 337.806 27.4293V103.015C337.806 115.72 334.28 125.061 327.227 131.036C320.175 137.012 310.781 140 299.044 140C287.308 140 277.914 137.039 270.861 131.117C263.809 125.195 260.283 115.828 260.283 103.015ZM356.703 132.409V27.4293C356.703 25.2759 357.725 23.6608 359.771 22.5841C361.817 21.5074 364.293 20.969 367.201 20.969C370.108 20.969 372.584 21.5074 374.63 22.5841C376.676 23.6608 377.699 25.2759 377.699 27.4293V120.619H417.106C419.044 120.619 420.579 121.534 421.709 123.365C422.84 125.195 423.405 127.349 423.405 129.825C423.405 132.301 422.84 134.455 421.709 136.285C420.579 138.116 419.044 139.031 417.106 139.031H365.908C363.432 139.031 361.279 138.439 359.448 137.254C357.618 136.07 356.703 134.455 356.703 132.409ZM434.872 132.409V31.467C434.872 27.9138 435.868 25.2759 437.86 23.5532C439.852 21.8304 442.355 20.969 445.37 20.969C449.354 20.969 452.423 21.6689 454.576 23.0686C456.729 24.4684 459.098 27.4832 461.682 32.1131L481.548 68.2907L501.413 32.1131C503.997 27.4832 506.393 24.4684 508.6 23.0686C510.808 21.6689 513.903 20.969 517.887 20.969C520.902 20.969 523.405 21.8304 525.397 23.5532C527.389 25.2759 528.385 27.9138 528.385 31.467V132.409C528.385 134.455 527.335 136.07 525.236 137.254C523.136 138.439 520.686 139.031 517.887 139.031C514.98 139.031 512.503 138.439 510.458 137.254C508.412 136.07 507.389 134.455 507.389 132.409V62.961L488.493 96.5545C486.985 99.354 484.616 100.754 481.386 100.754C478.264 100.754 475.949 99.354 474.441 96.5545L455.868 61.6689V132.409C455.868 134.455 454.818 136.07 452.719 137.254C450.619 138.439 448.17 139.031 445.37 139.031C442.463 139.031 439.987 138.439 437.941 137.254C435.895 136.07 434.872 134.455 434.872 132.409ZM539.529 130.31C539.529 130.094 539.637 129.556 539.852 128.694L571.023 27.1063C571.669 24.8452 573.257 23.0956 575.787 21.8573C578.318 20.6191 581.198 20 584.428 20C587.658 20 590.565 20.6191 593.149 21.8573C595.734 23.0956 597.349 24.8452 597.995 27.1063L629.166 128.694C629.381 129.556 629.489 130.094 629.489 130.31C629.489 132.678 628.035 134.724 625.128 136.447C622.221 138.17 619.26 139.031 616.245 139.031C612.261 139.031 609.892 137.631 609.139 134.832L603.001 113.351H566.016L559.879 134.832C559.125 137.631 556.756 139.031 552.773 139.031C549.65 139.031 546.662 138.197 543.809 136.528C540.956 134.859 539.529 132.786 539.529 130.31ZM570.377 96.8775H598.479L584.428 47.2948L570.377 96.8775Z" fill="black" class="bd-svg-black" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 110L10 40L50 0L100 50L70 80L110 120L50 160L0 110Z" fill="#00D1B2"/>
<a class="navbar-item primary" href="/">
<svg id="logo" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="498.424" height="148.888" viewBox="0 0 131.875 39.393">
<path d="M118.368 51.177c-3.682 0-6.537.32-8.566.958-1.99.6-3.419 1.635-4.283 3.099-.827 1.466-1.239 3.533-1.239 6.2 0 2.03.356 3.7 1.07 5.016.714 1.315 1.916 2.46 3.607 3.438 1.728.939 4.17 1.878 7.326 2.817 2.517.752 4.452 1.466 5.805 2.142 1.39.64 2.386 1.352 2.987 2.142.601.79.9 1.747.9 2.874 0 1.24-.224 2.198-.675 2.874-.451.64-1.202 1.09-2.254 1.353-1.052.263-2.536.375-4.452.338-1.916-.038-3.4-.226-4.453-.564-1.051-.376-1.822-.977-2.31-1.804-.451-.826-.733-2.01-.845-3.55h-7.045c-.113 3.157.263 5.598 1.127 7.327.864 1.728 2.348 2.95 4.452 3.663 2.142.714 5.166 1.07 9.074 1.07 3.795 0 6.706-.318 8.735-.958 2.066-.638 3.532-1.728 4.396-3.268.864-1.54 1.296-3.72 1.296-6.538 0-2.254-.357-4.095-1.07-5.522-.715-1.466-1.917-2.706-3.608-3.72-1.653-1.015-4.02-2.01-7.1-2.987-2.518-.79-4.49-1.485-5.918-2.085-1.39-.6-2.404-1.202-3.043-1.804-.639-.6-.959-1.277-.959-2.028 0-1.165.207-2.049.62-2.649.414-.601 1.09-1.033 2.03-1.296.976-.263 2.366-.395 4.17-.395 1.728 0 3.061.15 4.001.45.977.264 1.672.734 2.085 1.41.451.638.733 1.578.846 2.818h7.157c.038-2.856-.376-5.054-1.24-6.594-.863-1.54-2.292-2.63-4.283-3.27-1.954-.637-4.734-.957-8.34-.957zm-67.058.12a24.388 24.388 0 0 0-11.954 3.114c-3.67 2.051-8.73 6.137-6.085 10.87.37.66.9 1.222 1.47 1.714 1.53 1.322 2.98.222 4.554-.458.975-.42 1.95-.842 2.922-1.268.433-.19 1.01-.331 1.328-.7.858-.99.494-2.994.05-4.094a27.22 27.22 0 0 1 3.651-1.24v30.828h7.214V58.968c1.05.182 2.439.43 3.266 1.04.387.285.113.91.075 1.298-.08.827-.027 1.816.345 2.58.307.632 1.16.785 1.765 1.009l4.564 1.703c.628.233 1.33.644 1.979.298 2.822-1.508 3.574-5.39 1.842-8.023-1.164-1.771-3.254-3.13-5.034-4.216-3.69-2.254-7.822-3.347-11.952-3.36zm-39.287.443L1.146 90.063h7.045l2.423-8.453h12.962l2.48 8.453h7.101L22.055 51.74H12.023zm67.628.001L68.773 90.063h7.045l2.423-8.453h12.964l2.48 8.453h7.1L89.683 51.74H79.65zm-62.668 6.537h.056l4.903 17.076h-9.637l4.678-17.076zm67.628 0h.056l4.903 17.076h-9.637l4.678-17.076z" style="display:inline;fill:current;fill-opacity:1;stroke:none;stroke-width:.408654;stroke-opacity:1" transform="translate(-1.146 -51.177)"/>
</svg>
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
@ -17,9 +18,9 @@
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item">Home</a>
<a class="navbar-item">Documentation</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-item" href="/">Home</a>
<a class="navbar-item" href="/dbTest">API Integration <span class="tag is-info">Dev</span></a>
<!--<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">More</a>
<div class="navbar-dropdown">
<a class="navbar-item">About</a>
@ -28,10 +29,10 @@
<hr class="navbar-divider">
<a class="navbar-item">Report an issue</a>
</div>
</div>
</div>-->
</div>
<div class="navbar-end">
<!--<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary">
@ -40,6 +41,6 @@
<a class="button is-light">Log in</a>
</div>
</div>
</div>
</div>-->
</div>
</nav>

118
views/test.eta Normal file
View File

@ -0,0 +1,118 @@
<%~ include("partials/base_head.eta", {"title": "API Test"}) %>
<%~ include("partials/nav.eta") %>
<section class="hero is-primary" id="heroStatus">
<div class="hero-body">
<p class="title" data-tK="start-hero-header-welcome">Test Page</p>
<p class="subtitle" data-tK="start-hero-header-subtitle-default" id="heroExplainer">API Integration test page</p>
</div>
</section>
<section class="section">
<h1 class="title" data-tK="start-sysinfo-header">Singelton Query</h1>
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">Kontakte</p>
<p class="title"><span data-dataSource="AlertContacts" data-dataAction="COUNT" class="is-skeleton">Load.</span></p>
</div>
</div>
<!--<div class="level-item has-text-centered">
<div>
<p class="heading">Alarme</p>
<p class="title"><span data-dataSource="Alerts" data-dataCol="id" data-dataAction="COUNT" class="is-skeleton">Load.</span></p>
</div>
</div>-->
<div class="level-item has-text-centered">
<div>
<p class="heading">Version</p>
<p class="title"><span data-dataSource="version" data-dataAction="SPECIAL" class="is-skeleton">Load.</span></p>
</div>
</div>
</nav>
</section>
<!-- TODO: Mark required fields as required; add handling for validation -->
<div id="modal-js-example" class="modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box entryPhase is-hidden">
<h2 class="title">New Contact</h1>
<i class="bi bi-arrow-clockwise title"></i>
</div>
<div class="box entryPhase">
<h2 class="title">New Contact</h1>
<form data-targetTable="AlertContacts">
<div class="field">
<label class="label">Name</label>
<div class="control has-icons-left">
<input class="input" type="text" placeholder="John Doe" value="" name="name">
<span class="icon is-small is-left">
<i class="bi bi-file-earmark-person-fill"></i>
</span>
</div>
</div>
<div class="field">
<label class="label">Telephone</label>
<div class="control has-icons-left">
<input class="input" type="text" placeholder="+49 1234 5678912" value="" name="phone">
<span class="icon is-small is-left">
<i class="bi bi-telephone-fill"></i>
</span>
</div>
</div>
<div class="field">
<label class="label">Comment</label>
<div class="control has-icons-left">
<input class="input" type="text" placeholder="" value="" name="comment">
<span class="icon is-small is-left">
<i class="bi bi-chat-fill"></i>
</span>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<input type="submit" class="button is-link" value="Save" data-actionBtn="save">
</div>
<div class="control">
<button class="button is-link is-light" data-actionBtn="cancel">Cancel</button>
</div>
</div>
</form>
</div>
</div>
<button class="modal-close is-large" aria-label="close"></button>
</div>
<button class="js-modal-trigger button" data-target="modal-js-example">
Create new Contact
</button>
<section class="section">
<h1 class="title" data-tK="start-recent-header">Alarm Kontakte</h1>
<input class="input" type="text" data-searchTargetId="contactTable" placeholder="Search..." />
<table class="table is-striped is-fullwidth" data-dataSource="AlertContacts" id="contactTable">
<thead>
<tr>
<th data-dataCol = "id"><abbr title="Position">Pos</abbr></th>
<th data-dataCol = "name">Name</th>
<th data-dataCol = "phone">Telefon Nummer</th>
<th data-dataCol = "comment">Kommentar</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</section>
<%~ include("partials/footer.eta") %>
<%~ include("partials/base_foot.eta") %>