Compare commits

...

9 Commits

20 changed files with 966 additions and 32760 deletions

View File

@ -16,3 +16,34 @@ Funktionen:
- Erklärung MP3 - Erklärung MP3
- Quittierung MP3 - Quittierung MP3
- Verabschiedung MP3 - Verabschiedung MP3
## API Endpoint planning
alertContacts (CRUD Fully implemented)
alerts -> Only get
actionPlan (CRUD)
- select all prios
priorities (CRUD)
- select actionPlan
- Only allow changes to priority
content (CRUD)
- Howto handle upload?
POST /alert/[:alert_hook]
-> Check actionplan if hook exists and select current prios -> Write call request to XYXYX
1. create one or more alertContacts
2. create 4x content (all phases)
3. create actionplan with contents
4. create one or more priorities

File diff suppressed because one or more lines are too long

View File

@ -1,98 +0,0 @@
//// ------------------------------------------------------
//// THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
//// ------------------------------------------------------
Project "AssetFlow" {
database_type: ''
Note: ''
}
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]
alerts alerts [not null]
}
Table actionPlan {
id Int [pk, increment]
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]
actionplanId Int [not null]
indexes {
(priority, actionplanId) [unique]
}
}
Table content {
id Int [pk, increment]
type contentType [not null]
name String [not null]
filename String [not null]
actionplan actionPlan [not null]
}
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 {
voice_alarm
voice_explainer
voice_acknowledgement
voice_ending
}
Enum alertType {
generic
fire
fault
intrusion
clear
}
Enum alertState {
incomming
running
failed
acknowledged
}
Ref: alerts.actionplanId > actionPlan.id
Ref: priorities.contactId > alertContacts.id
Ref: priorities.actionplanId > actionPlan.id

File diff suppressed because one or more lines are too long

3708
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,8 +33,6 @@
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"prisma": "^6.2.1", "prisma": "^6.2.1",
"prisma-dbml-generator": "^0.12.0",
"prisma-docs-generator": "^0.8.0",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"
}, },
@ -46,9 +44,11 @@
"express": "^4.21.2", "express": "^4.21.2",
"express-fileupload": "^1.5.1", "express-fileupload": "^1.5.1",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"helmet": "^8.0.0",
"joi": "^17.13.3", "joi": "^17.13.3",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minio": "^8.0.4",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"signale": "^1.4.0", "signale": "^1.4.0",

View File

@ -13,23 +13,6 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
// https://github.com/pantharshit00/prisma-docs-generator
generator docs {
provider = "node node_modules/prisma-docs-generator"
output = "../docs"
}
// https://github.com/notiz-dev/prisma-dbml-generator
// Viewer: https://dbdiagram.io/d
generator dbml {
provider = "prisma-dbml-generator"
output = "../docs"
outputName = "schema.dbml"
projectName = "AssetFlow"
}
enum contentType { enum contentType {
voice_alarm voice_alarm
voice_explainer voice_explainer
@ -46,70 +29,74 @@ enum alertType {
} }
enum alertState { enum alertState {
incomming // Incomming alerts incoming // Incoming alerts
running // Started calling running // Started calling
failed // Failed to get acknowledgement of any alertContacts failed // Failed to get acknowledgement of any alertContacts
acknowledged // Some user acknowledged alert acknowledged // Some user acknowledged alert
} }
model alerts { model alerts {
id Int @id @unique @default(autoincrement()) id Int @id @unique @default(autoincrement())
type alertType type alertType
message String?
actionplan actionPlan? @relation(fields: [actionplanId], references: [id])
actionplanId Int?
date DateTime
state alertState state alertState
description String?
date DateTime
actionplan actionPlan? @relation(fields: [actionplanId], references: [id])
actionplanId Int?
acknowledged_by alertContacts[] acknowledged_by alertContacts[]
acknowledged_at DateTime? acknowledged_at DateTime?
@@fulltext([message])
@@fulltext([description])
} }
model alertContacts { model alertContacts {
id Int @id @unique @default(autoincrement()) id Int @id @unique @default(autoincrement())
name String name String
phone String @unique phone String @unique
comment String? comment String?
prios priorities[] prios priorities[]
alerts alerts[] alerts alerts[]
@@fulltext([name, phone, comment]) @@fulltext([name, phone, comment])
} }
model actionPlan { model actionPlan {
id Int @id @unique @default(autoincrement()) id Int @id @unique @default(autoincrement())
name String name String @unique
comment String? comment String?
alert_hook String @unique alerthook String @unique @default(ulid())
prio priorities[] prio priorities[]
content content[] // aka. all voice files content content[] // aka. all voice files
alerts alerts[] alerts alerts[]
@@fulltext([name, comment]) @@fulltext([name, comment])
} }
model priorities { model priorities {
id Int @id @unique @default(autoincrement()) id Int @id @unique @default(autoincrement())
Contact alertContacts @relation(fields: [contactId], references: [id]) Contact alertContacts @relation(fields: [contactId], references: [id])
contactId Int contactId Int
priority Int priority Int
actionplan actionPlan @relation(fields: [actionplanId], references: [id]) actionplan actionPlan @relation(fields: [actionplanId], references: [id])
actionplanId Int actionplanId Int
@@unique([priority, actionplanId]) @@unique([priority, actionplanId])
} }
model content { model content {
id Int @id @unique @default(autoincrement()) //id Int @id @unique @default(autoincrement())
type contentType s3_key String @id @unique
name String name String @unique
filename String type contentType
actionplan actionPlan[] actionplan actionPlan[]
@@fulltext([name, filename])
@@fulltext([name])
} }
// https://spacecdn.de/file/bma_stoe_v1.mp3 // https://spacecdn.de/file/bma_stoe_v1.mp3
// https://spacecdn.de/file/quittiert_v1.mp3 // https://spacecdn.de/file/quittiert_v1.mp3
// https://spacecdn.de/file/angenehmen_tag_v1.mp3 // https://spacecdn.de/file/angenehmen_tag_v1.mp3

View File

@ -1,14 +1,23 @@
import ConfigManager from '../libs/configManager.js'; import ConfigManager from '../libs/configManager.js';
import __path from "./path.js"; import __path from './path.js';
import _ from 'lodash'; import _ from 'lodash';
import log from './log.js';
// Create a new config instance. // Create a new config instance.
const config = new ConfigManager(__path + '/config.json', true, { const config = new ConfigManager(__path + '/config.json', true, {
db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE', db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE',
http_listen_address: '0.0.0.0', http_listen_address: '0.0.0.0',
http_port: 3000, http_port: 3000,
debug: true, http_domain: 'example.org',
http_enable_hsts: false,
devmode: true,
s3: {
endpoint: 'minio.example.org',
port: 443,
use_ssl: true,
access_key: '',
secret_key: ''
},
auth: { auth: {
cookie_secret: 'gen', cookie_secret: 'gen',
cookie_secure: true, cookie_secure: true,
@ -25,9 +34,11 @@ const config = new ConfigManager(__path + '/config.json', true, {
// If no local User exists, create the default with a generated password // If no local User exists, create the default with a generated password
if (_.isEqual(config.global.auth.local.users, {})) { if (_.isEqual(config.global.auth.local.users, {})) {
config.global.auth.local.users = { config.global.auth.local.users = {
'administrator': 'gen', administrator: 'gen'
}; };
config.save_config(); config.save_config();
} }
!config.global.devmode && log.core.error('devmode active! Do NOT use this in prod!');
export default config; export default config;

View File

@ -14,6 +14,7 @@ type log = {
core: Logger<unknown> core: Logger<unknown>
db: Logger<unknown> db: Logger<unknown>
web: Logger<unknown> web: Logger<unknown>
S3: Logger<unknown>
auth: Logger<unknown> auth: Logger<unknown>
api?: Logger<unknown> api?: Logger<unknown>
frontend?: Logger<unknown> frontend?: Logger<unknown>
@ -24,6 +25,7 @@ let log: log = {
core: new Logger(loggerConfig("Core")), core: new Logger(loggerConfig("Core")),
db: new Logger(loggerConfig("DB")), db: new Logger(loggerConfig("DB")),
web: new Logger(loggerConfig("Web")), web: new Logger(loggerConfig("Web")),
S3: new Logger(loggerConfig("S3")),
auth: new Logger(loggerConfig("Auth")), auth: new Logger(loggerConfig("Auth")),
// helper: new Logger(loggerConfig("HELPER")), // helper: new Logger(loggerConfig("HELPER")),
}; };

19
src/handlers/s3.ts Normal file
View File

@ -0,0 +1,19 @@
import * as Minio from 'minio';
import log from './log.js';
import config from './config.js';
const minioClient = new Minio.Client({
endPoint: config.global.s3.endpoint,
port: config.global.s3.port,
useSSL: config.global.s3.use_ssl,
accessKey: config.global.s3.access_key,
secretKey: config.global.s3.secret_key
});
export async function test() {
log.S3.debug('GET', await minioClient.presignedGetObject('atas-dev', 'test', 500));
log.S3.debug('PUT', await minioClient.presignedPutObject('atas-dev', 'test', 500));
}
export default minioClient;

View File

@ -1,17 +1,18 @@
// MARK: Imports // MARK: Imports
import path from 'node:path'; import path from 'node:path';
import __path from "./handlers/path.js"; import __path from './handlers/path.js';
import log from "./handlers/log.js"; import log from './handlers/log.js';
import db from "./handlers/db.js"; import db from './handlers/db.js';
import config from './handlers/config.js'; import config from './handlers/config.js';
// Express & more // Express & more
import express from 'express'; import express from 'express';
import cors from 'cors' import cors from 'cors';
import helmet from 'helmet';
import session from 'express-session'; import session from 'express-session';
import fileUpload from 'express-fileupload'; import fileUpload from 'express-fileupload';
import bodyParser, { Options } from 'body-parser'; import bodyParser, { Options } from 'body-parser';
import { Eta } from "eta"; import { Eta } from 'eta';
import passport from 'passport'; import passport from 'passport';
import ChildProcess from 'child_process'; import ChildProcess from 'child_process';
@ -20,28 +21,26 @@ import routes from './routes/index.js';
import fs from 'node:fs'; import fs from 'node:fs';
log.core.trace("Running from path: " + __path); log.core.trace('Running from path: ' + __path);
// MARK: Express // MARK: Express
const app = express(); const app = express();
// Versioning // Versioning
try { try {
const rawPkg = fs.readFileSync("package.json", 'utf8'); const rawPkg = fs.readFileSync('package.json', 'utf8');
const pkgJson = JSON.parse(rawPkg); const pkgJson = JSON.parse(rawPkg);
app.locals.version = pkgJson.version; app.locals.version = pkgJson.version;
} catch (error) { } catch (error) {
log.core.error("Failed to get version from package.json."); log.core.error('Failed to get version from package.json.');
app.locals.version = "0.0.0"; app.locals.version = '0.0.0';
} }
try {
try {
app.locals.versionRevLong = ChildProcess.execSync('git rev-parse HEAD').toString().trim(); app.locals.versionRevLong = ChildProcess.execSync('git rev-parse HEAD').toString().trim();
app.locals.versionRev = app.locals.versionRevLong.substring(0, 7); app.locals.versionRev = app.locals.versionRevLong.substring(0, 7);
} catch (error) { } catch (error) {
log.core.error("Failed to get git revision hash."); log.core.error('Failed to get git revision hash.');
app.locals.versionRev = '0'; app.locals.versionRev = '0';
app.locals.versionRevLong = '0'; app.locals.versionRevLong = '0';
} }
@ -49,7 +48,7 @@ try {
try { try {
app.locals.versionRevLatest = ChildProcess.execSync('git ls-remote --refs -q').toString().trim().split('\t')[0]; app.locals.versionRevLatest = ChildProcess.execSync('git ls-remote --refs -q').toString().trim().split('\t')[0];
} catch (error) { } catch (error) {
log.core.error("Failed to get latest git revision hash."); log.core.error('Failed to get latest git revision hash.');
app.locals.versionRevLatest = '0'; app.locals.versionRevLatest = '0';
} }
@ -61,19 +60,31 @@ if (app.locals.versionRevLong === app.locals.versionRevLatest) {
app.locals.versionUpdateAvailable = true; app.locals.versionUpdateAvailable = true;
} }
// ETA Init // ETA Init
const eta = new Eta({ views: path.join(__path, "views") }) const eta = new Eta({ views: path.join(__path, 'views') });
app.engine("eta", buildEtaEngine()) app.engine('eta', buildEtaEngine());
app.set("view engine", "eta") app.set('view engine', 'eta');
// MARK: Express Middleware & Config // MARK: Express Middleware & Config
app.set('x-powered-by', false); app.set('x-powered-by', false); // helmet does this too. But not in devmode
if (!config.global.devmode) {
app.use(
helmet({
strictTransportSecurity: config.global.http_enable_hsts,
contentSecurityPolicy: {
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", config.global.http_domain],
objectSrc: ["'none'"],
upgradeInsecureRequests: config.global.devmode ? null : []
}
}
})
); // Add headers
}
app.use(fileUpload()); app.use(fileUpload());
//app.use(cors());
app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json()); app.use(bodyParser.json());
@ -90,16 +101,13 @@ app.use(
app.use(passport.authenticate('session')); app.use(passport.authenticate('session'));
app.use(routes); app.use(routes);
app.listen(config.global.http_port, config.global.http_listen_address, () => { app.listen(config.global.http_port, config.global.http_listen_address, () => {
log.web.info(`Listening at http://${config.global.http_listen_address}:${config.global.http_port}`); log.web.info(`Listening at http://${config.global.http_listen_address}:${config.global.http_port}`);
}); });
// MARK: Helper Functions // MARK: Helper Functions
function buildEtaEngine() { function buildEtaEngine() {
return (path:string, opts:Options, callback: CallableFunction) => { return (path: string, opts: Options, callback: CallableFunction) => {
try { try {
const fileContent = eta.readFile(path); const fileContent = eta.readFile(path);
const renderedTemplate = eta.renderString(fileContent, opts); const renderedTemplate = eta.renderString(fileContent, opts);
@ -109,4 +117,3 @@ function buildEtaEngine() {
} }
}; };
} }

View File

@ -1,9 +1,11 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import validator from 'joi'; // DOCS: https://joi.dev/api import validator from 'joi'; // DOCS: https://joi.dev/api
import { Prisma } from '@prisma/client';
// MARK: GET alertContact // MARK: GET alertContact
const schema_get = validator.object({ const schema_get = validator.object({
sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'), //sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'),
sort: validator.string().valid(...Object.keys(Prisma.AlertContactsScalarFieldEnum)).default('id'),
order: validator.string().valid('asc', 'desc').default('asc'), order: validator.string().valid('asc', 'desc').default('asc'),
take: validator.number().min(1).max(512), take: validator.number().min(1).max(512),
skip: validator.number().min(0), skip: validator.number().min(0),

View File

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

View File

@ -1,11 +1,9 @@
import express from 'express'; import express from 'express';
// Route imports // Route imports
// import skuRoute from './:id.js';
// import skuRouteDash from './itemInfo.js'
// import testRoute from './test.js';
import dashboardRoute from './dashboard.js'; import dashboardRoute from './dashboard.js';
import testRoute from './test.js'; import testRoute from './test.js';
import contactRoute from './contact.js';
// import itemsRoute from './items.js'; // import itemsRoute from './items.js';
// import manage_routes from './manage/index.js'; // import manage_routes from './manage/index.js';
@ -22,5 +20,6 @@ const Router = express.Router({ strict: false });
Router.route('/').get(dashboardRoute.get); Router.route('/').get(dashboardRoute.get);
Router.route('/dbTest').get(testRoute.get); Router.route('/dbTest').get(testRoute.get);
Router.route('/contact').get(contactRoute.get);
export default Router; export default Router;

View File

@ -58,10 +58,63 @@ let _api = {
return; return;
} }
return result;
},
delete: async function (path, data) {
const options = {
method: 'DELETE',
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;
},
patch: async function (path, data) {
const options = {
method: 'PATCH',
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; return result;
} }
}; };
function updateRow(tableName, id, data) {
invalidateCache(tableName);
return _api.patch(`${tableName}`, { id: id, ...data });
}
function deleteRow(tableName, id) {
invalidateCache(tableName);
return _api.delete(`${tableName}`, { id: id });
}
function getApiDescriptionByTable(tableName) { function getApiDescriptionByTable(tableName) {
const keyDesc = `desc:${tableName}`; const keyDesc = `desc:${tableName}`;
const keyTime = `${keyDesc}:time`; const keyTime = `${keyDesc}:time`;

View File

@ -39,6 +39,9 @@ tables.forEach(async (table) => {
// Get THs and attach onClick event to sort // Get THs and attach onClick event to sort
const ths = table.querySelectorAll('th'); const ths = table.querySelectorAll('th');
ths.forEach((th) => { ths.forEach((th) => {
if(th.getAttribute('fnc') == "actions") {
return;
}
th.style.cursor = 'pointer'; th.style.cursor = 'pointer';
th.style.userSelect = 'none'; th.style.userSelect = 'none';
th.addEventListener('click', async function () { th.addEventListener('click', async function () {
@ -216,9 +219,17 @@ modalForms.forEach((modalForm) => {
jsonData[key] = value; jsonData[key] = value;
}); });
console.log('JSON Data: ', jsonData); console.log('JSON Data: ', jsonData);
let resp = await createEntry(table, jsonData); let resp = {};
if(modalForm.getAttribute('data-action') == 'edit') {
Rid = modalForm.getAttribute('data-rid');
resp = await updateRow(table, Rid,jsonData);
modalForm.setAttribute('data-action', 'create');
} else {
resp = await createEntry(table, jsonData);
}
console.log('Response: ', resp); console.log('Response: ', resp);
if (resp['status'] == 'CREATED') { if (resp['status'] == 'CREATED' || resp['status'] == 'UPDATED') {
console.log('Entry created successfully'); console.log('Entry created successfully');
modalForm.closest('.modal').classList.remove('is-active'); modalForm.closest('.modal').classList.remove('is-active');
modalForm.reset(); modalForm.reset();
@ -358,10 +369,18 @@ function writeDataToTable(table, data, paginationPassOn) {
// All required cols // All required cols
let requiredCols = []; let requiredCols = [];
let actionFields = [];
columns.forEach((column) => { columns.forEach((column) => {
// console.log('Column: ', column, ' FNC: ', column.getAttribute('data-fnc'), column.attributes);
if(column.getAttribute('data-fnc') == "actions") {
console.log('!!! Found actions column !!!');
actionFields.push(column);
return;
}
requiredCols.push(column.getAttribute('data-dataCol')); requiredCols.push(column.getAttribute('data-dataCol'));
}); });
// Get paginationPassOn // Get paginationPassOn
const start = paginationPassOn['start']; const start = paginationPassOn['start'];
const end = paginationPassOn['end']; const end = paginationPassOn['end'];
@ -457,6 +476,100 @@ function writeDataToTable(table, data, paginationPassOn) {
td.innerText = row[column]; td.innerText = row[column];
tr.appendChild(td); tr.appendChild(td);
}); });
// Add action fields
actionFields.forEach((actionField) => {
const td = document.createElement('td');
const actions = actionField.getAttribute('data-actions').split(',');
actions.forEach((action) => {
const button = document.createElement('button');
let icon = '';
let color = 'is-primary';
switch(action) {
case 'edit': {
icon = '<i class="bi bi-pencil"></i>';
break;
}
case 'delete': {
icon = '<i class="bi bi-trash"></i>';
color = 'is-danger';
break;
}
}
// Add classes
button.classList.add('button');
button.classList.add('is-small');
button.classList.add(color);
button.classList.add('is-outlined');
button.innerHTML = ` <span class="icon is-small">${icon}</span> `;
button.style.marginRight = '5px';
// Add data-action and data-id
button.setAttribute('data-action', action);
button.setAttribute("data-id", row["id"]);
// Add event listener
button.addEventListener('click', async function() {
const table = actionField.closest('table');
const row = button.closest('tr');
const columns = table.querySelectorAll('th');
const columnIndices = [];
columns.forEach((column, index) => {
columnIndices[column.getAttribute('data-dataCol')] = index;
});
const data = [];
columns.forEach((column) => {
data[column.getAttribute('data-dataCol')] = row.children[columnIndices[column.getAttribute('data-dataCol')]].innerText;
});
console.log('Data: ', data);
switch(action) {
case 'edit': {
// Open modal with form
const form = document.querySelector("form[data-targetTable='" + table.getAttribute('data-dataSource') + "']");
const formTitle = form.querySelector('.title');
const entryPhase = form.querySelector('.entryPhase');
const loadPhase = form.querySelector('.loadPhase');
const fields = form.querySelectorAll('input');
// Set modal to edit mode
form.setAttribute('data-action', 'edit');
form.setAttribute('data-rid', button.getAttribute('data-id'));
formTitle.innerText = 'Edit entry';
fields.forEach((field) => {
// Skip for submit button
if(field.getAttribute('type') == 'submit') {
return;
}
field.value = data[field.getAttribute('name')];
});
form.closest('.modal').classList.add('is-active');
// TBD
break;
}
case 'delete': {
// confirm
const confirm = window.confirm('Do you really want to delete this entry?');
// Delete entry
if(confirm) {
const table = actionField.closest('table');
const id = button.getAttribute('data-id');
const resp = await deleteRow(table.getAttribute('data-dataSource'), id);
if(resp['status'] == 'DELETED') {
refreshTable(table);
updateSingeltonsByTableName(table.getAttribute('data-dataSource'));
} else {
// Show error message
// TODO: Show error message
}
}
break;
}
}
}
);
td.appendChild(button);
});
tr.appendChild(td);
});
tbody.appendChild(tr); tbody.appendChild(tr);
} }
} }

View File

@ -96,7 +96,7 @@
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */

119
views/contacts.eta Normal file
View File

@ -0,0 +1,119 @@
<%~ include("partials/base_head.eta", {"title": "Kontakte"}) %>
<%~ include("partials/nav.eta") %>
<section class="hero is-primary" id="heroStatus">
<div class="hero-body">
<p class="title" data-tK="start-hero-header-welcome">Kontaktverwaltung</p>
<p class="subtitle" data-tK="start-hero-header-subtitle-default" id="heroExplainer">Erklärungstext</p>
</div>
</section>
<section class="section">
<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"><button class="js-modal-trigger button" data-target="modal-js-example">
Neuen Konakt anlegen
</button></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">Neuer Kontakt</h1>
<i class="bi bi-arrow-clockwise title"></i>
</div>
<div class="box entryPhase">
<form data-targetTable="AlertContacts">
<h2 class="title">Neuer Kontakt</h1>
<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">Telefonummer</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">Anmerkung</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>
<br>
<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 type="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>
<section class="section">
<h1 class="title" data-tK="start-recent-header">Kontaktübersicht</h1>
<input class="input" type="text" data-searchTargetId="contactTable" placeholder="Search..." />
<table class="table is-striped is-fullwidth is-hoverable" data-dataSource="AlertContacts" id="contactTable" data-pageSize="5">
<thead>
<tr>
<th data-dataCol = "id"><abbr title="Position">Pos</abbr></th>
<th data-dataCol = "name">Name</th>
<th data-dataCol = "phone">Telefonnummer</th>
<th data-dataCol = "comment">Kommentar</th>
<th data-fnc="actions" data-actions="edit,delete">Aktionen</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<nav class="pagination is-hidden" role="navigation" aria-label="pagination" data-targetTable="contactTable">
<ul class="pagination-list">
</ul>
</nav>
</section>
<%~ include("partials/footer.eta") %>
<%~ include("partials/base_foot.eta") %>

View File

@ -20,6 +20,7 @@
<div class="navbar-start"> <div class="navbar-start">
<a class="navbar-item" href="/">Home</a> <a class="navbar-item" href="/">Home</a>
<a class="navbar-item" href="/dbTest">API Integration <span class="tag is-info">Dev</span></a> <a class="navbar-item" href="/dbTest">API Integration <span class="tag is-info">Dev</span></a>
<a class="navbar-item" href="/contact">Kontakte <span class="tag is-primary">Neu!</span></a>
<!--<div class="navbar-item has-dropdown is-hoverable"> <!--<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">More</a> <a class="navbar-link">More</a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
@ -32,7 +33,7 @@
</div>--> </div>-->
</div> </div>
<!--<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <div class="buttons">
<a class="button is-primary"> <a class="button is-primary">
@ -41,6 +42,6 @@
<a class="button is-light">Log in</a> <a class="button is-light">Log in</a>
</div> </div>
</div> </div>
</div>--> </div>
</div> </div>
</nav> </nav>

View File

@ -32,6 +32,34 @@
</section> </section>
<section class="section">
<h1 class="title" data-tK="start-sysinfo-header">File test</h1>
<form method="put" action="/api/upload" enctype="multipart/form-data" id="uploadForm">
<div class="field">
<label class="label">File</label>
<div class="control">
<input class="input" type="file" name="file" id="file">
</div>
</div>
<!-- URL field -->
<div class="field">
<label class="label">URL</label>
<div class="control">
<input class="input" type="text" name="url" id="url">
</div>
<div class="field is-grouped">
<div class="control">
<input type="submit" class="button is-link" value="Upload" data-actionBtn="upload">
</div>
</div>
<script>
document.getElementById("url").addEventListener("input", function() {
document.getElementById("uploadForm").action = document.getElementById("url").value;
});
</script>
</form>
</section>
<!-- TODO: Mark required fields as required; add handling for validation --> <!-- TODO: Mark required fields as required; add handling for validation -->
<div id="modal-js-example" class="modal"> <div id="modal-js-example" class="modal">
<div class="modal-background"></div> <div class="modal-background"></div>
@ -43,8 +71,9 @@
<i class="bi bi-arrow-clockwise title"></i> <i class="bi bi-arrow-clockwise title"></i>
</div> </div>
<div class="box entryPhase"> <div class="box entryPhase">
<h2 class="title">New Contact</h1>
<form data-targetTable="AlertContacts"> <form data-targetTable="AlertContacts">
<h2 class="title">New Contact</h1>
<div class="field"> <div class="field">
<label class="label">Name</label> <label class="label">Name</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
@ -108,6 +137,7 @@
<th data-dataCol = "name">Name</th> <th data-dataCol = "name">Name</th>
<th data-dataCol = "phone">Telefon Nummer</th> <th data-dataCol = "phone">Telefon Nummer</th>
<th data-dataCol = "comment">Kommentar</th> <th data-dataCol = "comment">Kommentar</th>
<th data-fnc="actions" data-actions="edit,delete">Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>