Compare commits
97 Commits
524feee54d
...
AFLOW-36-p
Author | SHA1 | Date | |
---|---|---|---|
d359f55f39 | |||
85ccc7523f | |||
c23b1b306c | |||
2371089f88 | |||
6fa2797903 | |||
adc466e09a | |||
af896a6688 | |||
347979bb10 | |||
ddfdfc3092 | |||
56cbebb36b | |||
e307ff97ac | |||
ddb484cac9 | |||
cd37f096ca | |||
f52897fd4d | |||
b0b47e04f8 | |||
660c9c092e | |||
6b092b34b3 | |||
421085a8d5 | |||
ea80b4bf2b | |||
94186a3a18 | |||
db6df2fdc6 | |||
9b2db6eed7 | |||
cdbd4c3c10 | |||
bc9d395e77 | |||
16da321177 | |||
6f7f65fa36 | |||
1605987952 | |||
a79a1eab81 | |||
45a4935190 | |||
ff07698f16 | |||
5aeec6fb28 | |||
3be376b214 | |||
3f55b22ede | |||
534cc3055f | |||
abb7e7bab3 | |||
09e74f9eb6 | |||
720a969484 | |||
5524f14e1a | |||
58a2d2ad19 | |||
c50aa8990c | |||
8d954052f2 | |||
0e4bc7669a | |||
0233453084 | |||
c026b5f1a8 | |||
5d99baea8e | |||
587dac99c5 | |||
45bec04007 | |||
d38713e7ed | |||
5584cc5c41 | |||
57513da827 | |||
185d563ac0 | |||
e0ac509007 | |||
90924aa30d | |||
f4d6ed4d8f | |||
1b7b8af118 | |||
eb3b97240d | |||
6ba0716cfc | |||
b66367c34a | |||
a7864b3c11 | |||
b785dd8ca7 | |||
4c0be6d87b | |||
87e1c55553 | |||
a4d697265b | |||
ad84e6a3a0 | |||
656ca2f74a | |||
ac7ebbbf5e | |||
137da0e31e | |||
db0e8c2047 | |||
efe36fc60a | |||
9ab12118a0 | |||
578b21d4b5 | |||
24a9deae62 | |||
f249a4552c | |||
3b1e4a7cde | |||
ce31beb1a8 | |||
e11bea21ea | |||
2dd52a0c1d | |||
1f85dd5710 | |||
50d98c0894 | |||
74db923058 | |||
d5fcf94455 | |||
584b00c878 | |||
2b5831fccb | |||
ce04e4ff1c | |||
7562f7005b | |||
cfc28c5959 | |||
b29550f429 | |||
37649ec98e | |||
a8b0374d5e | |||
90dbadac24 | |||
4145dafb7d | |||
037d03cc50 | |||
b6ebda8fb5 | |||
1076c03f2c | |||
713cadcba1 | |||
9411f1ad72 | |||
259ec997c8 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ node_modules
|
||||
config.json
|
||||
dist
|
||||
docs
|
||||
demoData.json
|
@ -13,3 +13,4 @@ LocID_Regal16_Fach7
|
||||
StorageLocation_LocID_Regal16_Fach7
|
||||
|
||||
|
||||
Please also reference our wiki at https://project-name-here.atlassian.net/wiki/spaces/AFLOW/overview
|
@ -10,7 +10,9 @@
|
||||
"/@popperjs/core/dist/umd/popper.min.js.map",
|
||||
"/bootstrap/dist/js/bootstrap.bundle.min.js.map",
|
||||
"/bootstrap-icons/font/fonts/bootstrap-icons.woff",
|
||||
"/tsparticles-confetti/tsparticles.confetti.bundle.min.js"
|
||||
"/tsparticles-confetti/tsparticles.confetti.bundle.min.js",
|
||||
"/bootstrap-table/dist/bootstrap-table.min.js",
|
||||
"/bootstrap-table/dist/bootstrap-table.min.css"
|
||||
],
|
||||
"debugMode": false
|
||||
}
|
||||
|
1458
package-lock.json
generated
1458
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc && sass src/sass/dashboard-mod.scss static/css/dashboard-mod.css",
|
||||
"build": "tsc",
|
||||
"prestart": "npm run build",
|
||||
"start": "node .",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
@ -18,32 +18,39 @@
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"@prisma/client": "^4.13.0",
|
||||
"@prisma/client": "^4.14.1",
|
||||
"@sentry/node": "^7.52.1",
|
||||
"@sentry/tracing": "^7.52.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"bootstrap": "^5.3.0-alpha3",
|
||||
"bootstrap-icons": "^1.10.5",
|
||||
"bootstrap-table": "^1.22.1",
|
||||
"csv": "^6.2.11",
|
||||
"eta": "^2.0.1",
|
||||
"express": "^4.18.2",
|
||||
"express-fileupload": "^1.4.0",
|
||||
"express-session": "^1.17.3",
|
||||
"jquery": "^3.6.4",
|
||||
"lodash": "^4.17.21",
|
||||
"prisma": "^4.13.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"signale": "^1.4.0",
|
||||
"tsparticles-confetti": "^2.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@loancrate/prisma-schema-parser": "^2.0.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express-fileupload": "^1.4.1",
|
||||
"@types/express-session": "^1.17.7",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/passport": "^1.0.12",
|
||||
"@types/passport-local": "^1.0.35",
|
||||
"@types/signale": "^1.4.4",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"prisma": "^4.14.1",
|
||||
"prisma-dbml-generator": "^0.10.0",
|
||||
"prisma-docs-generator": "^0.7.0",
|
||||
"sass": "^1.62.1",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
|
@ -32,28 +32,32 @@ enum itemStatus {
|
||||
lost
|
||||
}
|
||||
|
||||
// comments and descriptions -> @db.VarChar(2048)
|
||||
model Item {
|
||||
id Int @id @unique @default(autoincrement())
|
||||
SKU String? @unique
|
||||
amount Int @default(1)
|
||||
name String
|
||||
comment String?
|
||||
comment String? @db.VarChar(2048)
|
||||
status itemStatus @default(normal) /// TODO: Would it be better to create a separate model for this as well instead of providing several static statuses to choose from(enum)?
|
||||
|
||||
manufacturer String
|
||||
contactInfo contactInfo? @relation(fields: [contactInfoId], references: [id])
|
||||
contactInfoId Int?
|
||||
|
||||
manufacturer String?
|
||||
|
||||
category itemCategory? @relation(fields: [categoryId], references: [id])
|
||||
categoryId Int?
|
||||
|
||||
items Item[] @relation("items") /// Item beinhaltet..
|
||||
baseItem Item[] @relation("items") /// Item zugehörig zu.
|
||||
contents Item[] @relation("items") /// Item beinhaltet..
|
||||
baseItem Item[] @relation("items") /// Item zugehörig zu
|
||||
|
||||
storageLocation StorageLocation? @relation(fields: [storageLocationId], references: [id])
|
||||
storageLocationId Int?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
importedBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdBy String?
|
||||
}
|
||||
|
||||
model StorageLocation {
|
||||
@ -78,11 +82,10 @@ model StorageUnit {
|
||||
model itemCategory {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
description String?
|
||||
description String? @db.VarChar(2048)
|
||||
Item Item[]
|
||||
}
|
||||
|
||||
/// TODO: Add relationship to StorageUnit, Item and if necessary to StorageLocation.
|
||||
model contactInfo {
|
||||
id Int @id @default(autoincrement())
|
||||
type contactType @default(person)
|
||||
@ -95,10 +98,28 @@ model contactInfo {
|
||||
country String
|
||||
|
||||
StorageUnit StorageUnit[]
|
||||
Item Item[]
|
||||
project project[]
|
||||
projectAssignedUsers project[] @relation("projectAssignedUsers")
|
||||
}
|
||||
|
||||
model project {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
description String? @db.VarChar(2048)
|
||||
// People
|
||||
manager contactInfo? @relation(fields: [contactInfoId], references: [id]) // Primary, manager of the project
|
||||
assignedUsers contactInfo[] @relation("projectAssignedUsers") // Secondary, assigned users to the project, stagehands, etc.
|
||||
contactInfoId Int?
|
||||
// When does it start and end
|
||||
startTime DateTime?
|
||||
endTime DateTime?
|
||||
}
|
||||
|
||||
/// TODO: Allow multiple types to be used?
|
||||
enum contactType {
|
||||
storageUnit
|
||||
owner
|
||||
person
|
||||
customer
|
||||
company
|
||||
|
@ -1,7 +1,8 @@
|
||||
import fs from 'node:fs';
|
||||
import _ from 'lodash';
|
||||
import { randomUUID, randomBytes } from 'crypto';
|
||||
|
||||
export type configObject = Record<any, any>
|
||||
export type configObject = Record<any, any>;
|
||||
|
||||
/**
|
||||
* This class is responsible to save/edit config files.
|
||||
@ -13,7 +14,8 @@ export type configObject = Record<any, any>
|
||||
export default class config {
|
||||
#configPath: string;
|
||||
//global = {[key: string] : string}
|
||||
global: configObject
|
||||
global: configObject;
|
||||
replaceSecrets: boolean;
|
||||
|
||||
/**
|
||||
* Creates an instance of config.
|
||||
@ -22,9 +24,10 @@ export default class config {
|
||||
* @param {string} configPath Path to config file.
|
||||
* @param {object} configPreset Default config object with default values.
|
||||
*/
|
||||
constructor(configPath: string, configPreset: object) {
|
||||
constructor(configPath: string, replaceSecrets: boolean, configPreset: object) {
|
||||
this.#configPath = configPath;
|
||||
this.global = configPreset;
|
||||
this.replaceSecrets = replaceSecrets;
|
||||
|
||||
try {
|
||||
// Read config
|
||||
@ -35,7 +38,15 @@ export default class config {
|
||||
// Save config.
|
||||
this.save_config();
|
||||
} catch (err) {
|
||||
console.error('Could not read config file at ' + this.#configPath + ' due to: ' + err);
|
||||
// If file does not exist, create it.
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log(`Config file does not exist. Creating it at ${this.#configPath} now.`);
|
||||
this.save_config();
|
||||
return;
|
||||
}
|
||||
console.error(`Could not read config file at ${this.#configPath} due to: ${err}`);
|
||||
// Exit process.
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,40 +55,86 @@ export default class config {
|
||||
*/
|
||||
save_config() {
|
||||
try {
|
||||
// If enabled replace tokens defines as "gen" with random token
|
||||
if (this.replaceSecrets) {
|
||||
// Replace tokens with value "gen"
|
||||
this.generate_secrets(this.global, 'gen')
|
||||
}
|
||||
|
||||
fs.writeFileSync(this.#configPath, JSON.stringify(this.global, null, 8));
|
||||
} catch (err) {
|
||||
console.error('Could not write config file at ' + this.#configPath + ' due to: ' + err);
|
||||
console.error(`Could not write config file at ${this.#configPath} due to: ${err}`);
|
||||
return;
|
||||
}
|
||||
console.log('Successfully written config file to ' + this.#configPath);
|
||||
console.log(`Successfully written config file to ${this.#configPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces each item matching the value of placeholder with a random UUID.
|
||||
* Thanks to https://stackoverflow.com/questions/8085004/iterate-through-nested-javascript-objects
|
||||
* @param {configObject} obj
|
||||
*/
|
||||
generate_secrets(obj: configObject, placeholder: string) {
|
||||
const stack = [obj];
|
||||
while (stack?.length > 0) {
|
||||
const currentObj = stack.pop();
|
||||
Object.keys(currentObj).forEach((key) => {
|
||||
|
||||
if (currentObj[key] === placeholder) {
|
||||
console.log('Generating secret: ' + key);
|
||||
currentObj[key] = randomBytes(48).toString('base64').replace(/\W/g, '');
|
||||
}
|
||||
|
||||
if (typeof currentObj[key] === 'object' && currentObj[key] !== null) {
|
||||
stack.push(currentObj[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BUG: If file does'nt exist -> fail.
|
||||
// TODO: Check for SyntaxError on fileread and ask if the user wants to continue -> overwrite everything. This behavior is currently standard.
|
||||
|
||||
/*
|
||||
|
||||
**** Example ****
|
||||
|
||||
const default_config = {
|
||||
token: 'your-token-goes-here',
|
||||
clientId: '',
|
||||
devserverID: '',
|
||||
devmode: true
|
||||
};
|
||||
import ConfigHandlerNG from './assets/configHandlerNG.js';
|
||||
|
||||
// Create a new config instance.
|
||||
export const config = new ConfigHandler(__path + '/config.json', true, {
|
||||
test1: 't1',
|
||||
test2: 't2',
|
||||
test3: 'gen',
|
||||
test4: 't4',
|
||||
test5: 'gen',
|
||||
testObj: {
|
||||
local: {
|
||||
active: true,
|
||||
users: {
|
||||
user1: 'gen',
|
||||
user2: 'gen',
|
||||
user3: 'gen',
|
||||
user4: 'gen',
|
||||
|
||||
import configHandler from './assets/config.js';
|
||||
const config = new configHandler(__path + '/config.json', default_config);
|
||||
}
|
||||
},
|
||||
oidc: {
|
||||
active: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Base Config:');
|
||||
console.log(config.global);
|
||||
|
||||
console.log('Add some new key to config and call save_config.');
|
||||
console.log('Add some new key to config and call save_config().');
|
||||
config.global.NewKey = 'ThisIsANewKey!'
|
||||
config.save_config()
|
||||
|
||||
console.log('This will add a new key with value gen, but gen gets replaced with a random UUID when save_config() is called.');
|
||||
config.global.someSecret = 'gen'
|
||||
config.save_config() // global.someSecret is getting replaced with some random UUID since it was set to 'gen'.
|
||||
|
||||
console.log('Complete Config:');
|
||||
console.log(config.global);
|
||||
*/
|
||||
|
113
src/assets/helper.ts
Normal file
113
src/assets/helper.ts
Normal file
@ -0,0 +1,113 @@
|
||||
// @ts-nocheck
|
||||
import { formatAst, parsePrismaSchema } from '@loancrate/prisma-schema-parser';
|
||||
import * as fs from 'fs';
|
||||
import { log } from '../index.js';
|
||||
|
||||
/**
|
||||
* A helper function which returns every models' required, optional and relation fields
|
||||
*
|
||||
* @returns {{}} An object containing every model and their required, optional and relation fields
|
||||
*/
|
||||
function returnAllModelFieldData() {
|
||||
const ast = parsePrismaSchema(fs.readFileSync('./prisma/schema.prisma', { encoding: 'utf8' }));
|
||||
const modelData: Record<string, object> = {};
|
||||
|
||||
Object.keys(ast.declarations).forEach((key) => {
|
||||
if (ast.declarations[key].kind === 'model') {
|
||||
log.helper.debug('Found model: ', ast.declarations[key].name.value);
|
||||
|
||||
Object.keys(ast.declarations[key].members).forEach((key2) => {
|
||||
if (ast.declarations[key].members[key2].kind === 'field') {
|
||||
const currentField = ast.declarations[key].members[key2];
|
||||
switch (currentField.type.kind) {
|
||||
case 'optional':
|
||||
log.helper.debug('Found optional field:', currentField.name.value);
|
||||
modelData[ast.declarations[key].name.value].optional.push(currentField.name.value);
|
||||
break;
|
||||
case 'typeId':
|
||||
// Required fields are not always required for our purposes, fields with a default value are not required
|
||||
let isRequired = true;
|
||||
if (currentField.attributes.length > 0) {
|
||||
Object.keys(currentField.attributes).forEach((key3) => {
|
||||
if (currentField.attributes[key3].path != {}) {
|
||||
if (currentField.attributes[key3].path.value == 'default') {
|
||||
const defValue = currentField.attributes[key3].args[0].value;
|
||||
log.helper.debug('Found default field:', currentField.name.value, 'with value: ', defValue);
|
||||
modelData[ast.declarations[key].name.value].optional.push(currentField.name.value);
|
||||
isRequired = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (isRequired) {
|
||||
modelData[ast.declarations[key].name.value].required.push(currentField.name.value);
|
||||
log.helper.debug('Found required field: ', currentField.name.value);
|
||||
}
|
||||
break;
|
||||
case 'list':
|
||||
log.helper.debug('Found relation/list field:', currentField.name.value);
|
||||
modelData[ast.declarations[key].name.value].relation.push(currentField.name.value);
|
||||
break;
|
||||
default:
|
||||
log.helper.error('Unable to determine field type:', currentField.name.value, currentField.type.kind);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return modelData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for parsing a string into a prisma connect object
|
||||
*
|
||||
* @export
|
||||
* @param {string} data
|
||||
* @param {string} [relation_name='id']
|
||||
* @returns {undefined || object} undefined or prisma connect object
|
||||
*/
|
||||
export function parseIntRelation(data: string, relation_name: string = 'id', doNotDisconnect: boolean = false) {
|
||||
// This function is perfect. If data is not a valid number, return `undefined`
|
||||
// If it is a valid number return `{connect: {relation_name: yourNumber}}}`
|
||||
// This can be used by prisma to connect relations
|
||||
|
||||
// If the incoming data is null or empty, return a prisma disconnect object instead of a connect one
|
||||
if (data === null || data === '' || data === "undefined") {
|
||||
if (doNotDisconnect) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(`{
|
||||
"disconnect": true
|
||||
}`);
|
||||
}
|
||||
|
||||
return isNaN(parseInt(data)) ? undefined : JSON.parse(`{
|
||||
"connect": {
|
||||
"${relation_name}": ${parseInt(data)}
|
||||
}
|
||||
}`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}" }`);
|
||||
}
|
@ -4,9 +4,33 @@
|
||||
<div class="background text-center">
|
||||
<div class="row align-items-start">
|
||||
<div class="col-9"></div>
|
||||
<div class="col-3 sidePanel ps-4 text-black">
|
||||
<div class="col-3 sidePanel ps-4 pe-4 text-black">
|
||||
<h1>Log into AssetFlow</h1>
|
||||
<div class="alert alert-danger" role="alert" id="passwordAlarm">
|
||||
User does not exist or password is incorrect.
|
||||
</div>
|
||||
|
||||
<form action="/auth/login" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="userName" class="form-label">Username</label>
|
||||
<input name="username" type="text" class="form-control" id="userName" aria-describedby="userNameHelp" />
|
||||
<!-- <div id="userNameHelp" class="form-text">We'll never share your email with anyone else.</div> -->
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="userPassword" class="form-label">Password</label>
|
||||
<input name="password" type="password" class="form-control" id="userPassword" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// if url parameter ?failed is set, show the password alarm
|
||||
if (window.location.search.includes("failed")) {
|
||||
document.getElementById("passwordAlarm").style.display = "block";
|
||||
} else {
|
||||
document.getElementById("passwordAlarm").style.display = "none";
|
||||
}
|
||||
</script>
|
||||
<%~ E.includeFile("../partials/foot.eta.html") %>
|
||||
</div>
|
||||
|
@ -1,6 +1,19 @@
|
||||
<%~ E.includeFile("partials/head.eta.html", {"title": "Dashboard"}) %> <%~ E.includeFile("partials/controls.eta.html", {"active": "Dashboard"}) %>
|
||||
|
||||
<h1>Good evening, ${user}</h1>
|
||||
<h1 onclick="doTheConfetti()" class="user-select-none" id="greeting">Good evening, ${user}</h1>
|
||||
<script>
|
||||
// Handle greeting
|
||||
var today = new Date();
|
||||
var curHr = today.getHours();
|
||||
|
||||
if (curHr < 12) {
|
||||
document.getElementById("greeting").innerHTML = "Good morning";
|
||||
} else if (curHr < 18) {
|
||||
document.getElementById("greeting").innerHTML = "Good afternoon";
|
||||
} else {
|
||||
document.getElementById("greeting").innerHTML = "Good evening";
|
||||
}
|
||||
</script>
|
||||
<div class="container text-center">
|
||||
<div class="row">
|
||||
<div class="card col m-2">
|
||||
@ -24,8 +37,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- TODO: Center table content -->
|
||||
<h2>Recent items</h2>
|
||||
<div class="container">
|
||||
<table class="table">
|
||||
@ -40,18 +51,20 @@
|
||||
<tbody>
|
||||
<% it.recents.forEach(function(user){ %>
|
||||
<tr>
|
||||
<th scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= user.id %>"><%= user.SKU %></th>
|
||||
<th scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= user.id %>"><% if (user.SKU == null) { %>
|
||||
<i>No SKU assigned</i>
|
||||
<% } else { %> <%= user.SKU %> <% } %></th>
|
||||
<td><%= user.name %></td>
|
||||
<% if(user.status == "normal") { %>
|
||||
|
||||
<td><span class="badge text-bg-success"><%= user.status %></span></td>
|
||||
<% } else if(user.status == "stolen") { %>
|
||||
<td><span class="badge text-bg-danger"><%= user.status %></span></td>
|
||||
<% } else if(user.status == "lost") { %>
|
||||
<td><span class="badge text-bg-warning"><%= user.status %></span></td>
|
||||
<% } else if(user.status == "borrowed") { %>
|
||||
<td><span class="badge text-bg-info"><%= user.status %></span></td>
|
||||
<% } %>
|
||||
<td><span class="badge text-bg-success"><%= user.status %></span></td>
|
||||
<% } else if(user.status == "stolen") { %>
|
||||
<td><span class="badge text-bg-danger"><%= user.status %></span></td>
|
||||
<% } else if(user.status == "lost") { %>
|
||||
<td><span class="badge text-bg-warning"><%= user.status %></span></td>
|
||||
<% } else if(user.status == "borrowed") { %>
|
||||
<td><span class="badge text-bg-info"><%= user.status %></span></td>
|
||||
<% } %>
|
||||
<!--<td><a href="#" class="btn btn-primary">Edit</a></td>-->
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
@ -6,7 +6,7 @@
|
||||
<a class="btn btn-secondary" data-bs-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="collapseExample"> Get the error </a>
|
||||
</p>
|
||||
<div class="collapse" id="collapseExample">
|
||||
<div class="card card-body">
|
||||
<div class="card card-body text-start">
|
||||
<pre><code><%= it.error %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
22
src/frontend/itemInfo.eta.html
Normal file
22
src/frontend/itemInfo.eta.html
Normal file
@ -0,0 +1,22 @@
|
||||
<%~ E.includeFile("partials/head.eta.html", {"title": "Item Info"}) %> <%~ E.includeFile("partials/controls.eta.html", {"active": "ItemInfo"}) %> <%~ E.includeFile("./partials/deleteModal.eta.html") %>
|
||||
|
||||
|
||||
<h1><%= it.name %></h1>
|
||||
<div class="container">
|
||||
<p><strong>Comment:</strong> <%= it.comment %></p>
|
||||
<p><strong>Category:</strong> <% if (it.category == null) { %> <i>No category assigned</i> <% } else { %> <%= it.category.name %> <% } %></p>
|
||||
<p><strong>Amount:</strong> <%= it.amount %></p>
|
||||
<p><strong>SKU:</strong> <%= it.SKU %></p>
|
||||
<p><strong>Status: </strong><% if(it.status == "normal") { %>
|
||||
|
||||
<span class="badge text-bg-success"><%= it.status %></span>
|
||||
<% } else if(it.status == "stolen") { %>
|
||||
<span class="badge text-bg-danger"><%= it.status %></span>
|
||||
<% } else if(it.status == "lost") { %>
|
||||
<span class="badge text-bg-warning"><%= it.status %></span>
|
||||
<% } else if(it.status == "borrowed") { %>
|
||||
<span class="badge text-bg-info"><%= it.status %></span>
|
||||
<% } %></p>
|
||||
|
||||
</div>
|
||||
<%~ E.includeFile("partials/controlsFoot.eta.html") %> <%~ E.includeFile("partials/foot.eta.html") %>
|
@ -1,15 +1,106 @@
|
||||
<%~ E.includeFile("partials/head.eta.html", {"title": "Items"}) %> <%~ E.includeFile("partials/controls.eta.html", {"active": "Items"}) %>
|
||||
<%~ E.includeFile("partials/head.eta.html", {"title": "Items"}) %> <%~ E.includeFile("partials/controls.eta.html", {"active": "Items"}) %> <%~ E.includeFile("./partials/deleteModal.eta.html") %>
|
||||
|
||||
<div class="modal fade" id="itemModifyModal" tabindex="-1" aria-labelledby="itemModifyModal" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg ">
|
||||
<div class="modal-content modal-dialog-scrollable">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="itemModifyModalLabel">Edit a item</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form class="frontendForm" method="patch" data-target="/api/v1/items" id="ItemModalForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="itemModifyModalName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="itemModifyModalName" name="name" maxlength="128" required />
|
||||
<div id="itemModifyModalNameText" class="form-text">This name should be unqiue.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="itemModifyModalComment" class="form-label">Comment</label>
|
||||
<input type="text" class="form-control" id="itemModifyModalComment" maxlength="2048" name="comment" />
|
||||
<div id="itemModifyModalDescText" class="form-text">Optional</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="itemModifyModalStorageLocation" class="form-label">Select a storage location</label>
|
||||
<select class="form-select" id="itemModifyModalStorageLocation" name="storageLocationId">
|
||||
<option value=""><i>Do not assign a storage location</i></option>
|
||||
<% it.storeLocs.forEach(function(locs){ %>
|
||||
<option value="<%= locs.id %>"><%= locs.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
|
||||
<div id="itemModifyModalStorageLocationText" class="form-text">You have to create a storage location beforehand.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="itemModifyModalAmount" class="form-label">Amount</label>
|
||||
<input type="number" min="0" class="form-control" id="itemModifyModalAmount" name="amount" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="itemModifyModalSKU" class="form-label">SKU</label>
|
||||
<input type="text" class="form-control" id="itemModifyModalSKU" maxlength="64" name="sku" />
|
||||
<div id="itemModifyModalSKUText" class="form-text">Optional</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="itemModifyModalManuf" class="form-label">Manufacturer</label>
|
||||
<input type="text" class="form-control" id="itemModifyModalManuf" maxlength="190" name="manufacturer" />
|
||||
<div id="itemModifyModalSKUText" class="form-text">Optional</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="itemModifyModalCategory" class="form-label">Select a category</label>
|
||||
<select class="form-select" id="itemModifyModalCategory" name="category">
|
||||
<option value=""><i>Do not assign a category</i></option>
|
||||
<% it.categories.forEach(function(cat){ %>
|
||||
<option value="<%= cat.id %>"><%= cat.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
|
||||
<div id="storageLocationModalLocationText" class="form-text">You have to create a storage location beforehand.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="itemModifyModalStatus" class="form-label">Status</label>
|
||||
<select class="form-select" id="itemModifyModalStatus" name="status" required>
|
||||
<option value="normal" class="text-success">Normal</option>
|
||||
<option value="borrowed" class="text-info">Borrowed</option>
|
||||
<option value="stolen" class="text-danger">Stolen</option>
|
||||
<option value="lost" class="text-warning">Lost</option>
|
||||
</select>
|
||||
|
||||
<div id="storageLocationModalLocationText" class="form-text">You have to create a storage location beforehand.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="itemModifyModalContact" class="form-label">Contact Info</label>
|
||||
<select class="form-select" id="itemModifyModalContact" name="contactInfoId" onchange="handleSelector()">
|
||||
<option value=""><i>Do not assign contact info</i></option>
|
||||
<% it.contactInfo.forEach(function(address){ %>
|
||||
<option value="<%= address.id %>"><%= address.street %> <%= address.houseNumber %>, <%= address.city %> <%= address.country %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<input type="text" id="itemModifyModalId" name="id" hidden />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Center table content -->
|
||||
<h1>Items</h1>
|
||||
<div class="container">
|
||||
<table class="table align-middle">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a href="/settings/category/new" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#itemModifyModal" onclick="primeCreateNew()">Create new item</a>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table align-middle" id="itemList" data-sortable="true" data-search-highlight="true" data-pagination="true" data-page-size="25" data-remember-order="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">SKU</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
<th scope="col" data-field="SKU" class="sku" data-sortable="true">SKU</th>
|
||||
<th scope="col" data-field="name" data-sortable="true">Name</th>
|
||||
<th scope="col" data-field="comment" data-sortable="true" data-width="80">Comment</th>
|
||||
<th scope="col" data-field="status" data-sortable="true">Status</th>
|
||||
<th scope="col" data-field="actions" data-sortable="false" data-searchable="false" data-width="160">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<% if(it.items.length == 0) { %>
|
||||
@ -19,39 +110,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
<% } %>
|
||||
<tbody>
|
||||
<% it.items.forEach(function(user){ %>
|
||||
<tr>
|
||||
<td scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= user.id %>"><%= user.SKU %></td>
|
||||
<td><%= user.name %></td>
|
||||
<% if(user.status == "normal") { %>
|
||||
|
||||
<td><span class="badge text-bg-success"><%= user.status %></span></td>
|
||||
<% } else if(user.status == "stolen") { %>
|
||||
<td><span class="badge text-bg-danger"><%= user.status %></span></td>
|
||||
<% } else if(user.status == "lost") { %>
|
||||
<td><span class="badge text-bg-warning"><%= user.status %></span></td>
|
||||
<% } else if(user.status == "borrowed") { %>
|
||||
<td><span class="badge text-bg-info"><%= user.status %></span></td>
|
||||
<% } %>
|
||||
<td><a href="#" class="btn btn-primary">Edit</a></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<% if(it.maxPages > 1) { %>
|
||||
<nav aria-label="Page selector">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <%= it.currentPage-1 < 1 ? 'disabled' : ''%>"><a class="page-link" href="?page=<%= it.currentPage - 1 %>">Previous</a></li>
|
||||
<% for (var i = 1; i <= it.maxPages; i++) { %>
|
||||
<li class="page-item <%= it.currentPage == i ? 'active' : ''%> "><a class="page-link" href="?page=<%= i %>"><%= i %></a></li>
|
||||
<% } %>
|
||||
|
||||
<li class="page-item <%= it.currentPage+1 > it.maxPages ? 'disabled' : ''%>"><a class="page-link" href="?page=<%= it.currentPage + 1 %>">Next</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<script src="/js/editItems.js"></script>
|
||||
<script src="/js/itemPageHandler.js"></script>
|
||||
<%~ E.includeFile("partials/controlsFoot.eta.html") %> <%~ E.includeFile("partials/foot.eta.html") %>
|
||||
|
@ -1,6 +1,4 @@
|
||||
<%~ E.includeFile("../partials/head.eta.html", {"title": "Settings - Category"}) %> <%~ E.includeFile("../partials/controls.eta.html", {"active": "SETT_CAT"}) %>
|
||||
|
||||
<%~ E.includeFile("../partials/deleteModal.eta.html") %>
|
||||
<%~ E.includeFile("../partials/head.eta.html", {"title": "Settings - Category"}) %> <%~ E.includeFile("../partials/controls.eta.html", {"active": "SETT_CAT"}) %> <%~ E.includeFile("../partials/deleteModal.eta.html") %>
|
||||
|
||||
<h1>Categories</h1>
|
||||
<div class="container">
|
||||
@ -11,9 +9,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal fade" id="editCategoryModal" tabindex="-1" aria-labelledby="editCategoryModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="editCategoryModalLabel">Edit a category</h1>
|
||||
@ -23,12 +20,12 @@
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="editCategoryModalName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="editCategoryModalName" name="name" required />
|
||||
<input type="text" class="form-control" id="editCategoryModalName" maxlength="128" name="name" required />
|
||||
<div id="editCategoryModalNameText" class="form-text">This name should be unqiue.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editCategoryModalDescription" class="form-label">Description</label>
|
||||
<input type="text" class="form-control" id="editCategoryModalDescription" name="description" />
|
||||
<input type="text" class="form-control" id="editCategoryModalDescription" maxlength="2048" name="description" />
|
||||
<div id="editCategoryModalDescText" class="form-text">Optional</div>
|
||||
</div>
|
||||
<input type="text" id="editCategoryModalId" name="id" hidden />
|
||||
@ -43,27 +40,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Table with all categories -->
|
||||
<table class="table align-middle">
|
||||
<table class="table align-middle" id="itemList" data-sortable="true" data-search-highlight="true" data-pagination="true" data-page-size="25" data-remember-order="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- <th scope="col">#</th> -->
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">Action</th>
|
||||
<th scope="col" data-field="name" data-sortable="true" data-width="300">Name</th>
|
||||
<th scope="col" data-field="description" data-sortable="true">Description</th>
|
||||
<th scope="col" data-field="actions" data-sortable="false" data-searchable="false" data-width="160">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% it.items.forEach(function(user){ %>
|
||||
<tr id="listEntry-<%= user.id %>">
|
||||
<td scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= user.id %>"><%= user.name %></td>
|
||||
<td><%= user.description %></td>
|
||||
<td>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#editCategoryModal" onclick="primeEdit(); getDataForEdit('<%= user.name %>')"><i class="bi bi-pencil"></i></button>
|
||||
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('<%= user.name %>','categories','Category', 'name')" data-bs-toggle="modal" data-bs-target="#staticBackdrop"><i class="bi bi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
<script src="/js/editCategory.js"></script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<%~ E.includeFile("../partials/head.eta.html", {"title": "Settings"}) %> <%~ E.includeFile("../partials/controls.eta.html", {"active": "SETT"}) %>
|
||||
|
||||
<h1>Manage your AssetFlow instance</h1>
|
||||
<div class="alert alert-success" role="alert">A new version is available. <a href="#" class="alert-link">Click here to update</a></div>
|
||||
<div class="alert alert-success" role="alert" id="updateNotifier">A new version is available. <a href="https://git.project-name-here.de/Project-Name-Here/assetflow/releases" class="alert-link">Click here to update</a></div>
|
||||
<div class="container text-center">
|
||||
<div class="row">
|
||||
<a class="card col m-2" href="/manage/categories">
|
||||
@ -30,5 +30,16 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$.getJSON("/api/v1/version", function (data) {
|
||||
if (data.updateAvailable) {
|
||||
$("#updateNotifier").show();
|
||||
// $("#updateNotifier").find(".alert-link").attr("href", data.url);
|
||||
}else {
|
||||
$("#updateNotifier").hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<%~ E.includeFile("../partials/controlsFoot.eta.html") %> <%~ E.includeFile("../partials/foot.eta.html") %>
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="storageLocationModal" tabindex="-1" aria-labelledby="storageLocationModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="storageLocationModalTitle">Edit or create a storage location</h1>
|
||||
@ -13,7 +13,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="storageLocationModalName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="storageLocationModalName" name="name" required />
|
||||
<input type="text" class="form-control" id="storageLocationModalName" name="name" maxlength="128" required />
|
||||
<div id="storageLocationModalNameText" class="form-text">This name should be unqiue.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@ -48,7 +48,7 @@
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="storageUnitModal" tabindex="-1" aria-labelledby="storageUnitModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="storageUnitModalLabel">Edit or create a storage unit</h1>
|
||||
@ -150,45 +150,20 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table align-middle">
|
||||
<table class="table align-middle" id="itemList" data-sortable="true" data-search-highlight="true" data-pagination="true" data-page-size="25" data-remember-order="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Storage Unit</th>
|
||||
<th scope="col">Actions</th>
|
||||
<th scope="col" data-field="name" data-sortable="true">Name</th>
|
||||
<th scope="col" data-field="storageUnit" data-sortable="false">Storage Unit</th>
|
||||
<th scope="col" data-field="actions" data-sortable="false" data-searchable="false" data-width="160">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% it.storLocs.forEach(function(locations){ %>
|
||||
<tr id="listEntry-<%= locations.id %>">
|
||||
<td scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= locations.id %>"><%= locations.name %></td>
|
||||
<td>
|
||||
<% if (locations.storageUnit == null) { %>
|
||||
<i>No storage unit connected</i>
|
||||
<% } else { %>
|
||||
<%= locations.storageUnit.name %>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageLocationModal" onclick="primeEdit(); getDataForEditLoc('<%= locations.id %>')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('<%= locations.id %>','storageLocations','Storage Location')" data-bs-toggle="modal" data-bs-target="#staticBackdrop"><i class="bi bi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Storage Unit -->
|
||||
<div class="tab-pane fade" id="storage-unit-tab-pane" role="tabpanel" aria-labelledby="storage-unit-tab-pane" tabindex="0">
|
||||
<br />
|
||||
@ -199,27 +174,15 @@
|
||||
<a class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageUnitModal" onclick="primeCreateNew()"><i class="bi bi-building-add"></i> Create new unit</a>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table align-middle">
|
||||
<table class="table align-middle" id="itemListUnit" data-sortable="true" data-search-highlight="true" data-pagination="true" data-page-size="25" data-remember-order="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Address</th>
|
||||
<th scope="col">Actions</th>
|
||||
<th scope="col" data-field="name" data-sortable="true">Name</th>
|
||||
<th scope="col "data-field="address" data-sortable="false">Address</th>
|
||||
<th scope="col" data-field="actions" data-searchable="false" data-width="160">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% it.storUnits.forEach(function(units){ %>
|
||||
<tr id="listEntry-<%= units.id %>">
|
||||
<td scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= units.id %>"><%= units.name %></td>
|
||||
<td><%= units.contactInfo.street %> <%= units.contactInfo.houseNumber %>, <%= units.contactInfo.city %> <%= units.contactInfo.country %></td>
|
||||
<td>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageUnitModal" onclick="primeEdit(); getDataForEdit('<%= units.id %>')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('<%= units.id %>', 'storageUnits', 'Storage Unit')" data-bs-toggle="modal" data-bs-target="#staticBackdrop"><i class="bi bi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -1,45 +1,52 @@
|
||||
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 test-white-50" onclick="doTheConfetti()">AssetFlow</a>
|
||||
<script>
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function doTheConfetti() {
|
||||
confetti({
|
||||
angle: randomInRange(40, 150),
|
||||
spread: randomInRange(50, 100),
|
||||
particleCount: randomInRange(50, 150),
|
||||
origin: { y: 0.6 }
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<button
|
||||
class="navbar-toggler position-absolute d-md-none collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#sidebarMenu"
|
||||
aria-controls="sidebarMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<input class="form-control form-control-dark w-100 bg-secondary" type="text" placeholder="Search" aria-label="Search" id="SearchBox" />
|
||||
<div class="autocomplete-items bg-secondary w-75 border-primary me-5 p-2" id="autocomplete-items" style="left: 16.7%"></div>
|
||||
|
||||
<div class="navbar-nav">
|
||||
<div class="nav-item text-nowrap">
|
||||
<a class="nav-link px-3" id="logoutButton">Sign out</a>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary sticky-top navShadow" style="z-index: 999">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand user-select-none ms-2" style="cursor: default" href="/">
|
||||
<img alt="AssetFlow Logo" draggable="false" class="me-2 headLogo" src="/logo/Design_icon.svg"/> AssetFlow</a>
|
||||
<button
|
||||
class="navbar-toggler position-absolute d-md-none collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#sidebarMenu"
|
||||
aria-controls="sidebarMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0"></ul>
|
||||
<form class="d-flex" role="search">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#search_modal"><i class="bi bi-search"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</nav>
|
||||
<div class="modal" id="search_modal">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="modalSearchBar">
|
||||
<div class="input-group mb-3">
|
||||
<form id="searchForm">
|
||||
<input type="text" id="SearchBoxInput" class="form-control focus" placeholder="Start typing to search..." aria-label="Search" autocomplete="off">
|
||||
</form>
|
||||
</div>
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="autocompletBody">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" id="masterToast" style="z-index: 2000">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">Hello, world! This is a toast message.</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toastMainController">
|
||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" id="generalToast" style="z-index: 2000">
|
||||
<div class="d-flex">
|
||||
@ -49,40 +56,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">Notification</strong>
|
||||
<small>Just now</small>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body" id="toastText">The button you just pressed is very useless.</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let texti = 0;
|
||||
alltexts = ['Nope, still useless', 'Stop pressing me!', 'There are NO USERS!', 'Please stop.', 'PLEASE!', 'Do you want an achivment or what?', 'This is not a game, please stop.', 'This is not the stanley parable.'];
|
||||
const toastLiveExample = document.getElementById('liveToast');
|
||||
const logoutButton = document.getElementById('logoutButton');
|
||||
logoutButton.addEventListener('click', () => {
|
||||
toastFunction();
|
||||
texti++;
|
||||
if (texti >= alltexts.length) texti = 0;
|
||||
});
|
||||
|
||||
function toastFunction() {
|
||||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
||||
toastBootstrap.show();
|
||||
setTimeout(function () {
|
||||
toastBootstrap.hide();
|
||||
document.getElementById('toastText').innerHTML = alltexts[texti];
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block sidebar collapse">
|
||||
<nav id="sidebarMenu" class="col-md-2 col-lg-2 d-md-block sidebar collapse">
|
||||
<div class="position-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
@ -105,15 +81,37 @@
|
||||
</li> -->
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<a href="/projects/" class="nav-link"
|
||||
>Projects<span class="badge rounded-pill bg-primary" >
|
||||
Alpha
|
||||
<span class="visually-hidden">Alpha feature</span>
|
||||
</span>
|
||||
</a>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= it.active == 'PROJ_HOME' ? 'active' : ''%>" href="/projects/"><i class="bi bi-kanban"></i> Manage Projects </a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= it.active == 'PROJ_LIST' ? 'active' : ''%>" href="/projects/lists"><i class="bi bi-card-checklist"></i> Packaging Lists </a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= it.active == 'PROJ_PEPS' ? 'active' : ''%>" href="/projects/people"><i class="bi bi-people-fill"></i> People </a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<a href="/manage/" class="nav-link"
|
||||
>Settings
|
||||
<span class="badge rounded-pill bg-danger invisible">
|
||||
2
|
||||
<span class="badge rounded-pill bg-danger invisible" id="notifcationInfo">
|
||||
1
|
||||
<span class="visually-hidden">changes or updates</span>
|
||||
</span>
|
||||
</a>
|
||||
</h6>
|
||||
|
||||
|
||||
<ul class="nav flex-column mb-2">
|
||||
<a class="nav-link <%= it.active == 'SETT_STORE' ? 'active' : ''%>" href="/manage/storages"
|
||||
@ -126,7 +124,7 @@
|
||||
data-bs-target="#collapseSettingsStorages"
|
||||
aria-expanded="<%= it.active == 'SETT_STORE' ? 'true' : 'false'%>"
|
||||
aria-controls="collapseSettingsStorages">
|
||||
<i class="bi bi-caret-left-fill dropdownIndicator"></i>
|
||||
<i class="bi bi-caret-left-fill dropdownIndicator" data-ref-target="#collapseSettingsStorages"></i>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@ -150,7 +148,6 @@
|
||||
aria-controls="collapseSettingsImport">
|
||||
<i class="bi bi-box-seam"></i> Import
|
||||
<i class="bi bi-caret-left-fill dropdownIndicator" data-ref-target="#collapseSettingsImport"></i>
|
||||
<!-- TODO: This little triangle does not care if it is collapsed or not. But it should so -->
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@ -159,20 +156,18 @@
|
||||
<a class="nav-link ms-4 <%= it.active == 'SETT_IMPORT_JSON' ? 'active' : ''%>" href="/manage/import/json"> <i class="bi bi-filetype-json"></i> JSON Import</a>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<!-- Align the mode picker at the bottom of the navbar -->
|
||||
|
||||
<ul class="nav flex-column mb-2 position-absolute bottom-0 align-items-center w-100">
|
||||
<ul class="nav flex-column mb-5 position-absolute bottom-0 align-items-center w-100">
|
||||
<div class="input-group mb-3 justify-content-center w-100">
|
||||
|
||||
<label class="btn btn-secondary" for="mode_light"><i class="bi bi-brightness-high"></i></label>
|
||||
<input type="radio" class="btn-check" name="options" id="mode_light" autocomplete="off" >
|
||||
|
||||
<input type="radio" class="btn-check" name="options" id="mode_auto" autocomplete="off" checked>
|
||||
<input type="radio" class="btn-check" name="options" id="mode_light" autocomplete="off" />
|
||||
|
||||
<input type="radio" class="btn-check" name="options" id="mode_auto" autocomplete="off" checked />
|
||||
<label class="btn btn-secondary" for="mode_auto"><i class="bi bi-magic"></i></label>
|
||||
|
||||
<input type="radio" class="btn-check" name="options" id="mode_dark" autocomplete="off">
|
||||
|
||||
<input type="radio" class="btn-check" name="options" id="mode_dark" autocomplete="off" />
|
||||
<label class="btn btn-secondary" for="mode_dark"><i class="bi bi-moon"></i></label>
|
||||
</div>
|
||||
<script>
|
||||
@ -189,20 +184,43 @@
|
||||
}
|
||||
modeLight.addEventListener('click', () => {
|
||||
localStorage.setItem('bs.theme', 'light');
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
updateColorMode();
|
||||
//document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
});
|
||||
modeAuto.addEventListener('click', () => {
|
||||
localStorage.setItem('bs.theme', 'auto');
|
||||
document.documentElement.setAttribute('data-bs-theme', 'auto');
|
||||
updateColorMode();
|
||||
//document.documentElement.setAttribute('data-bs-theme', 'auto');
|
||||
});
|
||||
modeDark.addEventListener('click', () => {
|
||||
localStorage.setItem('bs.theme', 'dark');
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
updateColorMode();
|
||||
//document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
});
|
||||
</script>
|
||||
</ul>
|
||||
|
||||
|
||||
</ul>
|
||||
<div onclick="toggleAutoReload();" class="text-secondary versionInfo nav flex-column position-absolute bottom-0 align-items-center w-100" id="versionInfo">AssetFlow Alpha <i>No version info</i> </div>
|
||||
<script>
|
||||
// Request /api/v1/version
|
||||
// If the response is 200, set the commit hash
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "/api/v1/version",
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
$('#versionInfo').text(`AssetFlow Alpha ${data.version} ${data.commit}`);
|
||||
if(data.updateAvailable ){
|
||||
$('#notifcationInfo').show();
|
||||
} else {
|
||||
$('#notifcationInfo').hide();
|
||||
}
|
||||
},
|
||||
error: function (data) {
|
||||
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Unable to load version information', "text-bg-danger", 3000, false)
|
||||
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</nav>
|
||||
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4" style="min-height: 100%">
|
||||
|
@ -4,4 +4,19 @@
|
||||
</div>
|
||||
<script src="/js/searchBox.js"></script>
|
||||
<script src="/js/handleSidebarTriangles.js"></script>
|
||||
<script src="/js/formHandler.js"></script>
|
||||
<script src="/js/formHandler.js"></script>
|
||||
<script>
|
||||
function activateTooltips(){
|
||||
// Enable all bootstrap tooltips.
|
||||
// https://getbootstrap.com/docs/5.3/components/tooltips/#enable-tooltips
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
}
|
||||
function activatePopovers(){
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
|
||||
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
|
||||
}
|
||||
|
||||
activatePopovers();
|
||||
activateTooltips();
|
||||
</script>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="staticBackdropLabel">Do you really want to delete <strong id="deleteNamePlaceholder">Placeholder</strong>?</h1>
|
||||
<h1 class="modal-title fs-5" id="staticBackdropLabel">Do you really want to delete <strong id="deleteNamePlaceholder"><span class="placeholder col-4"></span></strong>?</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">This will permanently delete the category and all its associated data.<br />Items will be kept but will be unassigned from this category.</div>
|
||||
|
@ -1,8 +1,2 @@
|
||||
<script>
|
||||
// Enable all bootstrap tooltips.
|
||||
// https://getbootstrap.com/docs/5.3/components/tooltips/#enable-tooltips
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -8,41 +8,20 @@
|
||||
<title>AssetFlow - <%= it.title %></title>
|
||||
<meta name="author" content="[Project-Name-Here]" />
|
||||
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/logo/Design_icon.svg" type="image/svg+xml" />
|
||||
|
||||
<script src="/js/normalizeToast.js"></script>
|
||||
<script src="/js/handleColorMode.js"></script>
|
||||
<script src="/static/jquery/dist/jquery.min.js"></script>
|
||||
<script src="/js/toastHandler.js"></script>
|
||||
<script src="/js/confettiHeader.js"></script>
|
||||
<link href="/static/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="/static/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet" />
|
||||
<link href="/css/dashboard.css" rel="stylesheet" />
|
||||
<script src="/static/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/@popperjs/core/dist/umd/popper.min.js"></script>
|
||||
<script src="/static/tsparticles-confetti/tsparticles.confetti.bundle.min.js"></script>
|
||||
<script>
|
||||
// Listen for changes in the prefers-color-scheme media query and update the "data-bs-theme" attribute on the <html> element.
|
||||
(function () {
|
||||
const currentTheme = localStorage.getItem('bs.theme') ?? 'auto';
|
||||
const isDark = currentTheme === 'dark';
|
||||
const isLight = currentTheme === 'light';
|
||||
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
|
||||
if (currentTheme === 'auto') {
|
||||
if (prefersDark) {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
} else if (prefersLight) {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
}
|
||||
} else if (isDark) {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
} else if (isLight) {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/static/bootstrap-table/dist/bootstrap-table.min.css">
|
||||
<script src="/static/bootstrap-table/dist/bootstrap-table.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- The body and html tag need to be left open! -->
|
||||
|
49
src/frontend/projects/dashboard.eta.html
Normal file
49
src/frontend/projects/dashboard.eta.html
Normal file
@ -0,0 +1,49 @@
|
||||
<%~ E.includeFile("../partials/head.eta.html", {"title": "Projects"}) %> <%~ E.includeFile("../partials/controls.eta.html", {"active": "PROJ_HOME"}) %>
|
||||
|
||||
<h1>Projectmanager</h1>
|
||||
<div class="container text-center">
|
||||
<div class="row">
|
||||
<a class="card col m-2" href="/manage/categories">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title"><i class="bi bi-tag"></i></h1>
|
||||
<p class="card-text">Manage categories</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card col m-2" href="/manage/storages">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title"><i class="bi bi-box-seam"></i></h1>
|
||||
<p class="card-text">Manage storages</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card col m-2" href="/manage/import/csv">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title"><i class="bi bi-filetype-csv"></i></h1>
|
||||
<p class="card-text">Import data via CSV</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card col m-2" href="/manage/import/json">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title"><i class="bi bi-filetype-json"></i></h1>
|
||||
<p class="card-text">Import data via JSON</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Recent projects</h2>
|
||||
<div class="container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Status</th>
|
||||
<!--<th scope="col">Actions</th>-->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<%~ E.includeFile("../partials/controlsFoot.eta.html") %> <%~ E.includeFile("../partials/foot.eta.html") %>
|
60
src/index.ts
60
src/index.ts
@ -5,6 +5,9 @@ import fileUpload from 'express-fileupload';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as eta from 'eta';
|
||||
import bodyParser from 'body-parser';
|
||||
import session from 'express-session';
|
||||
import passport from 'passport';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Sentry
|
||||
import * as Sentry from '@sentry/node';
|
||||
@ -20,25 +23,48 @@ const logger_settings = {
|
||||
logLevel: 'info',
|
||||
scope: 'Core',
|
||||
stream: process.stdout,
|
||||
displayFilename: true
|
||||
displayFilename: false
|
||||
};
|
||||
|
||||
const coreLogger = new Signale(logger_settings);
|
||||
export const log = {
|
||||
core: coreLogger,
|
||||
db: coreLogger.scope('DB'),
|
||||
web: coreLogger.scope('WEB')
|
||||
web: coreLogger.scope('WEB'),
|
||||
auth: coreLogger.scope('AUTH'),
|
||||
helper: coreLogger.scope('HELPER')
|
||||
};
|
||||
|
||||
// Create a new config instance.
|
||||
export const config = new ConfigHandler(__path + '/config.json', {
|
||||
export const config = new ConfigHandler(__path + '/config.json', true, {
|
||||
db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE',
|
||||
http_listen_address: '127.0.0.1',
|
||||
http_port: 3000,
|
||||
sentry_dsn: 'https://ID@sentry.example.com/PROJECTID',
|
||||
debug: false
|
||||
debug: false,
|
||||
auth: {
|
||||
cookie_secret: 'gen',
|
||||
cookie_secure: true,
|
||||
local: {
|
||||
active: true,
|
||||
users: {}
|
||||
},
|
||||
oidc: {
|
||||
active: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If no local User exists, create the default with a generated password
|
||||
if (_.isEqual(config.global.auth.local.users, {})) {
|
||||
config.global.auth.local.users = {
|
||||
'flowAdmin': 'gen',
|
||||
};
|
||||
config.save_config();
|
||||
}
|
||||
|
||||
|
||||
// TODO: Add errorhandling with some sort of message.
|
||||
export const prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
@ -66,6 +92,17 @@ Sentry.init({
|
||||
environment: config.global.debug ? 'development' : 'production'
|
||||
});
|
||||
|
||||
// TODO: Version check need to be rewritten.
|
||||
app.locals.versionRevLong = require('child_process').execSync('git rev-parse HEAD').toString().trim();
|
||||
app.locals.versionRev = require('child_process').execSync('git rev-parse --short HEAD').toString().trim();
|
||||
app.locals.versionRevLatest = require('child_process').execSync('git ls-remote --refs -q').toString().trim().split('\t')[0];
|
||||
|
||||
if (app.locals.versionRevLong === app.locals.versionRevLatest) {
|
||||
log.core.info(`Running Latest Version (${app.locals.versionRevLong})`);
|
||||
} else {
|
||||
log.core.info(`Running Version: ${app.locals.versionRevLong} (Latest: ${app.locals.versionRevLatest})`);
|
||||
}
|
||||
|
||||
// RequestHandler creates a separate execution context using domains, so that every
|
||||
// transaction/span/breadcrumb is attached to its own Hub instance
|
||||
app.use(Sentry.Handlers.requestHandler());
|
||||
@ -81,14 +118,23 @@ app.use(bodyParser.urlencoded({ extended: false }));
|
||||
// Using bodyParser to parse JSON bodies into JS objects
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// Session store
|
||||
// TODO: Move secret to config -> Autogenerate.
|
||||
app.use(
|
||||
session({
|
||||
secret: config.global.auth.cookie_secret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: { secure: config.global.auth.cookie_secure }
|
||||
})
|
||||
);
|
||||
app.use(passport.authenticate('session'));
|
||||
|
||||
app.use(fileUpload());
|
||||
app.use(express.static(__path + '/static'));
|
||||
|
||||
app.use(routes);
|
||||
|
||||
// The error handler must be before any other error middleware and after all controllers
|
||||
app.use(Sentry.Handlers.errorHandler());
|
||||
|
||||
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}`);
|
||||
});
|
||||
|
21
src/middleware/auth.mw.ts
Normal file
21
src/middleware/auth.mw.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export function checkAuthentication(req: any, res: any, next: Function) {
|
||||
if (req.isAuthenticated()) {
|
||||
//req.isAuthenticated() will return true if user is logged in
|
||||
next();
|
||||
} else {
|
||||
res.redirect('/auth/login');
|
||||
}
|
||||
}
|
||||
|
||||
// const checkIsInRole = (...roles) => (req, res, next) => {
|
||||
// if (!req.user) {
|
||||
// return res.redirect('/login')
|
||||
// }
|
||||
|
||||
// const hasRole = roles.find(role => req.user.role === role)
|
||||
// if (!hasRole) {
|
||||
// return res.redirect('/login')
|
||||
// }
|
||||
|
||||
// return next()
|
||||
// }
|
@ -1,38 +1,104 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma, __path, log } from '../../../index.js';
|
||||
import { parseIntOrUndefined, parseDynamicSortBy } from '../../../assets/helper.js';
|
||||
|
||||
// Get category.
|
||||
function get(req: Request, res: Response) {
|
||||
// Get category
|
||||
async function get(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.query.name) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
return;
|
||||
if (req.query.sort === undefined) {
|
||||
req.query.sort = 'id';
|
||||
}
|
||||
if (req.query.order === undefined) {
|
||||
req.query.order = 'asc';
|
||||
}
|
||||
if (req.query.search === undefined) {
|
||||
req.query.search = '';
|
||||
}
|
||||
|
||||
prisma.itemCategory
|
||||
.findUnique({
|
||||
if (req.query.name) {
|
||||
prisma.itemCategory
|
||||
.findUnique({
|
||||
where: {
|
||||
name: req.query.name.toString()
|
||||
}
|
||||
})
|
||||
.then((item) => {
|
||||
if (item) {
|
||||
res.status(200).json(item);
|
||||
} else {
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Category does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
} else {
|
||||
// Get all items
|
||||
const itemCountNotFiltered = await prisma.itemCategory.count({});
|
||||
|
||||
// Get all items (filtered)
|
||||
const itemCountFiltered = await prisma.itemCategory.count({
|
||||
where: {
|
||||
name: req.query.name.toString()
|
||||
}
|
||||
})
|
||||
.then((item) => {
|
||||
if (item) {
|
||||
res.status(200).json(JSON.stringify(item));
|
||||
} else {
|
||||
res.status(410).json({ error: 'Category does not exist.' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
{
|
||||
description: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
|
||||
});
|
||||
|
||||
prisma.itemCategory
|
||||
.findMany({
|
||||
take: parseIntOrUndefined(req.query.limit),
|
||||
skip: parseIntOrUndefined(req.query.offset),
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()),
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
{
|
||||
description: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
})
|
||||
.then((items) => {
|
||||
if (items) {
|
||||
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
|
||||
} else {
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create category.
|
||||
function post(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.name) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -48,17 +114,21 @@ function post(req: Request, res: Response) {
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'created', id: data.id });
|
||||
res.status(201).json({ status: 'CREATED', message: 'Successfully created category', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ error: 'Category already exists.' });
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Category already exists' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -67,7 +137,7 @@ function post(req: Request, res: Response) {
|
||||
async function patch(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id || !req.body.name) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -80,12 +150,12 @@ async function patch(req: Request, res: Response) {
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(410).json({ error: 'Category does not exist.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Category does not exist' });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
prisma.itemCategory
|
||||
@ -96,20 +166,27 @@ async function patch(req: Request, res: Response) {
|
||||
data: {
|
||||
name: req.body.name,
|
||||
description: req.body.description
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
res.status(201).json({ status: 'updated' });
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated category', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ error: 'Category already exists.' });
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Category already exists' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -118,7 +195,7 @@ async function patch(req: Request, res: Response) {
|
||||
async function del(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -131,12 +208,12 @@ async function del(req: Request, res: Response) {
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(410).json({ error: 'Category does not exist.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Category does not exist' });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
prisma.itemCategory
|
||||
@ -146,11 +223,11 @@ async function del(req: Request, res: Response) {
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
res.status(200).json({ status: 'deleted' });
|
||||
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted category' });
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
|
||||
|
244
src/routes/api/v1/contactInfo.ts
Normal file
244
src/routes/api/v1/contactInfo.ts
Normal file
@ -0,0 +1,244 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma, __path, log } from '../../../index.js';
|
||||
import { parseIntOrUndefined, parseDynamicSortBy } from '../../../assets/helper.js';
|
||||
|
||||
// Get category
|
||||
async function get(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (req.query.sort === undefined) {
|
||||
req.query.sort = 'id';
|
||||
}
|
||||
if (req.query.order === undefined) {
|
||||
req.query.order = 'asc';
|
||||
}
|
||||
if (req.query.search === undefined) {
|
||||
req.query.search = '';
|
||||
}
|
||||
|
||||
if (req.query.id) {
|
||||
prisma.contactInfo
|
||||
.findUnique({
|
||||
where: {
|
||||
id: parseInt(req.query.id.toString())
|
||||
}
|
||||
})
|
||||
.then((item) => {
|
||||
if (item) {
|
||||
res.status(200).json(item);
|
||||
} else {
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'ContactInfo does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
} else {
|
||||
// Get all items
|
||||
const itemCountNotFiltered = await prisma.contactInfo.count({});
|
||||
|
||||
// Get all items (filtered)
|
||||
const itemCountFiltered = await prisma.contactInfo.count({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
{
|
||||
street: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
|
||||
});
|
||||
|
||||
prisma.contactInfo
|
||||
.findMany({
|
||||
take: parseIntOrUndefined(req.query.limit),
|
||||
skip: parseIntOrUndefined(req.query.offset),
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()),
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
{
|
||||
street: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
})
|
||||
.then((items) => {
|
||||
if (items) {
|
||||
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
|
||||
} else {
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'ContactInfo does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create category.
|
||||
function post(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.street || !req.body.houseNumber || !req.body.zipCode || !req.body.city || !req.body.country) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Save data.
|
||||
prisma.contactInfo
|
||||
.create({
|
||||
data: {
|
||||
name: req.body.name,
|
||||
lastName: req.body.lastName,
|
||||
street: req.body.street,
|
||||
houseNumber: req.body.houseNumber,
|
||||
zipCode: req.body.zipCode,
|
||||
city: req.body.city,
|
||||
country: req.body.country,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'CREATED', message: 'Successfully created contactInfo', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'ContactInfo already exists' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update category.
|
||||
async function patch(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id || !req.body.name) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the category id exists. If not return 410 Gone.
|
||||
try {
|
||||
const result = await prisma.contactInfo.findUnique({
|
||||
where: {
|
||||
id: parseInt(req.body.id)
|
||||
}
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Category does not exist' });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
prisma.contactInfo
|
||||
.update({
|
||||
where: {
|
||||
id: parseInt(req.body.id)
|
||||
},
|
||||
data: {
|
||||
name: req.body.name,
|
||||
lastName: req.body.lastName,
|
||||
street: req.body.street,
|
||||
houseNumber: req.body.houseNumber,
|
||||
zipCode: req.body.zipCode,
|
||||
city: req.body.city,
|
||||
country: req.body.country,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated category', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Category already exists' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete category
|
||||
async function del(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Does the id exist? If not return 410 Gone.
|
||||
try {
|
||||
const result = await prisma.contactInfo.findUnique({
|
||||
where: {
|
||||
id: parseInt(req.body.id)
|
||||
}
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'ContactInfo does not exist' });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
prisma.contactInfo
|
||||
.delete({
|
||||
where: {
|
||||
id: parseInt(req.body.id)
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted contactInfo' });
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
|
||||
export default { get, post, patch, del };
|
@ -1,19 +1,39 @@
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
|
||||
// Route imports
|
||||
import testRoute from './test.js';
|
||||
import itemRoute from './items.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';
|
||||
|
||||
// Router base is '/api/v1'
|
||||
const Router = express.Router({ strict: false });
|
||||
|
||||
// All empty strings are null values.
|
||||
Router.use('*', function (req, res, next) {
|
||||
for (let key in req.body) {
|
||||
if (req.body[key] === '') {
|
||||
req.body[key] = null;
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
Router.route('/items').get(itemRoute.get).post(itemRoute.post).patch(itemRoute.patch).delete(itemRoute.del);
|
||||
Router.route('/categories').get(categoryRoute.get).post(categoryRoute.post).patch(categoryRoute.patch).delete(categoryRoute.del);
|
||||
// TODO: Migrate routes to lowercase.
|
||||
Router.route('/storageUnits').get(storageUnitRoute.get).post(storageUnitRoute.post).patch(storageUnitRoute.patch).delete(storageUnitRoute.del);
|
||||
Router.route('/storageLocations').get(storageLocationRoute.get).post(storageLocationRoute.post).patch(storageLocationRoute.patch).delete(storageLocationRoute.del);
|
||||
Router.route('/contactInfo').get(contactInfo.get).post(contactInfo.post).patch(contactInfo.patch).delete(contactInfo.del);
|
||||
|
||||
Router.route('/version').get(versionRoute.get);
|
||||
Router.use('/search', search_routes);
|
||||
|
||||
Router.route('/test').get(testRoute.get);
|
||||
|
||||
|
313
src/routes/api/v1/items.ts
Normal file
313
src/routes/api/v1/items.ts
Normal file
@ -0,0 +1,313 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma, __path, log } from '../../../index.js';
|
||||
import { itemStatus } from '@prisma/client';
|
||||
import { parseIntRelation, parseIntOrUndefined, parseDynamicSortBy } from '../../../assets/helper.js';
|
||||
// Get item.
|
||||
async function get(req: Request, res: Response) {
|
||||
// Set sane defaults if undefined.
|
||||
if (req.query.sort === undefined) {
|
||||
req.query.sort = 'id';
|
||||
}
|
||||
if (req.query.order === undefined) {
|
||||
req.query.order = 'asc';
|
||||
}
|
||||
if (req.query.search === undefined) {
|
||||
req.query.search = '';
|
||||
}
|
||||
|
||||
if (req.query.id) {
|
||||
// Check if number is a valid integer
|
||||
if (!Number.isInteger(parseInt(req.query.id.toString()))) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
prisma.item
|
||||
.findUnique({
|
||||
where: {
|
||||
id: parseInt(req.query.id.toString())
|
||||
},
|
||||
// Get contactInfo, category, storageLocation( storageUnit<contactInfo> ) from relations
|
||||
include: {
|
||||
contactInfo: true,
|
||||
category: true,
|
||||
storageLocation: {
|
||||
include: {
|
||||
storageUnit: {
|
||||
include: {
|
||||
contactInfo: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((items) => {
|
||||
if (items) {
|
||||
res.status(200).json(items);
|
||||
} else {
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
} else {
|
||||
// Get all items
|
||||
const itemCountNotFiltered = await prisma.item.count({});
|
||||
|
||||
// Get all items (filtered)
|
||||
const itemCountFiltered = await prisma.item.count({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
SKU: {
|
||||
// Probably use prisma's Full-text search if it's out of beta
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
|
||||
});
|
||||
// log.core.debug('Dynamic relation:', parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()));
|
||||
|
||||
prisma.item
|
||||
.findMany({
|
||||
take: parseIntOrUndefined(req.query.limit),
|
||||
skip: parseIntOrUndefined(req.query.offset),
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
SKU: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()),
|
||||
// Get contactInfo, category, storageLocation( storageUnit<contactInfo> ) from relations.
|
||||
include: {
|
||||
contactInfo: true,
|
||||
category: true,
|
||||
storageLocation: {
|
||||
include: {
|
||||
storageUnit: {
|
||||
include: {
|
||||
contactInfo: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((items) => {
|
||||
if (items) {
|
||||
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
|
||||
} else {
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create item.
|
||||
function post(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.name) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if status is valid.
|
||||
if (req.body.status !== undefined && !Object.keys(itemStatus).includes(req.body.status)) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: `Status is not valid, valid values are: ${Object.keys(itemStatus).join(', ')}` });
|
||||
return;
|
||||
}
|
||||
|
||||
prisma.item
|
||||
.create({
|
||||
data: {
|
||||
SKU: req.body.sku,
|
||||
amount: parseIntOrUndefined(req.body.amount), // FIXME: This is silently failing if NaN..
|
||||
name: req.body.name,
|
||||
comment: req.body.comment,
|
||||
status: req.body.status, // Only enum(itemStatus) values are valid
|
||||
// Relations
|
||||
contactInfo: parseIntRelation(req.body.contactInfoId, undefined, true),
|
||||
category: parseIntRelation(req.body.categoryId, undefined, true),
|
||||
storageLocation: parseIntRelation(req.body.storageLocationId, undefined, true),
|
||||
|
||||
manufacturer: req.body.manufacturer,
|
||||
|
||||
//contents: {
|
||||
// connect: [{ id: 1 }, { id: 2 }, { id: 3 }]
|
||||
//},
|
||||
//baseItem: {
|
||||
// connect: {
|
||||
// id: req.body.baseitemId
|
||||
// }
|
||||
//},
|
||||
createdBy: req.body.createdBy
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'CREATED', message: 'Successfully created item', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Item already exists' });
|
||||
} else if (err.code == 'P2003') {
|
||||
// P2003 -> "Foreign key constraint failed on the field: {field_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
// FIXME: Is this errormessage right?
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update storageLocation. -> Only existing contactInfo.
|
||||
async function patch(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if number is a valid integer
|
||||
if (!Number.isInteger(parseInt(req.body.id.toString()))) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'id field must be an integer' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if status is valid.
|
||||
if (req.body.status !== undefined && !Object.keys(itemStatus).includes(req.body.status)) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: `Status is not valid, valid values are: ${Object.keys(itemStatus).join(', ')}` });
|
||||
return;
|
||||
}
|
||||
|
||||
prisma.item
|
||||
.update({
|
||||
where: {
|
||||
id: parseInt(req.body.id)
|
||||
},
|
||||
data: {
|
||||
SKU: req.body.sku,
|
||||
amount: parseIntOrUndefined(req.body.amount), // FIXME: This is silently failing if NaN..
|
||||
name: req.body.name,
|
||||
comment: req.body.comment,
|
||||
status: req.body.status, // Only enum(itemStatus) values are valid
|
||||
// Relations
|
||||
contactInfo: parseIntRelation(req.body.contactInfoId),
|
||||
category: parseIntRelation(req.body.categoryId),
|
||||
storageLocation: parseIntRelation(req.body.storageLocationId),
|
||||
|
||||
manufacturer: req.body.manufacturer,
|
||||
|
||||
//contents: {
|
||||
// connect: [{ id: 1 }, { id: 2 }, { id: 3 }]
|
||||
//},
|
||||
//baseItem: {
|
||||
// connect: {
|
||||
// id: req.body.baseitemId
|
||||
// }
|
||||
//},
|
||||
createdBy: req.body.createdBy
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated item', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'Item already exists' });
|
||||
} else if (err.code == 'P2003') {
|
||||
// P2003 -> "Foreign key constraint failed on the field: {field_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete item.
|
||||
async function del(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id) {
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Does the id exist? If not return 410 Gone.
|
||||
try {
|
||||
const result = await prisma.item.findUnique({
|
||||
where: {
|
||||
id: parseInt(req.body.id)
|
||||
}
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'Item does not exist' });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
prisma.item
|
||||
.delete({
|
||||
where: {
|
||||
id: parseInt(req.body.id)
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted item' });
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
|
||||
export default { get, post, patch, del };
|
9
src/routes/api/v1/search/index.ts
Normal file
9
src/routes/api/v1/search/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import express from 'express';
|
||||
import sku from './sku.js';
|
||||
|
||||
// Router base is '/api/v1'
|
||||
const Router = express.Router({ strict: false });
|
||||
|
||||
Router.route('/sku').get(sku.get);
|
||||
|
||||
export default Router;
|
30
src/routes/api/v1/search/sku.ts
Normal file
30
src/routes/api/v1/search/sku.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma, __path, log } from '../../../../index.js';
|
||||
|
||||
// Get item.
|
||||
function get(req: Request, res: Response) {
|
||||
if (!req.query.sku) {
|
||||
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
prisma.item
|
||||
.findMany({
|
||||
where: {
|
||||
SKU: {
|
||||
contains: req.query.sku.toString()
|
||||
}
|
||||
},
|
||||
include: {
|
||||
category: true
|
||||
}
|
||||
})
|
||||
.then((items) => {
|
||||
res.status(200).json(items);
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
|
||||
});
|
||||
}
|
||||
|
||||
export default { get };
|
@ -1,15 +1,20 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma, __path, log } from '../../../index.js';
|
||||
import { contactType } from '@prisma/client';
|
||||
import { parseIntOrUndefined, parseDynamicSortBy, parseIntRelation } from '../../../assets/helper.js';
|
||||
|
||||
// Get storageLocation.
|
||||
function get(req: Request, res: Response) {
|
||||
if (req.query.getAll === undefined) {
|
||||
// Check if required fields are present.
|
||||
if (!req.query.id) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
return;
|
||||
}
|
||||
async function get(req: Request, res: Response) {
|
||||
if (req.query.sort === undefined) {
|
||||
req.query.sort = 'id';
|
||||
}
|
||||
if (req.query.order === undefined) {
|
||||
req.query.order = 'asc';
|
||||
}
|
||||
if (req.query.search === undefined) {
|
||||
req.query.search = '';
|
||||
}
|
||||
|
||||
if (req.query.id) {
|
||||
prisma.storageLocation
|
||||
.findUnique({
|
||||
where: {
|
||||
@ -22,33 +27,56 @@ function get(req: Request, res: Response) {
|
||||
})
|
||||
.then((items) => {
|
||||
if (items) {
|
||||
res.status(200).json(JSON.stringify(items));
|
||||
res.status(200).json(items);
|
||||
} else {
|
||||
res.status(410).json({ error: 'it seems that there is no storageLocation present.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
} else {
|
||||
// Get all items
|
||||
const itemCountNotFiltered = await prisma.storageLocation.count({});
|
||||
|
||||
// Get all items (filtered)
|
||||
const itemCountFiltered = await prisma.storageLocation.count({
|
||||
where: {
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
|
||||
});
|
||||
|
||||
prisma.storageLocation
|
||||
.findMany({
|
||||
take: parseIntOrUndefined(req.query.limit),
|
||||
skip: parseIntOrUndefined(req.query.offset),
|
||||
where: {
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
// Get storageUnit from relation.
|
||||
include: {
|
||||
storageUnit: true
|
||||
}
|
||||
},
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()),
|
||||
})
|
||||
.then((items) => {
|
||||
if (items) {
|
||||
res.status(200).json(JSON.stringify(items));
|
||||
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
|
||||
} else {
|
||||
res.status(410).json({ error: 'storageLocation does not exist.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -57,7 +85,7 @@ function get(req: Request, res: Response) {
|
||||
function post(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.name) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
// Create storageLocation with existing storageUnit.
|
||||
@ -72,21 +100,26 @@ function post(req: Request, res: Response) {
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'created', id: data.id });
|
||||
res.status(201).json({ status: 'CREATED', message: 'Successfully created storageLocation', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ error: 'storageLocation already exists.' });
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageLocation already exists' });
|
||||
} else if (err.code == 'P2003') {
|
||||
// P2003 -> "Foreign key constraint failed on the field: {field_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ error: 'specified storageUnitId does not exist' });
|
||||
// FIXME: Is this errormessage right?
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnitId does not exist' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -95,7 +128,7 @@ function post(req: Request, res: Response) {
|
||||
async function patch(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id || !req.body.name || !req.body.storageUnitId) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -108,12 +141,12 @@ async function patch(req: Request, res: Response) {
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(404).json({ error: 'storageLocation does not exist.' });
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation does not exist' });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
prisma.storageLocation
|
||||
@ -123,25 +156,33 @@ async function patch(req: Request, res: Response) {
|
||||
},
|
||||
data: {
|
||||
name: req.body.name,
|
||||
storageUnitId: parseInt(req.body.storageUnitId) || undefined
|
||||
storageUnit: parseIntRelation(req.body.storageUnitId)
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
res.status(201).json({ status: 'updated' });
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated storageLocation', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ error: 'storageLocation already exists.' });
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageLocation already exists' });
|
||||
} else if (err.code == 'P2003') {
|
||||
// P2003 -> "Foreign key constraint failed on the field: {field_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ error: 'specified storageUnitId does not exist' });
|
||||
// FIXME: Is this errormessage right?
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnitId does not exist' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -150,7 +191,7 @@ async function patch(req: Request, res: Response) {
|
||||
async function del(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -163,12 +204,12 @@ async function del(req: Request, res: Response) {
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(410).json({ error: 'storageLocation does not exist.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation does not exist' });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
prisma.storageLocation
|
||||
@ -178,11 +219,11 @@ async function del(req: Request, res: Response) {
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
res.status(200).json({ status: 'deleted' });
|
||||
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted storageLocation' });
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma, __path, log } from '../../../index.js';
|
||||
import { contactType } from '@prisma/client';
|
||||
import { parseDynamicSortBy, parseIntOrUndefined } from '../../../assets/helper.js';
|
||||
|
||||
// Get storageUnit.
|
||||
function get(req: Request, res: Response) {
|
||||
if (req.query.getAll === undefined) {
|
||||
// Check if required fields are present.
|
||||
if (!req.query.id) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
return;
|
||||
}
|
||||
async function get(req: Request, res: Response) {
|
||||
if (req.query.sort === undefined) {
|
||||
req.query.sort = 'id';
|
||||
}
|
||||
if (req.query.order === undefined) {
|
||||
req.query.order = 'asc';
|
||||
}
|
||||
if (req.query.search === undefined) {
|
||||
req.query.search = '';
|
||||
}
|
||||
if (req.query.id) {
|
||||
prisma.storageUnit
|
||||
.findUnique({
|
||||
where: {
|
||||
@ -23,34 +28,57 @@ function get(req: Request, res: Response) {
|
||||
})
|
||||
.then((items) => {
|
||||
if (items) {
|
||||
res.status(200).json(JSON.stringify(items));
|
||||
res.status(200).json(items);
|
||||
} else {
|
||||
res.status(410).json({ error: 'it seems that there is no storageUnit present.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
} else {
|
||||
// Get all items
|
||||
const itemCountNotFiltered = await prisma.storageUnit.count({});
|
||||
|
||||
// Get all items (filtered)
|
||||
const itemCountFiltered = await prisma.storageUnit.count({
|
||||
where: {
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
|
||||
});
|
||||
|
||||
prisma.storageUnit
|
||||
.findMany({
|
||||
take: parseIntOrUndefined(req.query.limit),
|
||||
skip: parseIntOrUndefined(req.query.offset),
|
||||
// Get contactInfo and StorageLocation from relation.
|
||||
include: {
|
||||
contactInfo: true,
|
||||
StorageLocation: true
|
||||
}
|
||||
},
|
||||
where: {
|
||||
name: {
|
||||
// @ts-ignore
|
||||
contains: req.query.search.length > 0 ? req.query.search : ''
|
||||
}
|
||||
},
|
||||
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString())
|
||||
})
|
||||
.then((items) => {
|
||||
if (items) {
|
||||
res.status(200).json(JSON.stringify(items));
|
||||
res.status(200).json({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
|
||||
} else {
|
||||
res.status(410).json({ error: 'storageUnit does not exist.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit does not exist' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
log.db.error(err);
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -61,7 +89,7 @@ function post(req: Request, res: Response) {
|
||||
if (req.body.locationId === 'META_CREATENEW') {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.street || !req.body.houseNumber || !req.body.zipCode || !req.body.city || !req.body.country || !req.body.name) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -86,23 +114,27 @@ function post(req: Request, res: Response) {
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'created', id: data.id });
|
||||
res.status(201).json({ status: 'CREATED', message: 'Successfully created storageUnit', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ error: 'storageUnit already exists.' });
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageUnit already exists' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.name || !req.body.locationId) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
// Create storageUnit with existing location.
|
||||
@ -121,17 +153,21 @@ function post(req: Request, res: Response) {
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'created', id: data.id });
|
||||
res.status(201).json({ status: 'CREATED', message: 'Successfully created storageUnit', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ error: 'storageUnit already exists.' });
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageUnit already exists' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -141,7 +177,7 @@ function post(req: Request, res: Response) {
|
||||
async function patch(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id || !req.body.name || !req.body.locationId) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -154,12 +190,12 @@ async function patch(req: Request, res: Response) {
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(410).json({ error: 'storageUnit does not exist.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit does not exist' });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
// Check if the locationId(contactInfo) exists. If not return 410 Gone.
|
||||
@ -171,12 +207,12 @@ async function patch(req: Request, res: Response) {
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(410).json({ error: 'locationId does not exist.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'locationId does not exist' });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
prisma.storageUnit
|
||||
@ -188,23 +224,30 @@ async function patch(req: Request, res: Response) {
|
||||
name: req.body.name,
|
||||
contactInfo: {
|
||||
connect: {
|
||||
id: parseInt(req.body.locationId)
|
||||
id: parseInt(req.body.locationId) // TODO: Rename to contactInfoId
|
||||
}
|
||||
}
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
res.status(201).json({ status: 'updated' });
|
||||
.then((data) => {
|
||||
res.status(201).json({ status: 'UPDATED', message: 'Successfully updated storageUnit', id: data.id });
|
||||
})
|
||||
.catch((err) => {
|
||||
// Check if an entry already exists.
|
||||
if (err.code === 'P2002') {
|
||||
// P2002 -> "Unique constraint failed on the {constraint}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(409).json({ error: 'storageUnit already exists.' });
|
||||
res.status(409).json({ status: 'ERROR', errorcode: 'EXISTING', message: 'storageUnit already exists' });
|
||||
} else if (err.code == 'P2000') {
|
||||
// P2000 -> "The provided value for the column is too long for the column's type. Column: {column_name}"
|
||||
// https://www.prisma.io/docs/reference/api-reference/error-reference
|
||||
res.status(404).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more fields exceed the maximum length restriction' });
|
||||
} else {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -213,7 +256,7 @@ async function patch(req: Request, res: Response) {
|
||||
async function del(req: Request, res: Response) {
|
||||
// Check if required fields are present.
|
||||
if (!req.body.id) {
|
||||
res.status(400).render(__path + '/src/frontend/errors/400.eta.html');
|
||||
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -226,12 +269,13 @@ async function del(req: Request, res: Response) {
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
res.status(410).json({ error: 'storageUnit does not exist.' });
|
||||
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit does not exist' });
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
}
|
||||
|
||||
prisma.storageUnit
|
||||
@ -241,11 +285,11 @@ async function del(req: Request, res: Response) {
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
res.status(200).json({ status: 'deleted' });
|
||||
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted storageUnit' });
|
||||
})
|
||||
.catch((err) => {
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||
});
|
||||
}
|
||||
|
||||
|
13
src/routes/api/v1/version.ts
Normal file
13
src/routes/api/v1/version.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
const revision = req.app.locals.versionRev;
|
||||
let updateAvailable = false;
|
||||
if(req.app.locals.versionRevLong !== req.app.locals.versionRevLatest) {
|
||||
updateAvailable = true;
|
||||
}
|
||||
|
||||
res.status(200).send({ version: '1.0.0', commit: revision, updateAvailable: updateAvailable });
|
||||
};
|
||||
|
||||
export default { get };
|
90
src/routes/auth/index.ts
Normal file
90
src/routes/auth/index.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import passport from 'passport';
|
||||
import { Strategy as LocalStrategy } from 'passport-local';
|
||||
import express, { Request, Response } from 'express';
|
||||
import { prisma, __path, log, config, app } from '../../index.js';
|
||||
|
||||
// Middleware Imports
|
||||
import { checkAuthentication } from '../../middleware/auth.mw.js'
|
||||
|
||||
/* Configure password authentication strategy.
|
||||
*
|
||||
* The `LocalStrategy` authenticates users by verifying a username and password.
|
||||
* The strategy parses the username and password from the request and calls the
|
||||
* `verify` function.
|
||||
*
|
||||
* The `verify` function queries the database for the user record and verifies
|
||||
* the password by hashing the password supplied by the user and comparing it to
|
||||
* the hashed password stored in the database. If the comparison succeeds, the
|
||||
* user is authenticated; otherwise, not.
|
||||
*/
|
||||
passport.use(
|
||||
new LocalStrategy(function verify(username, password, cb) {
|
||||
//log.auth.debug('LocalStrategy:', username, password);
|
||||
|
||||
for (const [user, pass] of Object.entries(config.global.auth.local.users)) {
|
||||
//log.auth.debug('Loop(REQ):', username, password);
|
||||
//log.auth.debug('Loop(CFG):', user, pass);
|
||||
|
||||
if (user.toLowerCase() === username.toLowerCase() && pass === password) {
|
||||
log.auth.debug('LocalStrategy: success');
|
||||
return cb(null, { username: username }); // This is the user object.
|
||||
}
|
||||
}
|
||||
log.auth.debug('LocalStrategy: failed');
|
||||
return cb(null, false, { message: 'Incorrect username or password.' });
|
||||
})
|
||||
/*
|
||||
1. If the user not found in DB,
|
||||
done (null, false)
|
||||
|
||||
2. If the user found in DB, but password does not match,
|
||||
done (null, false)
|
||||
|
||||
3. If user found in DB and password match,
|
||||
done (null, {authenticated_user})
|
||||
*/
|
||||
);
|
||||
|
||||
/* Configure session management.
|
||||
*
|
||||
* When a login session is established, information about the user will be
|
||||
* stored in the session. This information is supplied by the `serializeUser`
|
||||
* function, which is yielding the user ID and username.
|
||||
*
|
||||
* As the user interacts with the app, subsequent requests will be authenticated
|
||||
* by verifying the session. The same user information that was serialized at
|
||||
* session establishment will be restored when the session is authenticated by
|
||||
* the `deserializeUser` function.
|
||||
*
|
||||
*/
|
||||
passport.serializeUser(function (user: any, cb) {
|
||||
process.nextTick(function () {
|
||||
// log.auth.debug('Called seriealizeUser');
|
||||
// log.auth.debug('user:', user);
|
||||
return cb(null, {
|
||||
username: user.username
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
passport.deserializeUser(function (user, cb) {
|
||||
process.nextTick(function () {
|
||||
// log.auth.debug('Called deseriealizeUser');
|
||||
return cb(null, user);
|
||||
});
|
||||
});
|
||||
|
||||
// Route imports
|
||||
import testRoute from './test.js';
|
||||
import loginRoute from './login.js';
|
||||
//import logoutRoute from './login.js'
|
||||
|
||||
// Router base is '/auth'
|
||||
const Router = express.Router({ strict: false });
|
||||
|
||||
Router.route('/login').get(loginRoute.get);
|
||||
Router.route('/login').post(passport.authenticate('local', { successRedirect: '/', failureRedirect: '/auth/login?failed' }));
|
||||
|
||||
Router.route('/test').get(checkAuthentication, testRoute.get);
|
||||
|
||||
export default Router;
|
@ -1,5 +1,7 @@
|
||||
import passport from 'passport';
|
||||
|
||||
import express, { Request, Response } from 'express';
|
||||
import { prisma, __path } from '../../../index.js';
|
||||
import { prisma, __path, log } from '../../index.js';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
res.render(__path + '/src/frontend/auth/login.eta.html'); //, { items: items });
|
7
src/routes/auth/test.ts
Normal file
7
src/routes/auth/test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
res.status(200).send('Auth Test Successful!');
|
||||
};
|
||||
|
||||
export default { get };
|
@ -1,5 +1,5 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { prisma, __path } from '../../index.js';
|
||||
import { prisma, __path, log } from '../../index.js';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
prisma.item
|
||||
@ -17,7 +17,7 @@ function get(req: Request, res: Response) {
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
});
|
||||
}
|
||||
|
@ -2,10 +2,12 @@ import express from 'express';
|
||||
|
||||
// Route imports
|
||||
import skuRoute from './:id.js';
|
||||
import skuRouteDash from './itemInfo.js'
|
||||
import testRoute from './test.js';
|
||||
import dashboardRoute from './dashboard.js';
|
||||
import itemsRoute from './items.js';
|
||||
import manage_routes from './manage/index.js';
|
||||
import project_routes from './projects/index.js';
|
||||
|
||||
// Router base is '/'
|
||||
const Router = express.Router({ strict: false });
|
||||
@ -13,10 +15,13 @@ const Router = express.Router({ strict: false });
|
||||
Router.route('/test').get(testRoute.get);
|
||||
Router.route('/items').get(itemsRoute.get);
|
||||
|
||||
Router.route('/:id(\\w{8})').get(skuRoute.get);
|
||||
Router.use('/projects', project_routes); // has to be before skuRoute
|
||||
Router.route('/:id(\\w{8})').get(skuRoute.get); // we should probably deprecate this
|
||||
Router.route('/s/:id').get(skuRouteDash.get);
|
||||
|
||||
Router.use('/manage', manage_routes);
|
||||
|
||||
|
||||
Router.route('/').get(dashboardRoute.get);
|
||||
|
||||
export default Router;
|
||||
|
39
src/routes/frontend/itemInfo.ts
Normal file
39
src/routes/frontend/itemInfo.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma, __path } from '../../index.js';
|
||||
import * as Eta from 'eta';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
// Get data from database using sku from url.
|
||||
prisma.item
|
||||
.findFirst({
|
||||
where: {
|
||||
SKU: req.params.id
|
||||
},
|
||||
select: {
|
||||
SKU: true,
|
||||
name: true,
|
||||
comment: true,
|
||||
amount: true,
|
||||
status: true,
|
||||
// Get category name from relation.
|
||||
category: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((item) => {
|
||||
if (item) {
|
||||
Eta.renderFile(__path + '/src/frontend/itemInfo.eta.html', item).then((html) => {
|
||||
res.send(html);
|
||||
});
|
||||
} else {
|
||||
Eta.renderFile(__path + '/src/frontend/errors/404.eta.html', item).then((html) => {
|
||||
res.status(404).send(html);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default { get };
|
@ -1,35 +1,20 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma, __path } from '../../index.js';
|
||||
import { prisma, __path, log } from '../../index.js';
|
||||
|
||||
async function get(req: Request, res: Response) {
|
||||
// If no page is provided redirect to first
|
||||
if (req.query.page === undefined) {
|
||||
res.redirect('?page=1');
|
||||
return;
|
||||
}
|
||||
|
||||
let page = parseInt(req.query.page.toString());
|
||||
const itemCount = await prisma.item.count({}); // Count all items in the DB
|
||||
|
||||
const takeSize = 25; // Amount of times per page
|
||||
const pageSize = Math.ceil(itemCount / takeSize); // Amount of pages, always round up
|
||||
|
||||
// If page is less then 1 or more then the max page size redirect to first or last page
|
||||
if (page < 1) {
|
||||
res.redirect('?page=1');
|
||||
return;
|
||||
} else if (page > pageSize) {
|
||||
res.redirect('?page=' + pageSize);
|
||||
return;
|
||||
}
|
||||
|
||||
prisma.item
|
||||
.findMany({ skip: (page - 1) * takeSize, take: takeSize }) // Skip the amount of items per page times the page number minus 1; skip has to be (page-1)*takeSize because skip is 0 indexed
|
||||
.findMany({}) // Skip the amount of items per page times the page number minus 1; skip has to be (page-1)*takeSize because skip is 0 indexed
|
||||
.then((items) => {
|
||||
res.render(__path + '/src/frontend/items.eta.html', { items: items, currentPage: page, maxPages: pageSize });
|
||||
prisma.storageLocation.findMany({}).then((locations) => {
|
||||
prisma.itemCategory.findMany({}).then((categories) => {
|
||||
prisma.contactInfo.findMany({}).then((contactInfo) => {
|
||||
res.render(__path + '/src/frontend/items.eta.html', { items: items, storeLocs: locations, categories: categories, contactInfo: contactInfo });
|
||||
})
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
});
|
||||
}
|
||||
|
@ -7,10 +7,16 @@ function get(req: Request, res: Response) {
|
||||
.findMany({})
|
||||
.then((items) => {
|
||||
// Count amount of total items
|
||||
// Replace "null" with an empty string
|
||||
items.forEach((item) => {
|
||||
if (item.description == null || item.description == "null") {
|
||||
item.description = '';
|
||||
}
|
||||
});
|
||||
res.render(__path + '/src/frontend/manage/categoryManager.eta.html', { items: items });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
log.db.error(err);
|
||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||
});
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ function post(req: Request, res: Response) {
|
||||
// Handle file upload and import
|
||||
console.log(req.files);
|
||||
if (!req.files || Object.keys(req.files).length === 0) {
|
||||
return res.status(400).send('No files were uploaded.');
|
||||
return res.status(400).send('No files were uploaded');
|
||||
}
|
||||
|
||||
const file: UploadedFile = req.files.formFile as UploadedFile;
|
||||
@ -68,7 +68,7 @@ function post(req: Request, res: Response) {
|
||||
SKU: record.sku,
|
||||
manufacturer: record.manufacturer,
|
||||
status: itemStatus.normal,
|
||||
importedBy: 'CSV_IMPORT'
|
||||
createdBy: 'CSV_IMPORT'
|
||||
}
|
||||
});
|
||||
listOfPromises.push(promise);
|
||||
|
@ -64,7 +64,7 @@ function post(req: Request, res: Response) {
|
||||
SKU: record.sku,
|
||||
manufacturer: record.manufacturer,
|
||||
status: itemStatus.normal,
|
||||
importedBy: 'CSV_IMPORT'
|
||||
createdBy: 'CSV_IMPORT'
|
||||
}
|
||||
});
|
||||
listOfPromises.push(promise);
|
||||
|
@ -7,7 +7,6 @@ import jsonImportRoute from './import/jsonImport.js';
|
||||
import categoryManager from './categoryManager.js';
|
||||
import storageManager from './storageManager.js';
|
||||
import startpageRoute from './startpage.js';
|
||||
import demoLoginRoute from './demo.js'
|
||||
|
||||
// Router base is '/manage'
|
||||
const Router = express.Router({ strict: false });
|
||||
@ -17,7 +16,6 @@ Router.route('/categories').get(categoryManager.get);
|
||||
Router.route('/storages').get(storageManager.get);
|
||||
Router.route('/import/csv').get(csvImportRoute.get).post(csvImportRoute.post);
|
||||
Router.route('/import/json').get(jsonImportRoute.get).post(jsonImportRoute.post);
|
||||
Router.route('/demo/login').get(demoLoginRoute.get);
|
||||
Router.route('/').get(startpageRoute.get);
|
||||
|
||||
export default Router;
|
||||
|
9
src/routes/frontend/projects/dashboard.ts
Normal file
9
src/routes/frontend/projects/dashboard.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { prisma, __path, log } from '../../../index.js';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
res.render(__path + '/src/frontend/projects/dashboard.eta.html');
|
||||
|
||||
}
|
||||
|
||||
export default { get };
|
11
src/routes/frontend/projects/index.ts
Normal file
11
src/routes/frontend/projects/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import express from 'express';
|
||||
|
||||
// Route imports
|
||||
import dashboard from './dashboard.js';
|
||||
|
||||
// Router base is '/manage'
|
||||
const Router = express.Router({ strict: false });
|
||||
|
||||
Router.route('/').get(dashboard.get);
|
||||
|
||||
export default Router;
|
@ -1,25 +1,35 @@
|
||||
import express, { Express } from 'express';
|
||||
import { __path, prisma } from '../index.js';
|
||||
import * as Sentry from '@sentry/node';
|
||||
|
||||
// Middleware Imports
|
||||
import { checkAuthentication } from '../middleware/auth.mw.js'
|
||||
|
||||
// Route imports
|
||||
import frontend_routes from './frontend/index.js';
|
||||
import static_routes from './static/index.js';
|
||||
import api_routes from './api/index.js';
|
||||
import auth_routes from './auth/index.js';
|
||||
|
||||
const Router = express.Router({ strict: false });
|
||||
|
||||
Router.use('/static', static_routes);
|
||||
Router.use('/api', api_routes);
|
||||
Router.use('/', frontend_routes);
|
||||
Router.use('/api', checkAuthentication, api_routes);
|
||||
Router.use('/auth', auth_routes);
|
||||
Router.use('/', checkAuthentication, frontend_routes);
|
||||
|
||||
// The error handler must be before any other error middleware and after all controllers
|
||||
Router.use(Sentry.Handlers.errorHandler());
|
||||
|
||||
// Default route.
|
||||
Router.all('*', function (req, res) {
|
||||
// TODO: Respond based on content-type (with req.is('application/json'))
|
||||
if (req.is('application/json')) {
|
||||
res.status(418)//.json({ errorcode: '404' });
|
||||
res.status(404).json({ errorcode: 'NOT_FOUND', error: 'Not Found!' });
|
||||
} else {
|
||||
res.status(418)//.render(__path + '/src/frontend/errors/404.eta.html', { url: req.originalUrl });
|
||||
res.status(404).render(__path + '/src/frontend/errors/404.eta.html', { url: req.originalUrl });
|
||||
}
|
||||
});
|
||||
|
||||
export default Router;
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
.magicalTriangle[aria-expanded=true] {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
.magicalTriangle[aria-expanded=true] {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=dashboard-mod.css.map */
|
@ -1 +0,0 @@
|
||||
{"version":3,"sourceRoot":"","sources":["../../src/sass/dashboard-mod.scss"],"names":[],"mappings":"AAAA;EACC;EACA","file":"dashboard-mod.css"}
|
@ -2,26 +2,58 @@ body {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.headLogo {
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
/* Give the logo a dark shadow to make it pop out */
|
||||
.headLogo {
|
||||
filter: drop-shadow(0 0 0.85rem rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
.headLogo {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
.versionInfo {
|
||||
font-size: 0.75rem;
|
||||
|
||||
}
|
||||
/** Safari */
|
||||
|
||||
@media not all and (min-resolution:.001dpcm) {
|
||||
@supports (-webkit-appearance:none) and (stroke-color:transparent) {
|
||||
.headLogo {
|
||||
width: 1%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
top: 1.5rem;
|
||||
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
padding: 48px 0 0; /* Height of navbar */
|
||||
z-index: 100;
|
||||
/* Behind the navbar */
|
||||
padding: 48px 0 0;
|
||||
/* Height of navbar */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/*
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar {
|
||||
top: 5rem;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
@ -29,7 +61,8 @@ body {
|
||||
height: calc(100vh - 48px);
|
||||
padding-top: 0.5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
overflow-y: auto;
|
||||
/* Scrollable contents if viewport is shorter than content. */
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
@ -63,7 +96,6 @@ body {
|
||||
|
||||
/*
|
||||
* Navbar
|
||||
*/
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: 0.75rem;
|
||||
@ -71,7 +103,7 @@ body {
|
||||
font-size: 1rem;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
*/
|
||||
.navbar .navbar-toggler {
|
||||
top: 0.25rem;
|
||||
right: 1rem;
|
||||
@ -92,6 +124,7 @@ body {
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/*
|
||||
.autocomplete-items {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
@ -99,20 +132,31 @@ body {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
*/
|
||||
.rotate {
|
||||
transform: rotate(-90deg) !important;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.rotate::before {
|
||||
transform: rotate(-90deg) !important;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.dropdownIndicator {
|
||||
.derotate {
|
||||
transform: rotate(0deg) !important;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.derotate::before {
|
||||
transform: rotate(0deg) !important;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.dropdownIndicator {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Utilities
|
||||
@ -140,3 +184,8 @@ body {
|
||||
.loaderActive {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
|
||||
.navShadow {
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
12
static/js/confettiHeader.js
Normal file
12
static/js/confettiHeader.js
Normal file
@ -0,0 +1,12 @@
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function doTheConfetti() {
|
||||
confetti({
|
||||
angle: randomInRange(90, 110),
|
||||
spread: randomInRange(70, 120),
|
||||
particleCount: randomInRange(100, 200),
|
||||
origin: { y: 0.6, x: randomInRange(0.4, 0.8) },
|
||||
});
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
const FLAG_supports_new_data_loader = true;
|
||||
|
||||
function getDataForEdit(name) {
|
||||
$.ajax({
|
||||
type: 'get',
|
||||
url: `/api/v1/categories?name=${name}`,
|
||||
success: function (data) {
|
||||
const result = JSON.parse(data);
|
||||
|
||||
success: function (result) {
|
||||
// Get elements inside the editCategoryModal
|
||||
const modal_categoryName = document.getElementById('editCategoryModalName');
|
||||
const modal_categoryDescription = document.getElementById('editCategoryModalDescription');
|
||||
@ -39,3 +39,42 @@ function primeEdit() {
|
||||
form.setAttribute('method', 'PATCH');
|
||||
return true;
|
||||
}
|
||||
|
||||
const itemList = $('#itemList');
|
||||
// itemList.empty();
|
||||
itemList.bootstrapTable({ url: '/api/v1/categories', search: true, showRefresh: true, responseHandler: dataResponseHandler, sidePagination: 'server', serverSort: true, silentSort: false });
|
||||
setTimeout(() => {
|
||||
activateTooltips();
|
||||
}, 1000);
|
||||
|
||||
function loadPageData() {
|
||||
itemList.bootstrapTable('refresh')
|
||||
setTimeout(() => {
|
||||
$(".tooltip").tooltip("hide");
|
||||
activateTooltips();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function dataResponseHandler(json) {
|
||||
// console.log(json)
|
||||
totalNotFiltered = json.totalNotFiltered;
|
||||
total = json.total;
|
||||
json = json.items;
|
||||
json.forEach((item) => {
|
||||
item.actions = `
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#editCategoryModal" onclick="primeEdit(); getDataForEdit('${item.name}')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('${item.name}','categories','Category','name')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>`
|
||||
});
|
||||
///// --------------------------------- /////
|
||||
setTimeout(() => {
|
||||
activateTooltips();
|
||||
}, 200);
|
||||
return {"rows": json, total: total, totalNotFiltered: totalNotFiltered, totalRows: total};
|
||||
|
||||
}
|
||||
|
||||
loadPageData()
|
||||
|
89
static/js/editItems.js
Normal file
89
static/js/editItems.js
Normal file
@ -0,0 +1,89 @@
|
||||
|
||||
function primeCreateNew() {
|
||||
// Clear the form
|
||||
$('.form-control').val('');
|
||||
const form = document.getElementById('ItemModalForm');
|
||||
document.getElementById('itemModifyModalLabel').innerText= "Create a new item";
|
||||
form.setAttribute('method', 'POST');
|
||||
return true;
|
||||
}
|
||||
|
||||
function primeEdit() {
|
||||
const form = document.getElementById('ItemModalForm');
|
||||
document.getElementById('itemModifyModalLabel').innerText = 'Edit an item';
|
||||
form.setAttribute('method', 'PATCH');
|
||||
return true;
|
||||
}
|
||||
|
||||
function getDataForEdit(id) {
|
||||
$.ajax({
|
||||
type: 'get',
|
||||
url: `/api/v1/items?id=${id}`,
|
||||
success: function (result) {
|
||||
|
||||
// Get elements inside the editCategoryModal
|
||||
const modal_itemName = document.getElementById('itemModifyModalName');
|
||||
const modal_itemComment = document.getElementById('itemModifyModalComment');
|
||||
const modal_itemAmount = document.getElementById('itemModifyModalAmount');
|
||||
const modal_itemSKU = document.getElementById('itemModifyModalSKU');
|
||||
const modal_itemStorageLocation = document.getElementById('itemModifyModalStorageLocation');
|
||||
const modal_itemManufacturer = document.getElementById('itemModifyModalManuf');
|
||||
const modal_itemCategory = document.getElementById('itemModifyModalCategory');
|
||||
const modal_itemStatus = document.getElementById('itemModifyModalStatus');
|
||||
const modal_itemid = document.getElementById('itemModifyModalId');
|
||||
const modal_userinfo = document.getElementById('itemModifyModalContact');
|
||||
|
||||
modal_itemName.value = result.name;
|
||||
modal_itemComment.value = result.comment;
|
||||
modal_itemAmount.value = result.amount;
|
||||
modal_itemSKU.value = result.SKU;
|
||||
modal_itemManufacturer.value = result.manufacturer;
|
||||
|
||||
// Select the correct option in the dropdown
|
||||
const modal_itemCategoryOptions = modal_itemCategory.options;
|
||||
modal_itemCategoryOptions[0].selected = true;
|
||||
for (let i = 0; i < modal_itemCategoryOptions.length; i++) {
|
||||
if (modal_itemCategoryOptions[i].value == result.categoryId) {
|
||||
modal_itemCategoryOptions[i].selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Select the correct option in the dropdown
|
||||
const modal_itemStatusOptions = modal_itemStatus.options;
|
||||
modal_itemStatusOptions[0].selected = true;
|
||||
for (let i = 0; i < modal_itemStatusOptions.length; i++) {
|
||||
if (modal_itemStatusOptions[i].value == result.statusId) {
|
||||
modal_itemStatusOptions[i].selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Select the correct option in the dropdown
|
||||
const modal_itemStorageLocationOptions = modal_itemStorageLocation.options;
|
||||
modal_itemStorageLocationOptions[0].selected = true;
|
||||
for (let i = 0; i < modal_itemStorageLocationOptions.length; i++) {
|
||||
if (modal_itemStorageLocationOptions[i].value == result.storageLocationId) {
|
||||
modal_itemStorageLocationOptions[i].selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
modal_userinfo.selectedIndex = 0;
|
||||
// Select the correct option in the dropdown
|
||||
const modal_userInfoOptions = modal_userinfo.options;
|
||||
for (let i = 0; i < modal_userInfoOptions.length; i++) {
|
||||
if (modal_userInfoOptions[i].value == result.contactInfoId) {
|
||||
modal_userInfoOptions[i].selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
modal_itemid.value = result.id;
|
||||
},
|
||||
error: function (data) {
|
||||
console.log('!!!! ERROR !!!!', data);
|
||||
// Hide overlay with spinner
|
||||
$('.loader-overlay').removeClass('active');
|
||||
// Close the modal
|
||||
$('.modal').modal('hide');
|
||||
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The category does no longer exist.', "text-bg-danger")
|
||||
}
|
||||
});
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
// This magic js codes enables anchor links to work with bootstrap tabs
|
||||
// Taken from https://stackoverflow.com/a/9393768/11317151 (and edited, like a lot)
|
||||
|
||||
const FLAG_supports_new_data_loader = true;
|
||||
|
||||
// Also update on location change
|
||||
window.addEventListener(
|
||||
'hashchange',
|
||||
@ -29,9 +31,9 @@ function primeCreateNew() {
|
||||
const form = document.getElementById('storageUnitModalForm');
|
||||
const form2 = document.getElementById('storageLocationModalForm');
|
||||
document.getElementById('createNewLocationSelection').disabled = false;
|
||||
document.getElementById('storageUnitModalLocationSelectText').innerText= "Select or create a new location.";
|
||||
document.getElementById('storageUnitModalLabel').innerText = "Create new storage unit";
|
||||
document.getElementById('storageLocationModalTitle').innerText = "Create new storage location";
|
||||
document.getElementById('storageUnitModalLocationSelectText').innerText = 'Select or create a new location.';
|
||||
document.getElementById('storageUnitModalLabel').innerText = 'Create new storage unit';
|
||||
document.getElementById('storageLocationModalTitle').innerText = 'Create new storage location';
|
||||
form.setAttribute('method', 'POST');
|
||||
form2.setAttribute('method', 'POST');
|
||||
return true;
|
||||
@ -42,25 +44,25 @@ function primeEdit() {
|
||||
const form2 = document.getElementById('storageLocationModalForm');
|
||||
// Disable create new location
|
||||
document.getElementById('createNewLocationSelection').disabled = true;
|
||||
document.getElementById('storageUnitModalLocationSelectText').innerText= "While editing you can only select already existing locations. Use the settings to create new ones.";
|
||||
document.getElementById('storageUnitModalLabel').innerText = "Edit a storage unit";
|
||||
document.getElementById('storageLocationModalTitle').innerText = "Edit a storage location"
|
||||
document.getElementById('storageUnitModalLocationSelect').value = 1
|
||||
handleSelector()
|
||||
document.getElementById('storageUnitModalLocationSelectText').innerText = 'While editing you can only select already existing locations. Use the settings to create new ones.';
|
||||
document.getElementById('storageUnitModalLabel').innerText = 'Edit a storage unit';
|
||||
document.getElementById('storageLocationModalTitle').innerText = 'Edit a storage location';
|
||||
document.getElementById('storageUnitModalLocationSelect').selectedIndex = 1;
|
||||
handleSelector();
|
||||
form.setAttribute('method', 'PATCH');
|
||||
form2.setAttribute('method', 'PATCH');
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleSelector(){
|
||||
const selector = document.getElementById('storageUnitModalLocationSelect')
|
||||
function handleSelector() {
|
||||
const selector = document.getElementById('storageUnitModalLocationSelect');
|
||||
const value = selector.options[selector.selectedIndex].value;
|
||||
if(value == "META_CREATENEW") {
|
||||
$('#storageUnitModalContactInfoCreator').removeClass('d-none')
|
||||
$('.requireOnCreate').attr('required', true)
|
||||
if (value == 'META_CREATENEW') {
|
||||
$('#storageUnitModalContactInfoCreator').removeClass('d-none');
|
||||
$('.requireOnCreate').attr('required', true);
|
||||
} else {
|
||||
$('#storageUnitModalContactInfoCreator').addClass('d-none')
|
||||
$('.requireOnCreate').attr('required', false)
|
||||
$('#storageUnitModalContactInfoCreator').addClass('d-none');
|
||||
$('.requireOnCreate').attr('required', false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,9 +70,7 @@ function getDataForEdit(id) {
|
||||
$.ajax({
|
||||
type: 'get',
|
||||
url: `/api/v1/storageUnits?id=${id}`,
|
||||
success: function (data) {
|
||||
const result = JSON.parse(data);
|
||||
|
||||
success: function (result) {
|
||||
// Get elements inside the editCategoryModal
|
||||
const modal_unitName = document.getElementById('storageUnitModalName');
|
||||
const modal_unitLocation = document.getElementById('storageUnitModalLocationSelect');
|
||||
@ -81,15 +81,13 @@ function getDataForEdit(id) {
|
||||
modal_unitId.value = result.id;
|
||||
|
||||
// Select the correct location from the select based on the value of the option
|
||||
for(var i, j = 0; i = modal_unitLocation.options[j]; j++) {
|
||||
if(i.value == result.contactInfoId) {
|
||||
console.log("Found it");
|
||||
for (var i, j = 0; (i = modal_unitLocation.options[j]); j++) {
|
||||
if (i.value == result.contactInfoId) {
|
||||
console.log('Found it');
|
||||
modal_unitLocation.selectedIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
error: function (data) {
|
||||
console.log('!!!! ERROR !!!!', data);
|
||||
@ -97,19 +95,16 @@ function getDataForEdit(id) {
|
||||
$('.loader-overlay').removeClass('active');
|
||||
// Close the modal
|
||||
$('.modal').modal('hide');
|
||||
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The storage unit does no longer exist.', "text-bg-danger")
|
||||
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The storage unit does no longer exist.', 'text-bg-danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getDataForEditLoc(id) {
|
||||
$.ajax({
|
||||
type: 'get',
|
||||
url: `/api/v1/storageLocations?id=${id}`,
|
||||
success: function (data) {
|
||||
const result = JSON.parse(data);
|
||||
|
||||
success: function (result) {
|
||||
// Get elements inside the editCategoryModal
|
||||
const modal_locationName = document.getElementById('storageLocationModalName');
|
||||
const modal_locationUnitSel = document.getElementById('storageLocationModalUnit');
|
||||
@ -120,15 +115,13 @@ function getDataForEditLoc(id) {
|
||||
modal_locationId.value = result.id;
|
||||
|
||||
// Select the correct location from the select based on the value of the option
|
||||
for(var i, j = 0; i = modal_locationUnitSel.options[j]; j++) {
|
||||
if(i.value == result.storageUnit) {
|
||||
console.log("Found it");
|
||||
for (var i, j = 0; (i = modal_locationUnitSel.options[j]); j++) {
|
||||
if (i.value == result.storageUnitId) {
|
||||
console.log('Found it');
|
||||
modal_locationUnitSel.selectedIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
error: function (data) {
|
||||
console.log('!!!! ERROR !!!!', data);
|
||||
@ -136,9 +129,89 @@ function getDataForEditLoc(id) {
|
||||
$('.loader-overlay').removeClass('active');
|
||||
// Close the modal
|
||||
$('.modal').modal('hide');
|
||||
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The storage unit does no longer exist.', "text-bg-danger")
|
||||
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The storage unit does no longer exist.', 'text-bg-danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleSelector()
|
||||
const itemList = $('#itemList');
|
||||
const itemListUnit = $('#itemListUnit');
|
||||
|
||||
// itemList.empty();
|
||||
itemListUnit.bootstrapTable({ url: '/api/v1/storageUnits', search: true, showRefresh: true, responseHandler: dataResponseHandlerUnit, sidePagination: 'server', serverSort: true, silentSort: false });
|
||||
itemList.bootstrapTable({ url: '/api/v1/storageLocations', search: true, showRefresh: true, responseHandler: dataResponseHandler, sidePagination: 'server', serverSort: true, silentSort: false });
|
||||
setTimeout(() => {
|
||||
activateTooltips();
|
||||
}, 1000);
|
||||
|
||||
function loadPageData() {
|
||||
// itemList.empty();
|
||||
itemList.bootstrapTable('refresh');
|
||||
itemListUnit.bootstrapTable('refresh');
|
||||
|
||||
setTimeout(() => {
|
||||
$(".tooltip").tooltip("hide");
|
||||
activateTooltips();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function dataResponseHandler(json) {
|
||||
// console.log(json)
|
||||
totalNotFiltered = json.totalNotFiltered;
|
||||
total = json.total;
|
||||
json = json.items;
|
||||
json.forEach((item) => {
|
||||
colorStatus = '';
|
||||
item.actions = `
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageLocationModal" onclick="primeEdit(); getDataForEditLoc('${item.id}')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('${item.id}','storageLocations','Storage Location')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>`;
|
||||
if (item.storageUnit == null) {
|
||||
item.storageUnit = '<i>No storage unit assigned</i>';
|
||||
} else {
|
||||
item.storageUnit = item.storageUnit.name;
|
||||
console.log(item.storageUnit);
|
||||
}
|
||||
|
||||
// item.SKU = `<p data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: ${item.id}">${item.SKU}</p>`
|
||||
});
|
||||
///// --------------------------------- /////
|
||||
setTimeout(() => {
|
||||
activateTooltips();
|
||||
}, 200);
|
||||
return { rows: json, total: total, totalNotFiltered: totalNotFiltered, totalRows: total };
|
||||
}
|
||||
|
||||
function dataResponseHandlerUnit(json) {
|
||||
// console.log(json)
|
||||
totalNotFiltered = json.totalNotFiltered;
|
||||
total = json.total;
|
||||
json = json.items;
|
||||
json.forEach((item) => {
|
||||
colorStatus = '';
|
||||
item.actions = `
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#storageUnitModal" onclick="primeEdit(); getDataForEdit('${item.id}')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('${item.id}','storageUnits','Storage Unit')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>`;
|
||||
if (item.contactInfo == null) {
|
||||
item.address = '<i>No address assigned</i>';
|
||||
} else {
|
||||
item.address = `${item.contactInfo.street} ${item.contactInfo.houseNumber}, ${item.contactInfo.city} ${item.contactInfo.country}`;
|
||||
}
|
||||
|
||||
// item.SKU = `<p data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: ${item.id}">${item.SKU}</p>`
|
||||
});
|
||||
///// --------------------------------- /////
|
||||
setTimeout(() => {
|
||||
activateTooltips();
|
||||
}, 200);
|
||||
return { rows: json, total: total, totalNotFiltered: totalNotFiltered, totalRows: total };
|
||||
}
|
||||
|
||||
handleSelector();
|
||||
|
@ -1,5 +1,15 @@
|
||||
var amountOfForms = $('.frontendForm').length;
|
||||
|
||||
function isNewDataLoaderAvailable() {
|
||||
try {
|
||||
return FLAG_supports_new_data_loader;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$('.frontendForm').each(function () {
|
||||
// TODO Handle empty strings as null or undefined, not as ''
|
||||
$(this).on('submit', function (e) {
|
||||
e.preventDefault(); // Prevent the form from submitting via the browser
|
||||
|
||||
@ -25,7 +35,12 @@ $('.frontendForm').each(function () {
|
||||
// Clear all fields
|
||||
form.find('input, textarea').val('');
|
||||
// Create toast
|
||||
createNewToast('<i class="bi bi-check2"></i> Changes saved successfully.', "text-bg-success")
|
||||
if(isNewDataLoaderAvailable()) {
|
||||
createNewToast('<i class="bi bi-check2"></i> Changes saved successfully.', "text-bg-success", undefined, false)
|
||||
} else {
|
||||
createNewToast('<i class="bi bi-check2"></i> Changes saved successfully.', "text-bg-success")
|
||||
}
|
||||
|
||||
},
|
||||
error: function (data) {
|
||||
console.log('error');
|
||||
@ -56,7 +71,12 @@ function deleteEntryNxt(id, route, name) {
|
||||
data: { id: id },
|
||||
success: function (data) {
|
||||
$('#staticBackdrop').modal('hide');
|
||||
createNewToast(`<i class="bi bi-check2"></i> ${name} deleted successfully.`, "text-bg-success")
|
||||
if(isNewDataLoaderAvailable()) {
|
||||
createNewToast(`<i class="bi bi-check2"></i> ${name} deleted successfully.`, "text-bg-success", undefined, false)
|
||||
} else {
|
||||
createNewToast(`<i class="bi bi-check2"></i> ${name} deleted successfully.`, "text-bg-success")
|
||||
}
|
||||
|
||||
|
||||
confetti({
|
||||
spread: 360,
|
||||
@ -93,8 +113,7 @@ function preFillDeleteModalNxt(id, route, name, requestIdent='id') {
|
||||
$.ajax({
|
||||
type: 'get',
|
||||
url: `/api/v1/${route}?${requestIdent}=${id}`,
|
||||
success: function (data) {
|
||||
const result = JSON.parse(data);
|
||||
success: function (result) {
|
||||
|
||||
// Get elements inside the editCategoryModal
|
||||
const modal_categoryName = document.getElementById('deleteNamePlaceholder');
|
||||
@ -108,7 +127,7 @@ function preFillDeleteModalNxt(id, route, name, requestIdent='id') {
|
||||
document.getElementById('deleteNamePlaceholder').innerText = 'Deleted';
|
||||
|
||||
$('#staticBackdrop').modal('hide');
|
||||
createNewToast(`<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The ${name} does no longer exist.', "text-bg-danger`)
|
||||
createNewToast(`<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. The ${name} does no longer exist.`, `text-bg-danger`)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
30
static/js/handleColorMode.js
Normal file
30
static/js/handleColorMode.js
Normal file
@ -0,0 +1,30 @@
|
||||
// Listen for changes in the prefers-color-scheme media query and update the "data-bs-theme" attribute on the <html> element.
|
||||
|
||||
// TODO: Probably migrate theme mode storage to api.
|
||||
function updateColorMode() {
|
||||
const currentTheme = localStorage.getItem('bs.theme') ?? 'auto';
|
||||
const isDark = currentTheme === 'dark';
|
||||
const isLight = currentTheme === 'light';
|
||||
|
||||
const prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
|
||||
if (currentTheme === 'auto') {
|
||||
if (prefersLight) {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
}
|
||||
} else if (isDark) {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||
} else if (isLight) {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
}
|
||||
}
|
||||
|
||||
(function () {
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mql.addEventListener('change', () => {
|
||||
updateColorMode();
|
||||
});
|
||||
updateColorMode();
|
||||
})();
|
@ -1,18 +1,21 @@
|
||||
const trinagles = $('.dropdownIndicator');
|
||||
//const containers = $('');
|
||||
console.log(`Found ${trinagles.length} triangles`)
|
||||
|
||||
|
||||
trinagles.each(function () {
|
||||
var target = $(this.dataset.refTarget);
|
||||
var triTar = $(this);
|
||||
// Apply rotate if target is open
|
||||
if (target.hasClass('show')) {
|
||||
$(this).addClass('rotate');
|
||||
}
|
||||
|
||||
target.on('show.bs.collapse', function () {
|
||||
//$(this).parent.addClass('rotate');
|
||||
$(this).parent().find('.dropdownIndicator').addClass('rotate');
|
||||
console.log($(this).parent().find('.dropdownIndicator'));
|
||||
console.log('show');
|
||||
$(triTar).addClass('rotate');
|
||||
$(triTar).removeClass('derotate');
|
||||
});
|
||||
target.on('hide.bs.collapse', function () {
|
||||
//$(this).parent.removeClass('rotate');
|
||||
$(this).parent().find('.dropdownIndicator').removeClass('rotate');
|
||||
console.log('hide');
|
||||
$(triTar).removeClass('rotate');
|
||||
$(triTar).addClass('derotate');
|
||||
});
|
||||
// bootstrap.Collapse.getOrCreateInstance(document.querySelector(this.dataset.refTarget))
|
||||
});
|
||||
|
65
static/js/itemPageHandler.js
Normal file
65
static/js/itemPageHandler.js
Normal file
@ -0,0 +1,65 @@
|
||||
const FLAG_supports_new_data_loader = true;
|
||||
|
||||
/**
|
||||
* Should we ever implement items in items, have a look at this:
|
||||
* https://examples.bootstrap-table.com/index.html?extensions/treegrid.html#extensions/treegrid.html
|
||||
*/
|
||||
|
||||
// Inital thing
|
||||
const itemList = $('#itemList');
|
||||
itemList.bootstrapTable({ url: '/api/v1/items', search: true, showRefresh: true, responseHandler: dataResponseHandler, sidePagination: 'server', serverSort: true, silentSort: false });
|
||||
setTimeout(() => {
|
||||
activateTooltips();
|
||||
}, 1000);
|
||||
|
||||
function loadPageData() {
|
||||
itemList.bootstrapTable('refresh')
|
||||
setTimeout(() => {
|
||||
$(".tooltip").tooltip("hide");
|
||||
activateTooltips();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function dataResponseHandler(json) {
|
||||
// console.log(json)
|
||||
totalNotFiltered = json.totalNotFiltered;
|
||||
total = json.total;
|
||||
json = json.items;
|
||||
json.forEach((item) => {
|
||||
colorStatus = '';
|
||||
if(item.SKU == null) item.SKU = '<i>No SKU assigned</i>';
|
||||
switch (item.status) {
|
||||
case 'normal':
|
||||
colorStatus = 'success';
|
||||
break;
|
||||
case 'stolen':
|
||||
colorStatus = 'danger';
|
||||
break;
|
||||
case 'lost':
|
||||
colorStatus = 'warning';
|
||||
break;
|
||||
case 'borrowed':
|
||||
colorStatus = 'info';
|
||||
break;
|
||||
default:
|
||||
colorStatus = 'secondary';
|
||||
}
|
||||
item.status = `<span class="badge text-bg-${colorStatus}">${item.status}</span>`;
|
||||
item.actions = `
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#itemModifyModal" onclick="primeEdit(); getDataForEdit('${item.id}')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('${item.id}','items','Item')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>`
|
||||
item.SKU = `<p data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: ${item.id}">${item.SKU}</p>`
|
||||
});
|
||||
///// --------------------------------- /////
|
||||
setTimeout(() => {
|
||||
activateTooltips();
|
||||
}, 200);
|
||||
return {"rows": json, total: total, totalNotFiltered: totalNotFiltered, totalRows: total};
|
||||
|
||||
}
|
||||
|
||||
loadPageData()
|
@ -1,8 +0,0 @@
|
||||
function normalizeToast(){
|
||||
console.warn("Something is using the deprecated function normalizeToast(). Please use createNewToast() instead.")
|
||||
$('#generalToast').removeClass('text-bg-primary');
|
||||
$('#generalToast').removeClass('text-bg-success');
|
||||
$('#generalToast').removeClass('text-bg-danger');
|
||||
$('#generalToast').removeClass('text-bg-warning');
|
||||
$('#generalToast').removeClass('text-bg-info');
|
||||
}
|
@ -1,25 +1,112 @@
|
||||
document.getElementById("SearchBox").addEventListener("keyup", handleSearchChange);
|
||||
const autocompleteBox = document.getElementById("autocomplete-items");
|
||||
autocompleteBox.style.display = "none";
|
||||
document.getElementById('SearchBoxInput').addEventListener('keyup', handleSearchChange);
|
||||
document.getElementById('searchForm').addEventListener('submit', handleSearchSubmit);
|
||||
document.addEventListener('keyup', handleHotKey)
|
||||
const autocompleteBox = document.getElementById('autocompletBody');
|
||||
autocompleteBox.style.display = 'none';
|
||||
|
||||
currentBestGuessCommand = '';
|
||||
|
||||
function handleSearchChange(e) {
|
||||
console.log(e.target.value);
|
||||
// document.getElementById("SearchBox").setAttribute("data-bs-content", "Search results will show up here soon")
|
||||
|
||||
// return; // No you won't. I'm not done yet.
|
||||
// Check if known prefix is used (either > or #)
|
||||
if(e.target.value != "" ) {
|
||||
autocompleteBox.style.display = "block";
|
||||
autocompleteBox.innerHTML = "Search results will show up here soon <br> Trust me <br> Results";
|
||||
if (e.target.value != '') {
|
||||
autocompleteBox.style.display = 'block';
|
||||
autocompleteBox.innerHTML = 'Search results will show up here soon <br> Trust me <br> Results';
|
||||
} else {
|
||||
autocompleteBox.style.display = "none";
|
||||
autocompleteBox.style.display = 'none';
|
||||
}
|
||||
|
||||
if (e.target.value[0] == ">") {
|
||||
autocompleteBox.innerHTML = "Start typing to search for commands <br> >goto items";
|
||||
if(e.target.value == ">goto items") {
|
||||
autocompleteBox.innerHTML = "<a href='/allItems'>Goto Items</a>";
|
||||
if (e.target.value[0] == '>') {
|
||||
// List of valid routes
|
||||
urlList = {
|
||||
items: { url: '/items?page=1', alias: ['item'] },
|
||||
'storage locations': { url: '/manage/storages', alias: ['locations', 'storage'] },
|
||||
'storage units': { url: '/manage/storages#storage-unit-tab', alias: ['units'] },
|
||||
categories: { url: '/manage/categories', alias: ['category'] }
|
||||
};
|
||||
autocompleteBox.innerHTML = 'Start typing to search for commands <br> >goto items';
|
||||
const args = e.target.value.split(' ');
|
||||
console.log(args);
|
||||
if (args.length > 1) {
|
||||
if (args[0] == '>goto' || args[0] == '>g') {
|
||||
console.log('Handling >goto');
|
||||
autocompleteBox.innerHTML = 'Start typing to search for commands <br>' + Object.keys(urlList).join('<br>') + '<br>';
|
||||
if (args.length >= 2) {
|
||||
console.log("Autocomplete for 'goto' command with " + args[1] + " as the second argument")
|
||||
// Check if the second argument matches the urlList or any of its aliases
|
||||
for (const [key, value] of Object.entries(urlList)) {
|
||||
console.log(key, value)
|
||||
if (args[1] == key || value.alias.includes(args[1])) {
|
||||
// Match found
|
||||
console.log('Match found');
|
||||
autocompleteBox.innerHTML = `Go to <a href="${value.url}">${key}</a>`;
|
||||
currentBestGuessCommand = "open;" + value.url;
|
||||
break;
|
||||
} else {
|
||||
currentBestGuessCommand = '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (e.target.value[0] == "#") {
|
||||
} else if (e.target.value[0] == '#') {
|
||||
// Search for SKU
|
||||
autocompleteBox.innerHTML = "Start typing to search for items by SKU";
|
||||
const searchedSKU = e.target.value.substring(1);
|
||||
if(searchedSKU == '') {
|
||||
autocompleteBox.innerHTML = 'Start typing to search for commands <br> #SKU';
|
||||
return;
|
||||
}
|
||||
const baseURI = window.location.origin; // move to new fancy route
|
||||
const url = baseURI + '/api/v1/search/sku?sku=' + searchedSKU;
|
||||
|
||||
|
||||
$.ajax({
|
||||
type: 'get',
|
||||
url: url,
|
||||
success: function (result) {
|
||||
let htmlResult = ""
|
||||
result.forEach(element => {
|
||||
console.log(element);
|
||||
htmlResult += `<a href="/s/${element.SKU}">${element.name}</a><br>`
|
||||
});
|
||||
|
||||
autocompleteBox.innerHTML = htmlResult;
|
||||
},
|
||||
error: function (data) {
|
||||
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong while searching...', "text-bg-danger", autoHideTime = 3000, autoReload = false)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Search for name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearchSubmit(e) {
|
||||
console.log('Search submitted');
|
||||
if(currentBestGuessCommand != '') {
|
||||
console.log('Submitting command ' + currentBestGuessCommand);
|
||||
cmdArgs = currentBestGuessCommand.split(';');
|
||||
if(cmdArgs[0] == 'open') {
|
||||
// Open the url in the current tab
|
||||
setTimeout(() => {
|
||||
|
||||
window.location.replace(cmdArgs[1]);
|
||||
}, 200);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleHotKey(e) {
|
||||
// If c is pressed, focus on the search box
|
||||
if(e.key == 'c' && e.altKey && e.ctrlKey) {
|
||||
// Show search_modal modal
|
||||
bootstrap.Modal.getOrCreateInstance($('#search_modal')).show()
|
||||
document.getElementById('SearchBoxInput').focus();
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,76 @@
|
||||
const currentToasts = [];
|
||||
currentToasts = [];
|
||||
var forceSkipReload = false;
|
||||
forceSkipReload = localStorage.getItem('forceSkipReload') === 'true';
|
||||
if(forceSkipReload) {
|
||||
setTimeout(() => {
|
||||
createNewToast('Auto reload still disabled, click version number to reenable.', 'text-bg-warning', 3000, false);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function createNewToast(message, colorSelector, autoHideTime = 3000, autoReload = true){
|
||||
/**
|
||||
* Generic function to create a new toast
|
||||
* @param {String} message The message to be displayed
|
||||
* @param {String} colorSelector The bootstrap color selector class, can be one of the following: text-bg-primary, text-bg-success, text-bg-danger, text-bg-warning, text-bg-info
|
||||
* @param {Number} autoHideTime The time in milliseconds to auto hide the toast, default is 3000
|
||||
* @param {Boolean} autoReload Should the page reload after the toast is hidden, default is true (for compatibility with old code)
|
||||
* @returns {String} The id of the created toast, format: toast-<number>
|
||||
*/
|
||||
function createNewToast(message, colorSelector, autoHideTime = 1500, autoReload = true) {
|
||||
const targetContainer = document.getElementById('toastMainController');
|
||||
const masterToast = document.getElementById('masterToast');
|
||||
const newToast = masterToast.cloneNode(true);
|
||||
newToast.classList.add(colorSelector)
|
||||
newToast.classList.add(colorSelector);
|
||||
newToast.id = `toast-${currentToasts.length}`;
|
||||
console.log(newToast.childNodes[1]);
|
||||
newToast.childNodes[1].childNodes[1].innerHTML = message;
|
||||
targetContainer.appendChild(newToast);
|
||||
currentToasts.push(newToast);
|
||||
$(newToast).toast('show');
|
||||
try {
|
||||
loadPageData();
|
||||
} catch (error) {
|
||||
console.debug("Page does not support new data loading.")
|
||||
}
|
||||
setTimeout(() => {
|
||||
destroyToast(newToast.id);
|
||||
if(autoReload){
|
||||
if (autoReload && !forceSkipReload) {
|
||||
location.reload();
|
||||
}
|
||||
}, autoHideTime);
|
||||
return newToast.id;
|
||||
}
|
||||
|
||||
function destroyToast(id){
|
||||
/**
|
||||
* Generic function to destroy a toast
|
||||
* @param {String} id The id of the toast to destroy
|
||||
*/
|
||||
function destroyToast(id) {
|
||||
const targetContainer = document.getElementById('toastMainController');
|
||||
const targetToast = document.getElementById(id);
|
||||
targetContainer.removeChild(targetToast);
|
||||
currentToasts.splice(currentToasts.indexOf(targetToast), 1);
|
||||
}
|
||||
|
||||
// Moved here
|
||||
function normalizeToast() {
|
||||
console.warn('Something is using the deprecated function normalizeToast(). Please use createNewToast() instead.');
|
||||
$('#generalToast').removeClass('text-bg-primary');
|
||||
$('#generalToast').removeClass('text-bg-success');
|
||||
$('#generalToast').removeClass('text-bg-danger');
|
||||
$('#generalToast').removeClass('text-bg-warning');
|
||||
$('#generalToast').removeClass('text-bg-info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to handle the "secret" function to globally disable auto reload
|
||||
*/
|
||||
function toggleAutoReload() {
|
||||
forceSkipReload = !forceSkipReload;
|
||||
if(forceSkipReload) {
|
||||
createNewToast('Auto reload disabled', 'text-bg-warning', 1500, false);
|
||||
} else {
|
||||
createNewToast('Auto reload enabled', 'text-bg-success', 1500, false);
|
||||
}
|
||||
// Store the value in local storage
|
||||
localStorage.setItem('forceSkipReload', forceSkipReload);
|
||||
}
|
BIN
static/logo.png
Normal file
BIN
static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
1
static/logo/Design_Logo_Black.svg
Normal file
1
static/logo/Design_Logo_Black.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 25 KiB |
1
static/logo/Design_Logo_white.svg
Normal file
1
static/logo/Design_Logo_white.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 25 KiB |
1
static/logo/Design_icon.svg
Normal file
1
static/logo/Design_icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
35
tools/generate.js
Normal file
35
tools/generate.js
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* This file can be used to load-test the application
|
||||
*/
|
||||
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
// Temporary file to generate demo data for the inventory
|
||||
const inventoryItems = [];
|
||||
|
||||
// Function to add an item to the inventory using a simple function
|
||||
function addInventoryItem(name, amount, manufacturer, category, sku) {
|
||||
const item = {
|
||||
name: name,
|
||||
amount: amount,
|
||||
manufacturer: manufacturer,
|
||||
category: category,
|
||||
sku: sku
|
||||
};
|
||||
|
||||
inventoryItems.push(item);
|
||||
}
|
||||
|
||||
// Loop to generate 2000 items
|
||||
for (let i = 1; i <= 1024; i++) {
|
||||
const itemName = `Item ${i}`;
|
||||
const itemAmount = Math.floor(Math.random() * 100) + 1;
|
||||
const itemManufacturer = `Manufacturer ${i}`;
|
||||
const itemCategory = `Category ${i}`;
|
||||
const itemSKU = `SKU-${i}`;
|
||||
|
||||
addInventoryItem(itemName, itemAmount, itemManufacturer, itemCategory, itemSKU);
|
||||
}
|
||||
|
||||
// Save the generated data to a file
|
||||
writeFileSync('demoData.json', JSON.stringify(inventoryItems))
|
Reference in New Issue
Block a user