Compare commits
	
		
			3 Commits
		
	
	
		
			be1544a46f
			...
			d4da439542
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d4da439542 | |||
| 8981fec28d | |||
| c96bdfddb0 | 
							
								
								
									
										61
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										61
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -16,6 +16,7 @@
 | 
			
		||||
				"express": "^4.21.2",
 | 
			
		||||
				"express-fileupload": "^1.5.1",
 | 
			
		||||
				"express-session": "^1.18.1",
 | 
			
		||||
				"joi": "^17.13.3",
 | 
			
		||||
				"jquery": "^3.7.1",
 | 
			
		||||
				"lodash": "^4.17.21",
 | 
			
		||||
				"passport": "^0.7.0",
 | 
			
		||||
@@ -28,6 +29,7 @@
 | 
			
		||||
				"@types/express": "^5.0.0",
 | 
			
		||||
				"@types/express-fileupload": "^1.5.1",
 | 
			
		||||
				"@types/express-session": "^1.18.1",
 | 
			
		||||
				"@types/joi": "^17.2.2",
 | 
			
		||||
				"@types/lodash": "^4.17.14",
 | 
			
		||||
				"@types/node": "^22.10.5",
 | 
			
		||||
				"@types/passport": "^1.0.17",
 | 
			
		||||
@@ -619,6 +621,21 @@
 | 
			
		||||
				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@hapi/hoek": {
 | 
			
		||||
			"version": "9.3.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
 | 
			
		||||
			"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
 | 
			
		||||
			"license": "BSD-3-Clause"
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@hapi/topo": {
 | 
			
		||||
			"version": "5.1.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
 | 
			
		||||
			"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
 | 
			
		||||
			"license": "BSD-3-Clause",
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@hapi/hoek": "^9.0.0"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@humanfs/core": {
 | 
			
		||||
			"version": "0.19.1",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
 | 
			
		||||
@@ -1032,6 +1049,27 @@
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "Apache-2.0"
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@sideway/address": {
 | 
			
		||||
			"version": "4.1.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
 | 
			
		||||
			"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
 | 
			
		||||
			"license": "BSD-3-Clause",
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@hapi/hoek": "^9.0.0"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@sideway/formula": {
 | 
			
		||||
			"version": "3.0.1",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
 | 
			
		||||
			"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
 | 
			
		||||
			"license": "BSD-3-Clause"
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@sideway/pinpoint": {
 | 
			
		||||
			"version": "2.0.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
 | 
			
		||||
			"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
 | 
			
		||||
			"license": "BSD-3-Clause"
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@types/body-parser": {
 | 
			
		||||
			"version": "1.19.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
 | 
			
		||||
@@ -1154,6 +1192,16 @@
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT"
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@types/joi": {
 | 
			
		||||
			"version": "17.2.2",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@types/joi/-/joi-17.2.2.tgz",
 | 
			
		||||
			"integrity": "sha512-vPvPwxn0Y4pQyqkEcMCJYxXCMYcrHqdfFX4SpF4zcqYioYexmDyxtM3OK+m/ZwGBS8/dooJ0il9qCwAdd6KFtA==",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"joi": "*"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@types/json-schema": {
 | 
			
		||||
			"version": "7.0.15",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
 | 
			
		||||
@@ -3585,6 +3633,19 @@
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "ISC"
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/joi": {
 | 
			
		||||
			"version": "17.13.3",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
 | 
			
		||||
			"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
 | 
			
		||||
			"license": "BSD-3-Clause",
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@hapi/hoek": "^9.3.0",
 | 
			
		||||
				"@hapi/topo": "^5.1.0",
 | 
			
		||||
				"@sideway/address": "^4.1.5",
 | 
			
		||||
				"@sideway/formula": "^3.0.1",
 | 
			
		||||
				"@sideway/pinpoint": "^2.0.0"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/jquery": {
 | 
			
		||||
			"version": "3.7.1",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@
 | 
			
		||||
		"@types/express": "^5.0.0",
 | 
			
		||||
		"@types/express-fileupload": "^1.5.1",
 | 
			
		||||
		"@types/express-session": "^1.18.1",
 | 
			
		||||
		"@types/joi": "^17.2.2",
 | 
			
		||||
		"@types/lodash": "^4.17.14",
 | 
			
		||||
		"@types/node": "^22.10.5",
 | 
			
		||||
		"@types/passport": "^1.0.17",
 | 
			
		||||
@@ -45,6 +46,7 @@
 | 
			
		||||
		"express": "^4.21.2",
 | 
			
		||||
		"express-fileupload": "^1.5.1",
 | 
			
		||||
		"express-session": "^1.18.1",
 | 
			
		||||
		"joi": "^17.13.3",
 | 
			
		||||
		"jquery": "^3.7.1",
 | 
			
		||||
		"lodash": "^4.17.21",
 | 
			
		||||
		"passport": "^0.7.0",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								src/helpers/prisma_helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/helpers/prisma_helpers.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,116 +1,87 @@
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import db, { handlePrismaError } from '../../../handlers/db.js'; // Database
 | 
			
		||||
import log from '../../../handlers/log.js';
 | 
			
		||||
import { parseIntOrUndefined, parseDynamicSortBy } from '../../../helpers/prisma_helpers.js';
 | 
			
		||||
import { schema_get } from './alertContacts_schema.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
 | 
			
		||||
// MARK: 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';
 | 
			
		||||
	}
 | 
			
		||||
	const { error, value } = schema_get.validate(req.query);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		log.api?.debug('Error:', req.query, value);
 | 
			
		||||
		res.status(400).json({ error: error.details[0].message });
 | 
			
		||||
	} else {
 | 
			
		||||
		log.api?.debug('Success:', req.query, value);
 | 
			
		||||
 | 
			
		||||
	// 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 = {
 | 
			
		||||
		// Query with FullTextSearch
 | 
			
		||||
		const query_fts = {
 | 
			
		||||
			where: {
 | 
			
		||||
				OR: [{ id: parseIntOrUndefined(req.query.id) }, { name: { search: req.query.search } }, { phone: { search: req.query.search } }, { comment: { search: req.query.search } }]
 | 
			
		||||
				OR: [{ id: parseIntOrUndefined(value.id) }, { name: { search: value.search } }, { phone: { search: value.search } }, { comment: { search: value.search } }]
 | 
			
		||||
			},
 | 
			
		||||
			orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
 | 
			
		||||
			orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString())
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		if (req.query.count === undefined) {
 | 
			
		||||
			// get all entrys
 | 
			
		||||
			await db.alertContacts.findMany(query).then((result) => {
 | 
			
		||||
				res.status(200).json(result);
 | 
			
		||||
			});
 | 
			
		||||
		if (value.search !== undefined || value.id !== undefined) {
 | 
			
		||||
			// with FullTextSearch
 | 
			
		||||
			if (!value.count) {
 | 
			
		||||
				// get all entrys
 | 
			
		||||
				log.api?.trace('get all entrys - with FullTextSearch');
 | 
			
		||||
				await db.alertContacts.findMany(query_fts).then((result) => {
 | 
			
		||||
					res.status(200).json(result);
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				// count all entrys
 | 
			
		||||
				log.api?.trace('count all entrys - with FullTextSearch');
 | 
			
		||||
				await db.alertContacts.count(query_fts).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);
 | 
			
		||||
			});
 | 
			
		||||
			// without FullTextSearch
 | 
			
		||||
			if (!value.count) {
 | 
			
		||||
				// get all entrys
 | 
			
		||||
				log.api?.trace('get all entrys - without FullTextSearch');
 | 
			
		||||
				await db.alertContacts.findMany({ orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()) }).then((result) => {
 | 
			
		||||
					res.status(200).json(result);
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				// count all entrys without FullTextSearch
 | 
			
		||||
				log.api?.trace('count all entrys - without FullTextSearch');
 | 
			
		||||
				await db.alertContacts.count().then((result) => {
 | 
			
		||||
					res.status(200).json(result);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CREATE AlertContact
 | 
			
		||||
// MARK: 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 });
 | 
			
		||||
		})
 | 
			
		||||
			.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' });
 | 
			
		||||
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing or invalid' });
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UPDATE AlertContact
 | 
			
		||||
// MARK: UPDATE AlertContact
 | 
			
		||||
async function patch(req: Request, res: Response) {}
 | 
			
		||||
 | 
			
		||||
// DELETE AlertContact
 | 
			
		||||
// MARK: DELETE AlertContact
 | 
			
		||||
async function del(req: Request, res: Response) {}
 | 
			
		||||
 | 
			
		||||
export default { get, post, patch, del };
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								src/routes/api/v1/alertContacts_schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/routes/api/v1/alertContacts_schema.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import validator from 'joi'; // DOCS: https://joi.dev/api
 | 
			
		||||
import log from '../../../handlers/log.js';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const schema_get = validator.object({
 | 
			
		||||
	sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'),
 | 
			
		||||
	order: validator.string().valid('asc', 'desc').default('asc'),
 | 
			
		||||
	search: validator.string().min(3).max(20), // TODO: Check if * or ** or *** -> Due to crashes..
 | 
			
		||||
	id: validator.number().positive().precision(0),
 | 
			
		||||
	count: validator.boolean()
 | 
			
		||||
}).nand('id', 'search'); // Allow id or search. not both.
 | 
			
		||||
 | 
			
		||||
const schema_post = validator.object({
 | 
			
		||||
	name: validator.string().min(1).max(32).required(),
 | 
			
		||||
	phone: validator.string().pattern(new RegExp('^\\+(\\d{1,3})\\s*(?:\\(\\s*(\\d{2,5})\\s*\\)|\\s*(\\d{2,5})\\s*)\\s*(\\d{5,15})$')).required(),
 | 
			
		||||
	comment: validator.string().max(64),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const schema_patch = validator.object({
 | 
			
		||||
	sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'),
 | 
			
		||||
	order: validator.string().valid('asc', 'desc').default('asc'),
 | 
			
		||||
	search: validator.string().min(3).max(20), // TODO: Check if * or ** or *** -> Due to crashes..
 | 
			
		||||
	id: validator.number().positive().precision(0),
 | 
			
		||||
	count: validator.boolean()
 | 
			
		||||
}).nand('id', 'search'); // Allow id or search. not both.
 | 
			
		||||
 | 
			
		||||
const schema_del = validator.object({
 | 
			
		||||
	sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'),
 | 
			
		||||
	order: validator.string().valid('asc', 'desc').default('asc'),
 | 
			
		||||
	search: validator.string().min(3).max(20), // TODO: Check if * or ** or *** -> Due to crashes..
 | 
			
		||||
	id: validator.number().positive().precision(0),
 | 
			
		||||
	count: validator.boolean()
 | 
			
		||||
}).nand('id', 'search'); // Allow id or search. not both.
 | 
			
		||||
 | 
			
		||||
// Describe all schemas
 | 
			
		||||
const schema_get_describe = schema_get.describe();
 | 
			
		||||
const schema_post_describe = schema_post.describe();
 | 
			
		||||
const schema_patch_describe = schema_patch.describe();
 | 
			
		||||
const schema_del_describe = schema_del.describe();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// GET route
 | 
			
		||||
export default async function get(req: Request, res: Response) {
 | 
			
		||||
	res.status(200).json({
 | 
			
		||||
		GET: schema_get_describe,
 | 
			
		||||
		POST: schema_post_describe,
 | 
			
		||||
		PATCH: schema_patch_describe,
 | 
			
		||||
		DELETE: schema_del_describe
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { schema_get, schema_post, schema_patch, schema_del }
 | 
			
		||||
@@ -3,14 +3,11 @@ import passport from 'passport';
 | 
			
		||||
 | 
			
		||||
// Route imports
 | 
			
		||||
import testRoute from './test.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 search_routes from './search/index.js';
 | 
			
		||||
import alertContactsRoute from './alertContacts.js';
 | 
			
		||||
import alertContactsRoute_schema from './alertContacts_schema.js';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Router base is '/api/v1'
 | 
			
		||||
const Router = express.Router({ strict: false });
 | 
			
		||||
@@ -27,11 +24,7 @@ Router.use('*', function (req, res, next) {
 | 
			
		||||
 | 
			
		||||
// 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('/alertcontacts/describe').get(alertContactsRoute_schema);
 | 
			
		||||
 | 
			
		||||
Router.route('/version').get(versionRoute.get);
 | 
			
		||||
//Router.use('/search', search_routes);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,109 +1,175 @@
 | 
			
		||||
_wrapperVersion = "1.0.0"
 | 
			
		||||
_minApiVersion = "1.0.0"
 | 
			
		||||
_maxApiVersion = "1.0.0"
 | 
			
		||||
_wrapperVersion = '1.0.0';
 | 
			
		||||
_minApiVersion = '1.0.0';
 | 
			
		||||
_maxApiVersion = '1.0.0';
 | 
			
		||||
 | 
			
		||||
_defaultTTL = 60000;
 | 
			
		||||
 | 
			
		||||
_apiConfig = {
 | 
			
		||||
	"basePath": "/api/v1/"
 | 
			
		||||
	basePath: '/api/v1/'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (!window.localStorage) {
 | 
			
		||||
	console.warn('Local Storage is not available, some features may not work');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Generic driver functions
 | 
			
		||||
let _api = {
 | 
			
		||||
	"get": async function(path) {
 | 
			
		||||
	get: async function (path) {
 | 
			
		||||
		const options = {
 | 
			
		||||
			headers: new Headers({'content-type': 'application/json'})
 | 
			
		||||
		    };
 | 
			
		||||
		const response = await fetch(_apiConfig.basePath + path, 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
 | 
			
		||||
			_testPageFail(response.statusText);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		const result = await response.json()
 | 
			
		||||
		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
 | 
			
		||||
			_testPageFail('Invalid JSON response');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return result;
 | 
			
		||||
	},
 | 
			
		||||
	"getAsync": function(path, callback) {
 | 
			
		||||
	post: async function (path, data) {
 | 
			
		||||
		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))
 | 
			
		||||
			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 getApiDescriptionByTable(tableName) {
 | 
			
		||||
	const keyDesc = `desc:${tableName}`;
 | 
			
		||||
	const keyTime = `${keyDesc}:time`;
 | 
			
		||||
	const keyTTL = `${keyDesc}:ttl`;
 | 
			
		||||
 | 
			
		||||
	// Retrieve cached data
 | 
			
		||||
	const description = JSON.parse(localStorage.getItem(keyDesc));
 | 
			
		||||
	const timeCreated = parseInt(localStorage.getItem(keyTime));
 | 
			
		||||
	const ttl = parseInt(localStorage.getItem(keyTTL));
 | 
			
		||||
 | 
			
		||||
	// Check if valid cached data exists
 | 
			
		||||
	if (description && timeCreated && ttl) {
 | 
			
		||||
		const currentTime = Date.now();
 | 
			
		||||
		const age = currentTime - parseInt(timeCreated, 10);
 | 
			
		||||
		if (age < parseInt(ttl, 10)) {
 | 
			
		||||
			// Return cached data immediately
 | 
			
		||||
			return Promise.resolve(description);
 | 
			
		||||
		} else {
 | 
			
		||||
			console.warn('Cached description expired; fetching new data');
 | 
			
		||||
			// Fetch new data, update cache, and return it
 | 
			
		||||
			return fetchAndUpdateCache(tableName);
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		console.warn('No cached description; fetching from server');
 | 
			
		||||
		// Fetch data, update cache, and return it
 | 
			
		||||
		return fetchAndUpdateCache(tableName);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function fetchAndUpdateCache(tableName) {
 | 
			
		||||
		return _api
 | 
			
		||||
			.get(`${tableName}/describe`)
 | 
			
		||||
			.then((data) => {
 | 
			
		||||
				if (data) {
 | 
			
		||||
					// Update local storage with new data
 | 
			
		||||
					localStorage.setItem(keyDesc, JSON.stringify(data));
 | 
			
		||||
					localStorage.setItem(keyTime, Date.now().toString());
 | 
			
		||||
					localStorage.setItem(keyTTL, '60000'); // 60 seconds TTL
 | 
			
		||||
				}
 | 
			
		||||
				return data; // Return the fetched data
 | 
			
		||||
			})
 | 
			
		||||
			.catch((error) => {
 | 
			
		||||
				console.error('Failed to fetch description:', error);
 | 
			
		||||
				// Fallback to cached data if available (even if expired)
 | 
			
		||||
				return description || null;
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function returnTableDataByTableName(tableName) {
 | 
			
		||||
    return _api.get(tableName)
 | 
			
		||||
	return _api.get(tableName);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function returnTableDataByTableNameWithSearch(tableName, search) {
 | 
			
		||||
	return _api.get(tableName + "?search=" + search)
 | 
			
		||||
    }
 | 
			
		||||
	return _api.get(tableName + '?search=' + search);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function returnTableDataByTableNameAsync(tableName, callback) {
 | 
			
		||||
	    _api.getAsync(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
 | 
			
		||||
	// Stored in `data:count:${tableName}`
 | 
			
		||||
	let result = await _api.get(tableName + '?count=true');
 | 
			
		||||
	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
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRowsByTableAndColumnList(tableName, columnList) {
 | 
			
		||||
	    //return _api.get(tableName + '/rows/' + columnList.join(','))
 | 
			
		||||
	    return undefined
 | 
			
		||||
	//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('heroStatus').classList.remove('is-success');
 | 
			
		||||
	document.getElementById('heroStatus').classList.add('is-danger');
 | 
			
		||||
 | 
			
		||||
	document.getElementById("heroExplainer").innerHTML = "API Wrapper Test Failed, reason: " + reason
 | 
			
		||||
	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('heroStatus').classList.remove('is-success');
 | 
			
		||||
	document.getElementById('heroStatus').classList.add('is-warning');
 | 
			
		||||
 | 
			
		||||
	document.getElementById("heroExplainer").innerHTML = "API Wrapper Test Warning, reason: " + reason
 | 
			
		||||
	document.getElementById('heroExplainer').innerHTML = 'API Wrapper Test Warning, reason: ' + reason;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getServerVersion() {
 | 
			
		||||
	return _api.get('version')
 | 
			
		||||
	return _api.get('version');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createEntry(tableName, data) {
 | 
			
		||||
	return _api.post(tableName, data)
 | 
			
		||||
	invalidateCache(tableName);
 | 
			
		||||
	return _api.post(tableName, data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function invalidateCache(tableName) {
 | 
			
		||||
	const keyDesc = `desc:${tableName}`;
 | 
			
		||||
	const keyTime = `${keyDesc}:time`;
 | 
			
		||||
	const keyTTL = `${keyDesc}:ttl`;
 | 
			
		||||
 | 
			
		||||
	localStorage.removeItem(keyDesc);
 | 
			
		||||
	localStorage.removeItem(keyTime);
 | 
			
		||||
	localStorage.removeItem(keyTTL);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,55 +1,55 @@
 | 
			
		||||
_pageDriverVersion = "1.0.1";
 | 
			
		||||
_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"); 
 | 
			
		||||
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");
 | 
			
		||||
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");
 | 
			
		||||
	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 = 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]");
 | 
			
		||||
// 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]");
 | 
			
		||||
var searchFields = document.querySelectorAll('input[data-searchTargetId]');
 | 
			
		||||
 | 
			
		||||
// Find all modalForms
 | 
			
		||||
var modalForms = document.querySelectorAll("form[data-targetTable]");
 | 
			
		||||
var modalForms = document.querySelectorAll('form[data-targetTable]');
 | 
			
		||||
 | 
			
		||||
// Iterate over all tables
 | 
			
		||||
tables.forEach(async table => {
 | 
			
		||||
	console.log("Table found: ", table);
 | 
			
		||||
tables.forEach(async (table) => {
 | 
			
		||||
	console.log('Table found: ', table);
 | 
			
		||||
	// Get THEAD and TBODY elements
 | 
			
		||||
	const thead = table.querySelector("thead");
 | 
			
		||||
	const tbody = table.querySelector("tbody");
 | 
			
		||||
	const thead = table.querySelector('thead');
 | 
			
		||||
	const tbody = table.querySelector('tbody');
 | 
			
		||||
 | 
			
		||||
	// get index per column
 | 
			
		||||
	const columns = thead.querySelectorAll("th");
 | 
			
		||||
	const columns = thead.querySelectorAll('th');
 | 
			
		||||
	const columnIndices = [];
 | 
			
		||||
	columns.forEach((column, index) => {
 | 
			
		||||
		columnIndices[column.getAttribute("data-dataCol")] = index;
 | 
			
		||||
		columnIndices[column.getAttribute('data-dataCol')] = index;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// All required cols
 | 
			
		||||
	let requiredCols = [];
 | 
			
		||||
	columns.forEach(column => {
 | 
			
		||||
		requiredCols.push(column.getAttribute("data-dataCol"));
 | 
			
		||||
	columns.forEach((column) => {
 | 
			
		||||
		requiredCols.push(column.getAttribute('data-dataCol'));
 | 
			
		||||
	});
 | 
			
		||||
	console.log("Required columns: ", requiredCols);
 | 
			
		||||
	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"))
 | 
			
		||||
	let result = await returnTableDataByTableName(table.getAttribute('data-dataSource'));
 | 
			
		||||
	// for (resultIndex in result) {
 | 
			
		||||
	// 	const row = result[resultIndex];
 | 
			
		||||
	// 	const tr = document.createElement("tr");
 | 
			
		||||
@@ -62,122 +62,192 @@ tables.forEach(async table => {
 | 
			
		||||
	// }
 | 
			
		||||
	writeDataToTable(table, result);
 | 
			
		||||
 | 
			
		||||
	console.log("Column indices: ", columnIndices);
 | 
			
		||||
	console.log('Column indices: ', columnIndices);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
console.info("Processing single values");
 | 
			
		||||
console.info('Processing single values');
 | 
			
		||||
console.info(singleValues);
 | 
			
		||||
 | 
			
		||||
// Iterate over all single values
 | 
			
		||||
singleValues.forEach(async singleValue => {
 | 
			
		||||
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))
 | 
			
		||||
	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
 | 
			
		||||
		case 'SPECIAL': {
 | 
			
		||||
			if (table == 'version') {
 | 
			
		||||
				element.innerHTML = (await getServerVersion())['version'];
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
		}
 | 
			
		||||
		default: {
 | 
			
		||||
			console.error("Unknown action found: ", element.getAttribute("data-dataAction"));
 | 
			
		||||
			console.error('Unknown action found: ', element.getAttribute('data-dataAction'));
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	element.classList.remove("is-skeleton");
 | 
			
		||||
 | 
			
		||||
	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);
 | 
			
		||||
searchFields.forEach((searchField) => {
 | 
			
		||||
	// Apply restrictions to search field (min, max, chars, etc)
 | 
			
		||||
 | 
			
		||||
	getApiDescriptionByTable(document.getElementById(searchField.getAttribute('data-searchTargetId')).getAttribute('data-dataSource')).then((desc) => {
 | 
			
		||||
		desc = desc['GET']['keys']['search'];
 | 
			
		||||
		var rules = desc['rules'];
 | 
			
		||||
		rules.forEach((rule) => {
 | 
			
		||||
			switch (rule['name']) {
 | 
			
		||||
				case 'min': {
 | 
			
		||||
					searchField.setAttribute('minlength', rule['args']['limit']);
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
				case 'max': {
 | 
			
		||||
					searchField.setAttribute('maxlength', rule['args']['limit']);
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	searchField.addEventListener('input', async function () {
 | 
			
		||||
		console.log('Search field changed: ', searchField);
 | 
			
		||||
		if (searchField.checkValidity() == false) {
 | 
			
		||||
			console.log('Invalid input');
 | 
			
		||||
			searchField.classList.add('is-danger');
 | 
			
		||||
			return;
 | 
			
		||||
		} else {
 | 
			
		||||
			searchField.classList.remove('is-danger');
 | 
			
		||||
			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);
 | 
			
		||||
			//clearTable(target);
 | 
			
		||||
			//writeDataToTable(target, result);
 | 
			
		||||
			refreshTableByName(table);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Attach listeners to modal forms
 | 
			
		||||
modalForms.forEach(modalForm => {
 | 
			
		||||
	modalForm.addEventListener("submit", async function(event) {
 | 
			
		||||
modalForms.forEach((modalForm) => {
 | 
			
		||||
	// Add validation to form by using API description (everything is assumed POST for now)
 | 
			
		||||
	modalForm.addEventListener('input', async function (event) {
 | 
			
		||||
		if (event.target.checkValidity() == false) {
 | 
			
		||||
			modalForm.querySelector("input[type='submit']").setAttribute('disabled', true);
 | 
			
		||||
			event.target.classList.add('is-danger');
 | 
			
		||||
			return;
 | 
			
		||||
		} else {
 | 
			
		||||
			modalForm.querySelector("input[type='submit']").removeAttribute('disabled');
 | 
			
		||||
			event.target.classList.remove('is-danger');
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	getApiDescriptionByTable(modalForm.getAttribute('data-targetTable')).then((desc) => {
 | 
			
		||||
		console.log('Description: ', desc);
 | 
			
		||||
		const keys = desc['POST']['keys'];
 | 
			
		||||
		// Apply resitrictions and types to form fields
 | 
			
		||||
		for (key in keys) {
 | 
			
		||||
			const field = modalForm.querySelector("input[name='" + key + "']");
 | 
			
		||||
			if (field) {
 | 
			
		||||
				const rules = keys[key]['rules'];
 | 
			
		||||
				const flags = keys[key]['flags'];
 | 
			
		||||
				console.log('Field: ', field, ' Rules: ', rules, ' Flags: ', flags);
 | 
			
		||||
				rules.forEach((rule) => {
 | 
			
		||||
					switch (rule['name']) {
 | 
			
		||||
						case 'min': {
 | 
			
		||||
							field.setAttribute('minlength', rule['args']['limit']);
 | 
			
		||||
							break;
 | 
			
		||||
						}
 | 
			
		||||
						case 'max': {
 | 
			
		||||
							field.setAttribute('maxlength', rule['args']['limit']);
 | 
			
		||||
							break;
 | 
			
		||||
						}
 | 
			
		||||
						case 'pattern': {
 | 
			
		||||
							field.setAttribute('pattern', rule['args']['regex'].substring(1, rule['args']['regex'].length - 1));
 | 
			
		||||
							//field.setAttribute("pattern", "^[\\+]?[\\(]?[0-9]{3}[\\)]?[\\-\\s\\.]?[0-9]{3}[\\-\\s\\.]?[0-9]{4,9}$");
 | 
			
		||||
							break;
 | 
			
		||||
						}
 | 
			
		||||
						case 'type': {
 | 
			
		||||
							//field.setAttribute("type", rule["args"]["type"]);
 | 
			
		||||
							console.log('Type: ', rule['args']['type']);
 | 
			
		||||
							break;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
				if (flags) {
 | 
			
		||||
					flags['presence'] == 'required' ? field.setAttribute('required', true) : field.removeAttribute('required');
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		console.log('Keys: ', keys);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	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');
 | 
			
		||||
		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");
 | 
			
		||||
		const entryPhase = modalForm.querySelector('.entryPhase');
 | 
			
		||||
		const loadPhase = modalForm.querySelector('.loadPhase');
 | 
			
		||||
		if (entryPhase) {
 | 
			
		||||
			entryPhase.classList.add('is-hidden');
 | 
			
		||||
		}
 | 
			
		||||
		if(loadPhase) {
 | 
			
		||||
			loadPhase.classList.remove("is-hidden");
 | 
			
		||||
		if (loadPhase) {
 | 
			
		||||
			loadPhase.classList.remove('is-hidden');
 | 
			
		||||
		}
 | 
			
		||||
		console.log("Form submitted: ", modalForm);
 | 
			
		||||
		const table = modalForm.getAttribute("data-targetTable");
 | 
			
		||||
		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);
 | 
			
		||||
		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');
 | 
			
		||||
		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");
 | 
			
		||||
			if (loadPhase) {
 | 
			
		||||
				loadPhase.classList.add('is-hidden');
 | 
			
		||||
			}
 | 
			
		||||
			// Show entryPhase
 | 
			
		||||
			if(entryPhase) {
 | 
			
		||||
				entryPhase.classList.remove("is-hidden");
 | 
			
		||||
			if (entryPhase) {
 | 
			
		||||
				entryPhase.classList.remove('is-hidden');
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// Hide loadPhase
 | 
			
		||||
			if(loadPhase) {
 | 
			
		||||
				loadPhase.classList.add("is-hidden");
 | 
			
		||||
			if (loadPhase) {
 | 
			
		||||
				loadPhase.classList.add('is-hidden');
 | 
			
		||||
			}
 | 
			
		||||
			// Show entryPhase
 | 
			
		||||
			if(entryPhase) {
 | 
			
		||||
				entryPhase.classList.remove("is-hidden");
 | 
			
		||||
			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(() => {
 | 
			
		||||
@@ -191,14 +261,14 @@ modalForms.forEach(modalForm => {
 | 
			
		||||
async function refreshTable(table) {
 | 
			
		||||
	// Refresh a table while keeping (optionally set) search value
 | 
			
		||||
	const searchField = document.querySelector("input[data-searchTargetId='" + table.id + "']");
 | 
			
		||||
	if(searchField) {
 | 
			
		||||
	if (searchField && searchField.value != '') {
 | 
			
		||||
		const value = searchField.value;
 | 
			
		||||
		const dbTable = table.getAttribute("data-dataSource");
 | 
			
		||||
		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"));
 | 
			
		||||
		const result = await returnTableDataByTableName(table.getAttribute('data-dataSource'));
 | 
			
		||||
		clearTable(table);
 | 
			
		||||
		writeDataToTable(table, result);
 | 
			
		||||
	}
 | 
			
		||||
@@ -206,48 +276,47 @@ async function refreshTable(table) {
 | 
			
		||||
 | 
			
		||||
async function refreshTableByName(name) {
 | 
			
		||||
	const dirtyTables = document.querySelectorAll("table[data-dataSource='" + name + "']");
 | 
			
		||||
	for(dirty of dirtyTables) {
 | 
			
		||||
	for (dirty of dirtyTables) {
 | 
			
		||||
		refreshTable(dirty);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function updateSingeltonsByTableName(name) {
 | 
			
		||||
	const dirtySingles = document.querySelectorAll("span[data-dataSource='" + name + "']");
 | 
			
		||||
	for(dirty of dirtySingles) {
 | 
			
		||||
	for (dirty of dirtySingles) {
 | 
			
		||||
		writeSingelton(dirty);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function clearTable(table) {
 | 
			
		||||
	const tbody = table.querySelector("tbody");
 | 
			
		||||
	tbody.innerHTML = "";
 | 
			
		||||
	const tbody = table.querySelector('tbody');
 | 
			
		||||
	tbody.innerHTML = '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function writeDataToTable(table, data) {
 | 
			
		||||
	console.log("Writing data to table: ", 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");
 | 
			
		||||
	const thead = table.querySelector('thead');
 | 
			
		||||
	const tbody = table.querySelector('tbody');
 | 
			
		||||
 | 
			
		||||
	// get index per column
 | 
			
		||||
	const columns = thead.querySelectorAll("th");
 | 
			
		||||
	const columns = thead.querySelectorAll('th');
 | 
			
		||||
	const columnIndices = [];
 | 
			
		||||
	columns.forEach((column, index) => {
 | 
			
		||||
		columnIndices[column.getAttribute("data-dataCol")] = index;
 | 
			
		||||
		columnIndices[column.getAttribute('data-dataCol')] = index;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// All required cols
 | 
			
		||||
	let requiredCols = [];
 | 
			
		||||
	columns.forEach(column => {
 | 
			
		||||
		requiredCols.push(column.getAttribute("data-dataCol"));
 | 
			
		||||
	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");
 | 
			
		||||
		const tr = document.createElement('tr');
 | 
			
		||||
		requiredCols.forEach((column) => {
 | 
			
		||||
			const td = document.createElement('td');
 | 
			
		||||
			td.innerHTML = row[column];
 | 
			
		||||
			tr.appendChild(td);
 | 
			
		||||
		});
 | 
			
		||||
@@ -255,47 +324,46 @@ function writeDataToTable(table, data) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Handle modal 
 | 
			
		||||
// Handle modal
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
	// Functions to open and close a modal
 | 
			
		||||
	function openModal($el) {
 | 
			
		||||
	  $el.classList.add('is-active');
 | 
			
		||||
		$el.classList.add('is-active');
 | 
			
		||||
	}
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
	function closeModal($el) {
 | 
			
		||||
	  $el.classList.remove('is-active');
 | 
			
		||||
		$el.classList.remove('is-active');
 | 
			
		||||
	}
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
	function closeAllModals() {
 | 
			
		||||
	  (document.querySelectorAll('.modal') || []).forEach(($modal) => {
 | 
			
		||||
	    closeModal($modal);
 | 
			
		||||
	  });
 | 
			
		||||
		(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);
 | 
			
		||||
	  });
 | 
			
		||||
		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);
 | 
			
		||||
	  });
 | 
			
		||||
	(document.querySelectorAll('.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();
 | 
			
		||||
	  }
 | 
			
		||||
		if (event.key === 'Escape') {
 | 
			
		||||
			closeAllModals();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
      });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -35,67 +35,68 @@
 | 
			
		||||
<!-- 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">
 | 
			
		||||
		<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">
 | 
			
		||||
					<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>
 | 
			
		||||
					</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			      </div>
 | 
			
		||||
		      
 | 
			
		||||
		      <div class="field">
 | 
			
		||||
			</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">
 | 
			
		||||
				<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>
 | 
			
		||||
				</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		      </div>
 | 
			
		||||
		      
 | 
			
		||||
		      <div class="field">
 | 
			
		||||
			</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">
 | 
			
		||||
				<input class="input" type="text" placeholder="" value="" name="comment">
 | 
			
		||||
				<span class="icon is-small is-left">
 | 
			
		||||
				<i class="bi bi-chat-fill"></i>
 | 
			
		||||
			  </span>
 | 
			
		||||
				</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		      </div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<br>
 | 
			
		||||
 | 
			
		||||
		      <div class="field is-grouped">
 | 
			
		||||
			<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 class="control">
 | 
			
		||||
				<button type="button" class="button is-link is-light" data-actionBtn="cancel">Cancel</button>
 | 
			
		||||
			</div>-->
 | 
			
		||||
			</div>
 | 
			
		||||
		      </div>
 | 
			
		||||
		</form>
 | 
			
		||||
		      
 | 
			
		||||
		      
 | 
			
		||||
	  </div>
 | 
			
		||||
			
 | 
			
		||||
			
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
	<button class="modal-close is-large" aria-label="close"></button>
 | 
			
		||||
      </div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      <button class="js-modal-trigger button" data-target="modal-js-example">
 | 
			
		||||
	<button class="js-modal-trigger button" data-target="modal-js-example">
 | 
			
		||||
	Create new Contact
 | 
			
		||||
      </button>
 | 
			
		||||
	</button>
 | 
			
		||||
 | 
			
		||||
<section class="section">
 | 
			
		||||
	<h1 class="title" data-tK="start-recent-header">Alarm Kontakte</h1>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user