Compare commits
137 Commits
27a0235a06
...
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 | |||
524feee54d | |||
e6238e80e8 | |||
e98e46e1a2 | |||
bd9f629690 | |||
04b5bd60f2 | |||
90fc8068a0 | |||
533bc1744d | |||
64c14db183 | |||
e4295493f2 | |||
b514e81764 | |||
e82d16af3e | |||
2285b3dd33 | |||
532b7b9fe2 | |||
3819d007a5 | |||
d7abadf6a6 | |||
d51b063918 | |||
0c7c294823 | |||
4bfd71f09f | |||
6afdb4fcdd | |||
3b9813a680 | |||
b5314cb552 | |||
9a05743cb3 | |||
38cd29943f | |||
b1a73ebd4a | |||
9c4eb3200e | |||
ee61e94853 | |||
1f2eb78333 | |||
6344134a9e | |||
75a5580366 | |||
55ae8d4c8f | |||
d7794da74b | |||
739ab7b9ee | |||
5ade4891b5 | |||
7a26537903 | |||
3569c9ed4b | |||
96853debe2 | |||
c6fb84759f | |||
7cfca9abac | |||
43ef7fd395 | |||
4ce9dae7ab |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ node_modules
|
|||||||
config.json
|
config.json
|
||||||
dist
|
dist
|
||||||
docs
|
docs
|
||||||
|
demoData.json
|
@ -10,7 +10,7 @@
|
|||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
"insertPragma": false,
|
"insertPragma": false,
|
||||||
"jsxSingleQuote": false,
|
"jsxSingleQuote": false,
|
||||||
"printWidth": 200,
|
"printWidth": 225,
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "preserve",
|
||||||
"quoteProps": "as-needed",
|
"quoteProps": "as-needed",
|
||||||
"requirePragma": false,
|
"requirePragma": false,
|
||||||
|
6
.vsls.json
Normal file
6
.vsls.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/vsls",
|
||||||
|
"gitignore": "hide",
|
||||||
|
"excludeFiles": ["!node_modules"],
|
||||||
|
"hideFiles": ["node_modules"]
|
||||||
|
}
|
16
README.md
16
README.md
@ -1,4 +1,16 @@
|
|||||||
# Assetflow
|
# Assetflow
|
||||||
|
|
||||||
## Theme?
|
Assetflow is an inventory management solution targeted at the event industry.
|
||||||
https://bootswatch.com/darkly/
|
|
||||||
|
## Formats and conventions (WiP)
|
||||||
|
|
||||||
|
SKU
|
||||||
|
Stock Keeping Unit
|
||||||
|
|
||||||
|
LocID
|
||||||
|
LocID_Regal16_Fach7
|
||||||
|
|
||||||
|
StorageLocation_LocID_Regal16_Fach7
|
||||||
|
|
||||||
|
|
||||||
|
Please also reference our wiki at https://project-name-here.atlassian.net/wiki/spaces/AFLOW/overview
|
@ -4,12 +4,15 @@
|
|||||||
"/bootstrap/dist/css/bootstrap.min.css",
|
"/bootstrap/dist/css/bootstrap.min.css",
|
||||||
"/bootstrap/dist/js/bootstrap.bundle.min.js",
|
"/bootstrap/dist/js/bootstrap.bundle.min.js",
|
||||||
"/jquery/dist/jquery.min.js",
|
"/jquery/dist/jquery.min.js",
|
||||||
"/darkreader/darkreader.js",
|
|
||||||
"/bootstrap-icons/font/fonts/bootstrap-icons.woff2",
|
"/bootstrap-icons/font/fonts/bootstrap-icons.woff2",
|
||||||
"/bootstrap/dist/css/bootstrap.min.css.map",
|
"/bootstrap/dist/css/bootstrap.min.css.map",
|
||||||
"/@popperjs/core/dist/umd/popper.min.js",
|
"/@popperjs/core/dist/umd/popper.min.js",
|
||||||
"/@popperjs/core/dist/umd/popper.min.js.map",
|
"/@popperjs/core/dist/umd/popper.min.js.map",
|
||||||
"/bootstrap/dist/js/bootstrap.bundle.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",
|
||||||
|
"/bootstrap-table/dist/bootstrap-table.min.js",
|
||||||
|
"/bootstrap-table/dist/bootstrap-table.min.css"
|
||||||
],
|
],
|
||||||
"debugMode": false
|
"debugMode": false
|
||||||
}
|
}
|
||||||
|
1741
package-lock.json
generated
1741
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -18,25 +18,37 @@
|
|||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.7",
|
"@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": "^5.3.0-alpha3",
|
||||||
"bootstrap-icons": "^1.10.5",
|
"bootstrap-icons": "^1.10.5",
|
||||||
|
"bootstrap-table": "^1.22.1",
|
||||||
"csv": "^6.2.11",
|
"csv": "^6.2.11",
|
||||||
"eta": "^2.0.1",
|
"eta": "^2.0.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-fileupload": "^1.4.0",
|
"express-fileupload": "^1.4.0",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
"jquery": "^3.6.4",
|
"jquery": "^3.6.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"prisma": "^4.13.0",
|
"passport": "^0.6.0",
|
||||||
"signale": "^1.4.0"
|
"passport-local": "^1.0.0",
|
||||||
|
"signale": "^1.4.0",
|
||||||
|
"tsparticles-confetti": "^2.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@loancrate/prisma-schema-parser": "^2.0.0",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/express-fileupload": "^1.4.1",
|
"@types/express-fileupload": "^1.4.1",
|
||||||
|
"@types/express-session": "^1.17.7",
|
||||||
"@types/lodash": "^4.14.194",
|
"@types/lodash": "^4.14.194",
|
||||||
|
"@types/passport": "^1.0.12",
|
||||||
|
"@types/passport-local": "^1.0.35",
|
||||||
"@types/signale": "^1.4.4",
|
"@types/signale": "^1.4.4",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"prisma": "^4.14.1",
|
||||||
"prisma-dbml-generator": "^0.10.0",
|
"prisma-dbml-generator": "^0.10.0",
|
||||||
"prisma-docs-generator": "^0.7.0",
|
"prisma-docs-generator": "^0.7.0",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
|
@ -13,64 +13,116 @@ datasource db {
|
|||||||
// https://github.com/pantharshit00/prisma-docs-generator
|
// https://github.com/pantharshit00/prisma-docs-generator
|
||||||
generator docs {
|
generator docs {
|
||||||
provider = "node node_modules/prisma-docs-generator"
|
provider = "node node_modules/prisma-docs-generator"
|
||||||
output = "../docs"
|
output = "../docs"
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/notiz-dev/prisma-dbml-generator
|
// https://github.com/notiz-dev/prisma-dbml-generator
|
||||||
// Viewer: https://dbdiagram.io/d
|
// Viewer: https://dbdiagram.io/d
|
||||||
generator dbml {
|
generator dbml {
|
||||||
provider = "prisma-dbml-generator"
|
provider = "prisma-dbml-generator"
|
||||||
output = "../docs"
|
output = "../docs"
|
||||||
outputName = "schema.dbml"
|
outputName = "schema.dbml"
|
||||||
projectName = "AssetFlow"
|
projectName = "AssetFlow"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Status {
|
enum itemStatus {
|
||||||
normal
|
normal
|
||||||
borrowed
|
borrowed
|
||||||
stolen
|
stolen
|
||||||
lost
|
lost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// comments and descriptions -> @db.VarChar(2048)
|
||||||
model Item {
|
model Item {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @unique @default(autoincrement())
|
||||||
SKU String? @unique
|
SKU String? @unique
|
||||||
Amount Int
|
amount Int @default(1)
|
||||||
Comment String?
|
name String
|
||||||
name String
|
comment String? @db.VarChar(2048)
|
||||||
manufacturer String
|
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)?
|
||||||
category Category @relation(fields: [categoryId], references: [id])
|
|
||||||
categoryId Int
|
contactInfo contactInfo? @relation(fields: [contactInfoId], references: [id])
|
||||||
status Status
|
contactInfoId Int?
|
||||||
StorageLocation StorageLocation? @relation(fields: [storageLocationId], references: [id])
|
|
||||||
|
manufacturer String?
|
||||||
|
|
||||||
|
category itemCategory? @relation(fields: [categoryId], references: [id])
|
||||||
|
categoryId Int?
|
||||||
|
|
||||||
|
contents Item[] @relation("items") /// Item beinhaltet..
|
||||||
|
baseItem Item[] @relation("items") /// Item zugehörig zu
|
||||||
|
|
||||||
|
storageLocation StorageLocation? @relation(fields: [storageLocationId], references: [id])
|
||||||
storageLocationId Int?
|
storageLocationId Int?
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
importedBy String?
|
updatedAt DateTime @updatedAt
|
||||||
|
createdBy String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model StorageLocation {
|
model StorageLocation {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String @unique /// This is our LocationID for external use prefixed with: '%StorageUnit%_'
|
||||||
storageBuilding StorageBuilding? @relation(fields: [storageBuildingId], references: [id])
|
storageUnit StorageUnit? @relation(fields: [storageUnitId], references: [id])
|
||||||
storageBuildingId Int?
|
storageUnitId Int?
|
||||||
Item Item[]
|
Item Item[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model StorageBuilding {
|
/// A StorageUnit is the base and can hold multiple StorageLocations.
|
||||||
id Int @id @default(autoincrement())
|
model StorageUnit {
|
||||||
name String
|
id Int @id @default(autoincrement())
|
||||||
street String
|
name String @unique
|
||||||
houseNumber String
|
|
||||||
zipCode String
|
contactInfo contactInfo @relation(fields: [contactInfoId], references: [id])
|
||||||
city String
|
contactInfoId Int
|
||||||
country String
|
|
||||||
StorageLocation StorageLocation[]
|
StorageLocation StorageLocation[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Category {
|
model itemCategory {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
description String?
|
description String? @db.VarChar(2048)
|
||||||
Item Item[]
|
Item Item[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model contactInfo {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
type contactType @default(person)
|
||||||
|
name String?
|
||||||
|
lastName String?
|
||||||
|
street String
|
||||||
|
houseNumber String
|
||||||
|
zipCode String
|
||||||
|
city String
|
||||||
|
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
|
||||||
|
partner
|
||||||
|
enemy
|
||||||
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import _ from 'lodash';
|
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.
|
* This class is responsible to save/edit config files.
|
||||||
@ -13,7 +14,8 @@ export type configObject = Record<any, any>
|
|||||||
export default class config {
|
export default class config {
|
||||||
#configPath: string;
|
#configPath: string;
|
||||||
//global = {[key: string] : string}
|
//global = {[key: string] : string}
|
||||||
global: configObject
|
global: configObject;
|
||||||
|
replaceSecrets: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of config.
|
* Creates an instance of config.
|
||||||
@ -22,9 +24,10 @@ export default class config {
|
|||||||
* @param {string} configPath Path to config file.
|
* @param {string} configPath Path to config file.
|
||||||
* @param {object} configPreset Default config object with default values.
|
* @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.#configPath = configPath;
|
||||||
this.global = configPreset;
|
this.global = configPreset;
|
||||||
|
this.replaceSecrets = replaceSecrets;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read config
|
// Read config
|
||||||
@ -35,7 +38,15 @@ export default class config {
|
|||||||
// Save config.
|
// Save config.
|
||||||
this.save_config();
|
this.save_config();
|
||||||
} catch (err) {
|
} 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() {
|
save_config() {
|
||||||
try {
|
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));
|
fs.writeFileSync(this.#configPath, JSON.stringify(this.global, null, 8));
|
||||||
} catch (err) {
|
} 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;
|
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 ****
|
**** Example ****
|
||||||
|
|
||||||
const default_config = {
|
import ConfigHandlerNG from './assets/configHandlerNG.js';
|
||||||
token: 'your-token-goes-here',
|
|
||||||
clientId: '',
|
|
||||||
devserverID: '',
|
|
||||||
devmode: true
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 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('Base Config:');
|
||||||
console.log(config.global);
|
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.global.NewKey = 'ThisIsANewKey!'
|
||||||
config.save_config()
|
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('Complete Config:');
|
||||||
console.log(config.global);
|
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}" }`);
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
<%~ E.includeFile("partials/head.eta.html", {"title": "Dashboard"}) %> <%~ E.includeFile("partials/controls.eta.html", {"active": "AllItems"}) %>
|
|
||||||
|
|
||||||
<h1>All items</h1>
|
|
||||||
<div class="container">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">#</th>
|
|
||||||
<th scope="col">Name</th>
|
|
||||||
<th scope="col">Status</th>
|
|
||||||
<th scope="col">SKU</th>
|
|
||||||
<th scope="col">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% it.items.forEach(function(user){ %>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><%= user.id %></th>
|
|
||||||
<td><%= user.name %></td>
|
|
||||||
<td><span class="badge text-bg-success"><%= user.status %></span></td>
|
|
||||||
<td><%= user.SKU %></td>
|
|
||||||
<td><a href="#" class="btn btn-primary">Edit</a></td>
|
|
||||||
</tr>
|
|
||||||
<% }) %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%~ E.includeFile("partials/controlsFoot.eta.html") %> <%~ E.includeFile("partials/foot.eta.html") %>
|
|
36
src/frontend/auth/login.eta.html
Normal file
36
src/frontend/auth/login.eta.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<%~ E.includeFile("../partials/head.eta.html", {"title": "Login"}) %>
|
||||||
|
<link href="/css/login.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<div class="background text-center">
|
||||||
|
<div class="row align-items-start">
|
||||||
|
<div class="col-9"></div>
|
||||||
|
<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"}) %>
|
<%~ 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="container text-center">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="card col m-2">
|
<div class="card col m-2">
|
||||||
@ -23,28 +36,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-light" role="alert">A new version is available. <a href="#" class="alert-link">Click here to update</a></div>
|
|
||||||
|
|
||||||
<h2>Recent items</h2>
|
<h2>Recent items</h2>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">#</th>
|
<th scope="col">SKU</th>
|
||||||
<th scope="col">Name</th>
|
<th scope="col">Name</th>
|
||||||
<th scope="col">Status</th>
|
<th scope="col">Status</th>
|
||||||
<th scope="col">SKU</th>
|
<!--<th scope="col">Actions</th>-->
|
||||||
<th scope="col">Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% it.recents.forEach(function(user){ %>
|
<% it.recents.forEach(function(user){ %>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row"><%= user.id %></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>
|
<td><%= user.name %></td>
|
||||||
|
<% if(user.status == "normal") { %>
|
||||||
|
|
||||||
<td><span class="badge text-bg-success"><%= user.status %></span></td>
|
<td><span class="badge text-bg-success"><%= user.status %></span></td>
|
||||||
<td><%= user.SKU %></td>
|
<% } else if(user.status == "stolen") { %>
|
||||||
<td><a href="#" class="btn btn-primary">Edit</a></td>
|
<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>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
8
src/frontend/errors/400.eta.html
Normal file
8
src/frontend/errors/400.eta.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<%~ E.includeFile("../partials/head.eta.html", {"title": "Error 400"}) %> <%~ E.includeFile("../partials/controls.eta.html", {"active": "error_400"}) %>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="bi bi-bug-fill " style="font-size: 5rem"></i>
|
||||||
|
<p class="mt-2 mb-0 fs-4">Bad Request!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%~ E.includeFile("../partials/controlsFoot.eta.html") %> <%~ E.includeFile("../partials/foot.eta.html") %>
|
@ -3,11 +3,11 @@
|
|||||||
<i class="bi bi-database-fill-slash" style="font-size: 5rem"></i>
|
<i class="bi bi-database-fill-slash" style="font-size: 5rem"></i>
|
||||||
<p class="mt-2 mb-0">There seems to be an error with the database</p>
|
<p class="mt-2 mb-0">There seems to be an error with the database</p>
|
||||||
<p>
|
<p>
|
||||||
<a class="btn btn-primary" data-bs-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="collapseExample"> Get the error </a>
|
<a class="btn btn-secondary" data-bs-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="collapseExample"> Get the error </a>
|
||||||
</p>
|
</p>
|
||||||
<div class="collapse" id="collapseExample">
|
<div class="collapse" id="collapseExample">
|
||||||
<div class="card card-body">
|
<div class="card card-body text-start">
|
||||||
<pre><code><%= error %></code></pre>
|
<pre><code><%= it.error %></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
<%~ E.includeFile("../partials/head.eta.html", {"title": "Importer - CSV" }) %>
|
|
||||||
<%~ E.includeFile("../partials/controls.eta.html", {"active": "CSV_import" }) %>
|
|
||||||
|
|
||||||
<h1>Import A CSV File</h1>
|
|
||||||
Upload a CSV file to import into the database. The CSV file must have the following columns:
|
|
||||||
<ul>
|
|
||||||
<li> Name</li>
|
|
||||||
<li> Amount</li>
|
|
||||||
<li> Manufacturer</li>
|
|
||||||
<li> Category</li>
|
|
||||||
</ul>
|
|
||||||
The following columns are optional:
|
|
||||||
<ul>
|
|
||||||
<li> SKU</li>
|
|
||||||
<li> Comment</li>
|
|
||||||
<li> StorageLocation (import currently not supported)</li>
|
|
||||||
</ul>
|
|
||||||
<form method="post" encType="multipart/form-data">
|
|
||||||
<label for="formFile" class="form-label">CSV Inventory File Upload</label>
|
|
||||||
<input class="form-control" type="file" id="formFile" name="formFile"><br>
|
|
||||||
|
|
||||||
<input type="submit" value="Run import" class="btn btn-primary">
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
|
||||||
<%~ E.includeFile("../partials/controlsFoot.eta.html") %>
|
|
||||||
<%~ E.includeFile("../partials/foot.eta.html") %>
|
|
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") %>
|
117
src/frontend/items.eta.html
Normal file
117
src/frontend/items.eta.html
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<%~ 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>
|
||||||
|
|
||||||
|
<h1>Items</h1>
|
||||||
|
<div class="container">
|
||||||
|
<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" 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) { %>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center">No items found</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<% } %>
|
||||||
|
</table>
|
||||||
|
</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") %>
|
56
src/frontend/manage/categoryManager.eta.html
Normal file
56
src/frontend/manage/categoryManager.eta.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<%~ 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">
|
||||||
|
<!-- Create new category button -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="/settings/category/new" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#editCategoryModal" onclick="primeCreateNew()">Create new category</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="editCategoryModal" tabindex="-1" aria-labelledby="editCategoryModal" aria-hidden="true">
|
||||||
|
<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>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form class="frontendForm" method="patch" data-target="/api/v1/categories" id="CategoryModalForm">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editCategoryModalName" class="form-label">Name</label>
|
||||||
|
<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" maxlength="2048" name="description" />
|
||||||
|
<div id="editCategoryModalDescText" class="form-text">Optional</div>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="editCategoryModalId" 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>
|
||||||
|
|
||||||
|
<!-- Table with all categories -->
|
||||||
|
<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" 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>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script src="/js/editCategory.js"></script>
|
||||||
|
|
||||||
|
<%~ E.includeFile("../partials/controlsFoot.eta.html") %> <%~ E.includeFile("../partials/foot.eta.html") %>
|
24
src/frontend/manage/imports/csvImport.eta.html
Normal file
24
src/frontend/manage/imports/csvImport.eta.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<%~ E.includeFile("../../partials/head.eta.html", {"title": "Importer - CSV" }) %> <%~ E.includeFile("../../partials/controls.eta.html", {"active": "SETT_IMPORT_CSV" }) %>
|
||||||
|
|
||||||
|
<h1>CSV Import</h1>
|
||||||
|
Upload a CSV file to import into the database. The CSV file must have the following columns:
|
||||||
|
<ul>
|
||||||
|
<li>Name</li>
|
||||||
|
<li>Amount</li>
|
||||||
|
<li>Manufacturer</li>
|
||||||
|
<li>Category</li>
|
||||||
|
</ul>
|
||||||
|
The following columns are optional:
|
||||||
|
<ul>
|
||||||
|
<li>SKU</li>
|
||||||
|
<li>Comment</li>
|
||||||
|
<li>StorageLocation (import currently not supported)</li>
|
||||||
|
</ul>
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<label for="formFile" class="form-label">CSV Inventory File Upload</label>
|
||||||
|
<input class="form-control" type="file" id="formFile" name="formFile" /><br />
|
||||||
|
|
||||||
|
<input type="submit" value="Run import" class="btn btn-primary" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<%~ E.includeFile("../../partials/controlsFoot.eta.html") %> <%~ E.includeFile("../../partials/foot.eta.html") %>
|
45
src/frontend/manage/imports/jsonImport.eta.html
Normal file
45
src/frontend/manage/imports/jsonImport.eta.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<%~ E.includeFile("../../partials/head.eta.html", {"title": "Importer - JSON" }) %> <%~ E.includeFile("../../partials/controls.eta.html", {"active": "SETT_IMPORT_JSON" }) %>
|
||||||
|
|
||||||
|
<h1>JSON Import</h1>
|
||||||
|
Upload a JSON file to import into the database. The JSON file must have the following columns:
|
||||||
|
<ul>
|
||||||
|
<li>name</li>
|
||||||
|
<li>amount</li>
|
||||||
|
<li>manufacturer</li>
|
||||||
|
<li>category</li>
|
||||||
|
</ul>
|
||||||
|
The following columns are optional:
|
||||||
|
<ul>
|
||||||
|
<li>sku</li>
|
||||||
|
<li>comment</li>
|
||||||
|
<li>StorageLocation (import currently not supported)</li>
|
||||||
|
</ul>
|
||||||
|
It should be formated as a list of objects, like this:
|
||||||
|
<pre>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Item 1",
|
||||||
|
"amount": 1,
|
||||||
|
"manufacturer": "Manufacturer 1",
|
||||||
|
"category": "Category 1",
|
||||||
|
"sku": "SKU 1",
|
||||||
|
"comment": "Comment 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Item 2",
|
||||||
|
"amount": 2,
|
||||||
|
"manufacturer": "Manufacturer 2",
|
||||||
|
"category": "Category 2",
|
||||||
|
"sku": "SKU 2",
|
||||||
|
"comment": "Comment 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</pre>
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<label for="formFile" class="form-label">JSON Inventory File Upload</label>
|
||||||
|
<input class="form-control" type="file" id="formFile" name="formFile" /><br />
|
||||||
|
|
||||||
|
<input type="submit" value="Run import" class="btn btn-primary" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<%~ E.includeFile("../../partials/controlsFoot.eta.html") %> <%~ E.includeFile("../../partials/foot.eta.html") %>
|
45
src/frontend/manage/startpage.eta.html
Normal file
45
src/frontend/manage/startpage.eta.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<%~ 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" 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">
|
||||||
|
<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>
|
||||||
|
<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") %>
|
192
src/frontend/manage/storageManager.eta.html
Normal file
192
src/frontend/manage/storageManager.eta.html
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<%~ E.includeFile("../partials/head.eta.html", {"title": "Settings - Storage Manager"}) %> <%~ E.includeFile("../partials/controls.eta.html", {"active": "SETT_STORE"}) %> <%~ E.includeFile("../partials/deleteModal.eta.html")
|
||||||
|
%>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal fade" id="storageLocationModal" tabindex="-1" aria-labelledby="storageLocationModal" aria-hidden="true">
|
||||||
|
<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>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form id="storageLocationModalForm" class="frontendForm" method="post" data-target="/api/v1/storageLocations">
|
||||||
|
<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" maxlength="128" required />
|
||||||
|
<div id="storageLocationModalNameText" class="form-text">This name should be unqiue.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="storageLocationModalUnit" class="form-label">Select a storage unit</label>
|
||||||
|
<select class="form-select" id="storageLocationModalUnit" name="storageUnitId" required>
|
||||||
|
<option value="undefined"><i>Do not assign a storage unit</i></option>
|
||||||
|
<% it.storUnits.forEach(function(storageunits){ %>
|
||||||
|
<option value="<%= storageunits.id %>"><%= storageunits.name %></option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!--<input type="text" class="form-control" id="createNewCategoryModalDescription" name="description" />-->
|
||||||
|
<div id="storageLocationModalUnitText" class="form-text">You have to create a storage unit beforehand.</div>
|
||||||
|
<input type="hidden" id="storageLocationModalIdHidden" name="id" />
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
<!-- loader overlay -->
|
||||||
|
<div class="loader-overlay">
|
||||||
|
<div class="loader">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="storageUnitModal" tabindex="-1" aria-labelledby="storageUnitModal" aria-hidden="true">
|
||||||
|
<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>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form class="frontendForm" method="post" data-target="/api/v1/storageUnits" id="storageUnitModalForm">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="storageUnitModalName" class="form-label">Name</label>
|
||||||
|
<input type="text" class="form-control" id="storageUnitModalName" name="name" required />
|
||||||
|
<div id="storageUnitModalNameText" class="form-text">This name should be unqiue.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="storageUnitModalLocationSelect" class="form-label">Storage Location</label>
|
||||||
|
<select class="form-select" id="storageUnitModalLocationSelect" name="locationId" onchange="handleSelector()" required>
|
||||||
|
<option value="META_CREATENEW" id="createNewLocationSelection">➕ Create new location</option>
|
||||||
|
<% it.address.forEach(function(address){ %>
|
||||||
|
<option value="<%= address.id %>"><%= address.street %> <%= address.houseNumber %>, <%= address.city %> <%= address.country %></option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!--<input type="text" class="form-control" id="storageUnitModalLocationSelect" name="select" required />-->
|
||||||
|
<div id="storageUnitModalLocationSelectText" class="form-text">Select or create a new address.</div>
|
||||||
|
<input type="hidden" id="storageUnitModalLocationSelectHidden" name="id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="storageUnitModalContactInfoCreator" class="d-none">
|
||||||
|
<hr />
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="storageUnitModalStreet" class="form-label">Street</label>
|
||||||
|
<input type="text" class="form-control requireOnCreate" id="storageUnitModalStreet" name="street" />
|
||||||
|
<div id="storageUnitModalStreetText" class="form-text">Example Avenue</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="storageUnitModalHouseNumber" class="form-label">Housenumber</label>
|
||||||
|
<input type="text" class="form-control requireOnCreate" id="storageUnitModalHouseNumber" name="houseNumber" />
|
||||||
|
<div id="storageUnitModalHouseNumberText" class="form-text">6a</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="storageUnitModalzipcode" class="form-label">Zipcode</label>
|
||||||
|
<input type="text" class="form-control requireOnCreate" id="storageUnitModalzipcode" name="zipCode" />
|
||||||
|
<div id="storageUnitModalzipcodeText" class="form-text">123456</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="storageUnitModalCity" class="form-label">City</label>
|
||||||
|
<input type="text" class="form-control requireOnCreate" id="storageUnitModalCity" name="city" />
|
||||||
|
<div id="storageUnitModalCityText" class="form-text">Berlin</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="storageUnitModalCountry" class="form-label">Country</label>
|
||||||
|
<input type="text" class="form-control requireOnCreate" id="storageUnitModalCountry" name="country" />
|
||||||
|
<div id="storageUnitModalCountryText" class="form-text">Germany</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
<!-- loader overlay -->
|
||||||
|
<div class="loader-overlay">
|
||||||
|
<div class="loader">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Storages</h1>
|
||||||
|
<ul class="nav nav-underline" id="storageTabList" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="storage-loc-tab" data-bs-toggle="tab" data-bs-target="#storage-loc-tab-pane" type="button" role="tab" aria-controls="storage-loc-tab-pane" aria-selected="true">
|
||||||
|
<i class="bi bi-bookshelf"></i> Storage Location
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="storage-unit-tab" data-bs-toggle="tab" data-bs-target="#storage-unit-tab-pane" type="button" role="tab" aria-controls="storage-unit-tab-pane" aria-selected="false">
|
||||||
|
<i class="bi bi-buildings"></i> Storage Unit
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content" id="storageTabListContent">
|
||||||
|
<div class="tab-pane fade show active" id="storage-loc-tab-pane" role="tabpanel" aria-labelledby="storage-loc-tab-pane" tabindex="0">
|
||||||
|
<br />
|
||||||
|
A storage location is a place where you can store your items. It can be a room, a shelf or a box.
|
||||||
|
<br />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="/settings/category/new" class="btn btn-primary" onclick="primeCreateNew()" data-bs-toggle="modal" data-bs-target="#storageLocationModal"
|
||||||
|
><i class="bi bi-plus-lg"></i> Create new Location</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" 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>
|
||||||
|
|
||||||
|
</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 />
|
||||||
|
A storage unit is a physical place, like a warehouse. This contains an address and a name.
|
||||||
|
<br />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<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" 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" 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>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/js/editStorages.js"></script>
|
||||||
|
|
||||||
|
<%~ E.includeFile("../partials/controlsFoot.eta.html") %> <%~ E.includeFile("../partials/foot.eta.html") %>
|
@ -1,99 +1,227 @@
|
|||||||
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
|
<nav class="navbar navbar-expand-lg bg-body-tertiary sticky-top navShadow" style="z-index: 999">
|
||||||
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 test-white-50" href="#">AssetFlow</a>
|
<div class="container-fluid">
|
||||||
<button
|
<a class="navbar-brand user-select-none ms-2" style="cursor: default" href="/">
|
||||||
class="navbar-toggler position-absolute d-md-none collapsed"
|
<img alt="AssetFlow Logo" draggable="false" class="me-2 headLogo" src="/logo/Design_icon.svg"/> AssetFlow</a>
|
||||||
type="button"
|
<button
|
||||||
data-bs-toggle="collapse"
|
class="navbar-toggler position-absolute d-md-none collapsed"
|
||||||
data-bs-target="#sidebarMenu"
|
type="button"
|
||||||
aria-controls="sidebarMenu"
|
data-bs-toggle="collapse"
|
||||||
aria-expanded="false"
|
data-bs-target="#sidebarMenu"
|
||||||
aria-label="Toggle navigation">
|
aria-controls="sidebarMenu"
|
||||||
<span class="navbar-toggler-icon"></span>
|
aria-expanded="false"
|
||||||
</button>
|
aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
<input class="form-control form-control-dark w-100 bg-secondary" type="text" placeholder="Search" aria-label="Search" id="SearchBox" />
|
</button>
|
||||||
<div class="autocomplete-items bg-secondary w-75 border-primary me-5 p-2" id="autocomplete-items" style="left: 16.7%">
|
<div class="" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0"></ul>
|
||||||
</div>
|
<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>
|
||||||
<div class="navbar-nav">
|
</form>
|
||||||
<div class="nav-item text-nowrap">
|
|
||||||
<a class="nav-link px-3" id="logoutButton">Sign out</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</nav>
|
||||||
|
<div class="modal" id="search_modal">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="toast-container position-fixed bottom-0 end-0 p-3 ">
|
<div class="modal-content">
|
||||||
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
<div class="modal-header">
|
||||||
<div class="toast-header">
|
<h1 class="modal-title fs-5" id="modalSearchBar">
|
||||||
<strong class="me-auto">Notification</strong>
|
<div class="input-group mb-3">
|
||||||
<small>Just now</small>
|
<form id="searchForm">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
<input type="text" id="SearchBoxInput" class="form-control focus" placeholder="Start typing to search..." aria-label="Search" autocomplete="off">
|
||||||
</div>
|
</form>
|
||||||
<div class="toast-body" id="toastText">
|
</div>
|
||||||
The button you just pressed is very useless.
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
let texti = 0;
|
|
||||||
alltexts = ["Nope, still useless", "Stop pressing me!", "There are NO USERS!", "Please stop.", "PLEASE!"];
|
|
||||||
const toastLiveExample = document.getElementById('liveToast')
|
|
||||||
const logoutButton = document.getElementById('logoutButton')
|
|
||||||
logoutButton.addEventListener('click', () => {
|
|
||||||
toastFunction();
|
|
||||||
texti++;
|
|
||||||
if(texti >= alltexts.length) texti = 0;
|
|
||||||
})
|
|
||||||
|
|
||||||
function toastFunction() {
|
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" id="masterToast" style="z-index: 2000">
|
||||||
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
|
<div class="d-flex">
|
||||||
toastBootstrap.show()
|
<div class="toast-body">Hello, world! This is a toast message.</div>
|
||||||
setTimeout(function(){ toastBootstrap.hide(); document.getElementById("toastText").innerHTML = alltexts[texti] }, 3000);
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
}
|
</div>
|
||||||
</script>
|
</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">
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
|
<nav id="sidebarMenu" class="col-md-2 col-lg-2 d-md-block sidebar collapse">
|
||||||
<div class="position-sticky pt-3">
|
<div class="position-sticky pt-3">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= it.active == 'Dashboard' ? 'active' : ''%>" aria-current="page" href="/"> <i class="bi bi-house"></i> Dashboard </a>
|
<a class="nav-link <%= it.active == 'Dashboard' ? 'active' : ''%>" aria-current="page" href="/"> <i class="bi bi-house"></i> <strong>Dashboard</strong> </a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= it.active == 'AllItems' ? 'active' : ''%>"" href="/allItems"> <i class="bi bi-list-ul"></i> All Items </a>
|
<a class="nav-link <%= it.active == 'Items' ? 'active' : ''%>" href="/items"> <i class="bi bi-list-ul"></i> Items </a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="nav-item">
|
||||||
|
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>" href="#"> Products </a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>"" href="#"> Products </a>
|
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>" href="#"> Customers </a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>"" href="#"> Customers </a>
|
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>" href="#"> Reports </a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>"" href="#"> Reports </a>
|
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>" href="#"> Integrations </a>
|
||||||
</li>
|
</li> -->
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>"" href="#"> Integrations </a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||||
<span>Importer</span>
|
<a href="/projects/" class="nav-link"
|
||||||
<a class="link-secondary" href="#" aria-label="Add a new report"> </a>
|
>Projects<span class="badge rounded-pill bg-primary" >
|
||||||
|
Alpha
|
||||||
|
<span class="visually-hidden">Alpha feature</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= it.active == 'CSV_import' ? 'active' : ''%>" href="/import/csv"><i class="bi bi-filetype-csv"></i> CSV Import </a>
|
<a class="nav-link <%= it.active == 'PROJ_HOME' ? 'active' : ''%>" href="/projects/"><i class="bi bi-kanban"></i> Manage Projects </a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= it.active == 'json_import' ? 'active' : ''%>" href="/import/json"> <i class="bi bi-filetype-json"></i>JSON Import </a>
|
<a class="nav-link <%= it.active == 'PROJ_LIST' ? 'active' : ''%>" href="/projects/lists"><i class="bi bi-card-checklist"></i> Packaging Lists </a>
|
||||||
</li>
|
</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" 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"
|
||||||
|
><i class="bi bi-box-seam"></i> Manage storages
|
||||||
|
<span
|
||||||
|
class="<%= it.active == 'SETT_STORE' ? 'active' : ''%>"
|
||||||
|
type="button"
|
||||||
|
onclick="return false"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#collapseSettingsStorages"
|
||||||
|
aria-expanded="<%= it.active == 'SETT_STORE' ? 'true' : 'false'%>"
|
||||||
|
aria-controls="collapseSettingsStorages">
|
||||||
|
<i class="bi bi-caret-left-fill dropdownIndicator" data-ref-target="#collapseSettingsStorages"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="collapse <%= it.active == 'SETT_STORE' ? 'show' : ''%>" id="collapseSettingsStorages">
|
||||||
|
<a class="nav-link ms-4" href="/manage/storages#storage-loc-tab"><i class="bi bi-bookshelf"></i> Manage locations </a>
|
||||||
|
<a class="nav-link ms-4" href="/manage/storages#storage-unit-tab"><i class="bi bi-buildings"></i> Manage units </a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= it.active == 'SETT_CAT' ? 'active' : ''%>" href="/manage/categories"><i class="bi bi-tag"></i> Manage categories </a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<a class="nav-link">
|
||||||
|
<span
|
||||||
|
class="<%= it.active == 'SETT_IMPORT' ? 'active' : ''%>"
|
||||||
|
type="button"
|
||||||
|
onclick="return false"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#collapseSettingsImport"
|
||||||
|
aria-expanded="<%= it.active.includes('SETT_IMPORT') ? 'true' : 'false'%>"
|
||||||
|
aria-controls="collapseSettingsImport">
|
||||||
|
<i class="bi bi-box-seam"></i> Import
|
||||||
|
<i class="bi bi-caret-left-fill dropdownIndicator" data-ref-target="#collapseSettingsImport"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="collapse <%= it.active.includes('SETT_IMPORT') ? 'show' : ''%>" id="collapseSettingsImport">
|
||||||
|
<a class="nav-link ms-4 <%= it.active == 'SETT_IMPORT_CSV' ? 'active' : ''%>" href="/manage/import/csv"><i class="bi bi-filetype-csv"></i> CSV Import </a>
|
||||||
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Align the mode picker at the bottom of the navbar -->
|
||||||
|
|
||||||
|
<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 />
|
||||||
|
<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" />
|
||||||
|
<label class="btn btn-secondary" for="mode_dark"><i class="bi bi-moon"></i></label>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const modeFromStorage = localStorage.getItem('bs.theme') ?? 'auto';
|
||||||
|
const modeLight = document.getElementById('mode_light');
|
||||||
|
const modeAuto = document.getElementById('mode_auto');
|
||||||
|
const modeDark = document.getElementById('mode_dark');
|
||||||
|
if (modeFromStorage === 'light') {
|
||||||
|
modeLight.checked = true;
|
||||||
|
} else if (modeFromStorage === 'dark') {
|
||||||
|
modeDark.checked = true;
|
||||||
|
} else {
|
||||||
|
modeAuto.checked = true;
|
||||||
|
}
|
||||||
|
modeLight.addEventListener('click', () => {
|
||||||
|
localStorage.setItem('bs.theme', 'light');
|
||||||
|
updateColorMode();
|
||||||
|
//document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||||
|
});
|
||||||
|
modeAuto.addEventListener('click', () => {
|
||||||
|
localStorage.setItem('bs.theme', 'auto');
|
||||||
|
updateColorMode();
|
||||||
|
//document.documentElement.setAttribute('data-bs-theme', 'auto');
|
||||||
|
});
|
||||||
|
modeDark.addEventListener('click', () => {
|
||||||
|
localStorage.setItem('bs.theme', 'dark');
|
||||||
|
updateColorMode();
|
||||||
|
//document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</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>
|
</nav>
|
||||||
|
|
||||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4" style="min-height: 100%;">
|
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4" style="min-height: 100%">
|
||||||
|
<!-- The main tag needs to be left open! -->
|
||||||
|
@ -1,4 +1,22 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/js/searchBox.js"></script>
|
<script src="/js/searchBox.js"></script>
|
||||||
|
<script src="/js/handleSidebarTriangles.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>
|
||||||
|
16
src/frontend/partials/deleteModal.eta.html
Normal file
16
src/frontend/partials/deleteModal.eta.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!-- 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 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"><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>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancle</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="deleteActionBtn"><i class="bi bi-trash"></i> Yes, delete.</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,5 +1,2 @@
|
|||||||
<script src="/static/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="/static/@popperjs/core/dist/umd/popper.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -8,13 +8,20 @@
|
|||||||
<title>AssetFlow - <%= it.title %></title>
|
<title>AssetFlow - <%= it.title %></title>
|
||||||
<meta name="author" content="[Project-Name-Here]" />
|
<meta name="author" content="[Project-Name-Here]" />
|
||||||
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/logo/Design_icon.svg" type="image/svg+xml" />
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
||||||
|
|
||||||
|
<script src="/js/handleColorMode.js"></script>
|
||||||
<script src="/static/jquery/dist/jquery.min.js"></script>
|
<script src="/static/jquery/dist/jquery.min.js"></script>
|
||||||
<link href="/static/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
<script src="/js/toastHandler.js"></script>
|
||||||
<link href="/static/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
<script src="/js/confettiHeader.js"></script>
|
||||||
<link href="/css/dashboard.css" rel="stylesheet">
|
<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>
|
||||||
|
<link rel="stylesheet" href="/static/bootstrap-table/dist/bootstrap-table.min.css">
|
||||||
|
<script src="/static/bootstrap-table/dist/bootstrap-table.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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") %>
|
@ -2,11 +2,21 @@
|
|||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h2><%= it.name %></h2>
|
<h2><%= it.name %></h2>
|
||||||
<p><strong>Category:</strong> <%= it.category%></p>
|
|
||||||
<p><strong>Amount:</strong> <%= it.Amount %></p>
|
|
||||||
<p><strong>SKU:</strong> <%= it.SKU %></p>
|
|
||||||
<p><strong>Comment:</strong> <%= it.comment %></p>
|
<p><strong>Comment:</strong> <%= it.comment %></p>
|
||||||
|
<p><strong>Category:</strong> <%= 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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<%~ E.includeFile("partials/foot.eta.html") %>
|
<%~ E.includeFile("partials/foot.eta.html") %>
|
||||||
|
110
src/index.ts
110
src/index.ts
@ -1,10 +1,17 @@
|
|||||||
import { Signale } from 'signale';
|
import { Signale } from 'signale';
|
||||||
import ConfigHandler from './assets/configHandler';
|
import ConfigHandler from './assets/configHandler';
|
||||||
import express, { Request, Response } from 'express';
|
import express, { NextFunction, Request, Response } from 'express';
|
||||||
import fileUpload from 'express-fileupload';
|
import fileUpload from 'express-fileupload';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { Status, Category } from '@prisma/client';
|
|
||||||
import * as eta from 'eta';
|
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';
|
||||||
|
import * as Tracing from '@sentry/tracing';
|
||||||
|
|
||||||
import routes from './routes/index.js';
|
import routes from './routes/index.js';
|
||||||
|
|
||||||
@ -16,24 +23,48 @@ const logger_settings = {
|
|||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
scope: 'Core',
|
scope: 'Core',
|
||||||
stream: process.stdout,
|
stream: process.stdout,
|
||||||
displayFilename: true
|
displayFilename: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const coreLogger = new Signale(logger_settings);
|
const coreLogger = new Signale(logger_settings);
|
||||||
export const log = {
|
export const log = {
|
||||||
core: coreLogger,
|
core: coreLogger,
|
||||||
db: coreLogger.scope('DB'),
|
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.
|
// 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',
|
db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE',
|
||||||
http_listen_address: '127.0.0.1',
|
http_listen_address: '127.0.0.1',
|
||||||
http_port: 3000,
|
http_port: 3000,
|
||||||
debug: false
|
sentry_dsn: 'https://ID@sentry.example.com/PROJECTID',
|
||||||
|
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({
|
export const prisma = new PrismaClient({
|
||||||
datasources: {
|
datasources: {
|
||||||
db: {
|
db: {
|
||||||
@ -43,18 +74,65 @@ export const prisma = new PrismaClient({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const app = express();
|
export const app = express();
|
||||||
app.set('x-powered-by', false);
|
Sentry.init({
|
||||||
app.set('strict routing', true);
|
dsn: config.global.sentry_dsn,
|
||||||
app.engine('html', eta.renderFile);
|
integrations: [
|
||||||
app.use(fileUpload());
|
// enable HTTP calls tracing
|
||||||
// Configure static https://expressjs.com/de/starter/static-files.html
|
new Sentry.Integrations.Http({ tracing: true }),
|
||||||
// app.use('/static', express.static('public'));
|
// enable Express.js middleware tracing
|
||||||
|
new Tracing.Integrations.Express({ app }),
|
||||||
|
// @ts-ignore
|
||||||
|
new Sentry.Integrations.Prisma({ prisma })
|
||||||
|
],
|
||||||
|
|
||||||
|
// Set tracesSampleRate to 1.0 to capture 100%
|
||||||
|
// of transactions for performance monitoring.
|
||||||
|
// We recommend adjusting this value in production
|
||||||
|
tracesSampleRate: config.global.debug ? 1.0 : 0.5,
|
||||||
|
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());
|
||||||
|
// TracingHandler creates a trace for every incoming request
|
||||||
|
app.use(Sentry.Handlers.tracingHandler());
|
||||||
|
|
||||||
|
app.set('x-powered-by', false);
|
||||||
|
app.engine('html', eta.renderFile);
|
||||||
|
|
||||||
|
// app.use(cors());
|
||||||
|
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(express.static(__path + '/static'));
|
||||||
/* app.use((req, res, next) => {
|
|
||||||
res.status(404).send("Sorry can't find that!");
|
|
||||||
}); */
|
|
||||||
//routes(app);
|
|
||||||
app.use(routes);
|
app.use(routes);
|
||||||
|
|
||||||
app.listen(config.global.http_port, config.global.http_listen_address, () => {
|
app.listen(config.global.http_port, config.global.http_listen_address, () => {
|
||||||
|
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,11 +1,11 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
// Route imports
|
// Route imports
|
||||||
import testRoute from './test.js';
|
import v1_routes from './v1/index.js';
|
||||||
|
|
||||||
// Router base is '/api'
|
// Router base is '/api'
|
||||||
const Router = express.Router();
|
const Router = express.Router({ strict: false });
|
||||||
|
|
||||||
Router.use('/test', testRoute);
|
Router.use('/v1', v1_routes);
|
||||||
|
|
||||||
export default Router;
|
export default Router;
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import express, { Request, Response } from 'express';
|
|
||||||
|
|
||||||
export default (req: Request, res: Response) => {
|
|
||||||
res.status(200).send('API Test Successful!');
|
|
||||||
};
|
|
234
src/routes/api/v1/categories.ts
Normal file
234
src/routes/api/v1/categories.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
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.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: {
|
||||||
|
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).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save data.
|
||||||
|
prisma.itemCategory
|
||||||
|
.create({
|
||||||
|
data: {
|
||||||
|
name: req.body.name,
|
||||||
|
description: req.body.description
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
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({ 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' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.itemCategory.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.itemCategory
|
||||||
|
.update({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: req.body.name,
|
||||||
|
description: req.body.description
|
||||||
|
},
|
||||||
|
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.itemCategory.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.itemCategory
|
||||||
|
.delete({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted category' });
|
||||||
|
})
|
||||||
|
.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 };
|
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 };
|
40
src/routes/api/v1/index.ts
Normal file
40
src/routes/api/v1/index.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
export default Router;
|
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 };
|
230
src/routes/api/v1/storageLocations.ts
Normal file
230
src/routes/api/v1/storageLocations.ts
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { prisma, __path, log } from '../../../index.js';
|
||||||
|
import { parseIntOrUndefined, parseDynamicSortBy, parseIntRelation } from '../../../assets/helper.js';
|
||||||
|
|
||||||
|
// Get storageLocation.
|
||||||
|
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: {
|
||||||
|
id: parseInt(req.query.id.toString())
|
||||||
|
},
|
||||||
|
// Get storageUnit from relation.
|
||||||
|
include: {
|
||||||
|
storageUnit: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((items) => {
|
||||||
|
if (items) {
|
||||||
|
res.status(200).json(items);
|
||||||
|
} else {
|
||||||
|
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation 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.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({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
|
||||||
|
} else {
|
||||||
|
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation 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 storageLocation.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// Create storageLocation with existing storageUnit.
|
||||||
|
prisma.storageLocation
|
||||||
|
.create({
|
||||||
|
data: {
|
||||||
|
name: req.body.name,
|
||||||
|
storageUnitId: parseInt(req.body.storageUnitId) || undefined
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
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({ 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
|
||||||
|
// 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).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 || !req.body.name || !req.body.storageUnitId) {
|
||||||
|
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the storageLocation id exists. If not return 410 Gone.
|
||||||
|
try {
|
||||||
|
const result = await prisma.storageLocation.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation 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.storageLocation
|
||||||
|
.update({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: req.body.name,
|
||||||
|
storageUnit: parseIntRelation(req.body.storageUnitId)
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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({ 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
|
||||||
|
// 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).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete storageLocation.
|
||||||
|
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.storageLocation.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageLocation 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.storageLocation
|
||||||
|
.delete({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted storageLocation' });
|
||||||
|
})
|
||||||
|
.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 };
|
296
src/routes/api/v1/storageUnits.ts
Normal file
296
src/routes/api/v1/storageUnits.ts
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
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.
|
||||||
|
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: {
|
||||||
|
id: parseInt(req.query.id.toString())
|
||||||
|
},
|
||||||
|
// Get contactInfo and StorageLocation from relation.
|
||||||
|
include: {
|
||||||
|
contactInfo: true,
|
||||||
|
StorageLocation: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((items) => {
|
||||||
|
if (items) {
|
||||||
|
res.status(200).json(items);
|
||||||
|
} else {
|
||||||
|
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit 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.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({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items });
|
||||||
|
} else {
|
||||||
|
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit 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 storageUnit.
|
||||||
|
function post(req: Request, res: Response) {
|
||||||
|
// If the frontend wants to create a StorageUnit with non-existing ContactInfo.
|
||||||
|
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).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create storageUnit and location.
|
||||||
|
prisma.storageUnit
|
||||||
|
.create({
|
||||||
|
data: {
|
||||||
|
name: req.body.name,
|
||||||
|
contactInfo: {
|
||||||
|
create: {
|
||||||
|
type: contactType.storageUnit,
|
||||||
|
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 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({ 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).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).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create storageUnit with existing location.
|
||||||
|
prisma.storageUnit
|
||||||
|
.create({
|
||||||
|
data: {
|
||||||
|
name: req.body.name,
|
||||||
|
contactInfo: {
|
||||||
|
connect: {
|
||||||
|
id: parseInt(req.body.locationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
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({ 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).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update storageUnit. -> Only existing contactInfo.
|
||||||
|
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).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the storageUnit id exists. If not return 410 Gone.
|
||||||
|
try {
|
||||||
|
const result = await prisma.storageUnit.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit 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' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the locationId(contactInfo) exists. If not return 410 Gone.
|
||||||
|
try {
|
||||||
|
const result = await prisma.contactInfo.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.locationId)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'locationId 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.storageUnit
|
||||||
|
.update({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: req.body.name,
|
||||||
|
contactInfo: {
|
||||||
|
connect: {
|
||||||
|
id: parseInt(req.body.locationId) // TODO: Rename to contactInfoId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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({ 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).json({ status: 'ERROR', errorcode: 'DB_ERROR', error: err, message: 'An error occurred during the database operation' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete storageUnit.
|
||||||
|
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.storageUnit.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
res.status(410).json({ status: 'ERROR', errorcode: 'NOT_EXISTING', message: 'storageUnit 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.storageUnit
|
||||||
|
.delete({
|
||||||
|
where: {
|
||||||
|
id: parseInt(req.body.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted storageUnit' });
|
||||||
|
})
|
||||||
|
.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 };
|
7
src/routes/api/v1/test.ts
Normal file
7
src/routes/api/v1/test.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
|
||||||
|
function get(req: Request, res: Response) {
|
||||||
|
res.status(200).send('API v1 Test Successful!');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { get };
|
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;
|
10
src/routes/auth/login.ts
Normal file
10
src/routes/auth/login.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import passport from 'passport';
|
||||||
|
|
||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import { prisma, __path, log } from '../../index.js';
|
||||||
|
|
||||||
|
function get(req: Request, res: Response) {
|
||||||
|
res.render(__path + '/src/frontend/auth/login.eta.html'); //, { items: items });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { get };
|
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,11 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
|
|
||||||
// Route imports
|
|
||||||
import setDemoData from './setDemoData.js';
|
|
||||||
|
|
||||||
// Router base is '/dev'
|
|
||||||
const Router = express.Router();
|
|
||||||
|
|
||||||
Router.use("/setDemoData", setDemoData)
|
|
||||||
|
|
||||||
export default Router;
|
|
@ -1,27 +0,0 @@
|
|||||||
import { Request, Response } from 'express';
|
|
||||||
import { prisma } from '../../index.js';
|
|
||||||
import { Status, Category } from '@prisma/client';
|
|
||||||
|
|
||||||
export default (req: Request, res: Response) => {
|
|
||||||
// fill database with demo data
|
|
||||||
|
|
||||||
/*
|
|
||||||
prisma.item
|
|
||||||
.create({
|
|
||||||
data: {
|
|
||||||
SKU: 'ee189749',
|
|
||||||
Amount: 1,
|
|
||||||
name: 'Test Item',
|
|
||||||
manufacturer: 'Test Manufacturer',
|
|
||||||
category: Category.Other,
|
|
||||||
status: Status.normal
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
res.send('Demo data added');
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
res.send('Error adding demo data: ' + err);
|
|
||||||
});*/
|
|
||||||
res.send('No data was added');
|
|
||||||
};
|
|
@ -2,12 +2,25 @@ import { Request, Response } from 'express';
|
|||||||
import { prisma, __path } from '../../index.js';
|
import { prisma, __path } from '../../index.js';
|
||||||
import * as Eta from 'eta';
|
import * as Eta from 'eta';
|
||||||
|
|
||||||
export default (req: Request, res: Response) => {
|
function get(req: Request, res: Response) {
|
||||||
// retrieve data from database using id from url
|
// Get data from database using sku from url.
|
||||||
prisma.item
|
prisma.item
|
||||||
.findFirst({
|
.findFirst({
|
||||||
where: {
|
where: {
|
||||||
SKU: req.params.id
|
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) => {
|
.then((item) => {
|
||||||
@ -19,4 +32,6 @@ export default (req: Request, res: Response) => {
|
|||||||
res.send('Item not found');
|
res.send('Item not found');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default { get };
|
||||||
|
@ -1,28 +1,25 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import { prisma, __path } from '../../index.js';
|
import { prisma, __path, log } from '../../index.js';
|
||||||
|
|
||||||
export default (req: Request, res: Response) => {
|
function get(req: Request, res: Response) {
|
||||||
// TODO: Fix it? Express behaves like fucking shit with routers and /. Do not ask about it or touch it. EVER! (But if you can fix it a PR is welcome!)
|
prisma.item
|
||||||
if (req.originalUrl !== '/') {
|
.findMany({
|
||||||
res.status(404).render(__path + '/src/frontend/errors/404.eta.html', { url: req.originalUrl });
|
orderBy: {
|
||||||
} else {
|
updatedAt: 'desc'
|
||||||
prisma.item
|
},
|
||||||
.findMany({
|
// Limit to 10 items
|
||||||
orderBy: {
|
take: 10
|
||||||
updatedAt: 'desc'
|
})
|
||||||
},
|
.then((items) => {
|
||||||
// Limit to 10 items
|
// Count amount of total items
|
||||||
take: 10
|
prisma.item.count().then((count) => {
|
||||||
})
|
res.render(__path + '/src/frontend/dashboard.eta.html', { recents: items, stats: { total: count } });
|
||||||
.then((items) => {
|
|
||||||
// Count amount of total items
|
|
||||||
prisma.item.count().then((count) => {
|
|
||||||
res.render(__path + '/src/frontend/dashboard.eta.html', { recents: items, stats: { total: count } });
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
};
|
.catch((err) => {
|
||||||
|
log.db.error(err);
|
||||||
|
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { get };
|
||||||
|
@ -1,102 +0,0 @@
|
|||||||
import express, { Request, Response } from 'express';
|
|
||||||
import { prisma, __path, log } from '../../../index.js';
|
|
||||||
import { UploadedFile } from 'express-fileupload';
|
|
||||||
import { parse, transform } from 'csv';
|
|
||||||
import { Status, Category, PrismaPromise } from '@prisma/client';
|
|
||||||
|
|
||||||
export default (req: Request, res: Response) => {
|
|
||||||
// Decide wether its post or get
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
// 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.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const file: UploadedFile = req.files.formFile as UploadedFile;
|
|
||||||
const csv = file.data.toString();
|
|
||||||
parse(csv, { columns: true }, function (err, records) {
|
|
||||||
if (err) {
|
|
||||||
res.send(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Find all categories and save them into a set
|
|
||||||
const categories = new Set<string>();
|
|
||||||
records.forEach((record: any) => {
|
|
||||||
categories.add(record.category);
|
|
||||||
});
|
|
||||||
// Remove categories that already exists in the database
|
|
||||||
prisma.category.findMany({
|
|
||||||
where: {
|
|
||||||
name: {
|
|
||||||
in: Array.from(categories)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).then((values) => {
|
|
||||||
values.forEach((value) => {
|
|
||||||
categories.delete(value.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const categoryPromises: PrismaPromise<Category>[] = [];
|
|
||||||
categories.forEach((category: string) => {
|
|
||||||
const promise = prisma.category.create({
|
|
||||||
data: {
|
|
||||||
name: category
|
|
||||||
}
|
|
||||||
})
|
|
||||||
categoryPromises.push(promise);
|
|
||||||
});
|
|
||||||
Promise.all(categoryPromises).then((values) => {
|
|
||||||
// Create items
|
|
||||||
const listOfPromises = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < records.length; i++) {
|
|
||||||
const record = records[i];
|
|
||||||
const promise = prisma.item.create({
|
|
||||||
data: {
|
|
||||||
name: record.name,
|
|
||||||
Amount: parseInt(record.amount),
|
|
||||||
Comment: record.comment,
|
|
||||||
category: {
|
|
||||||
connect: {
|
|
||||||
name: record.category
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SKU: record.sku,
|
|
||||||
manufacturer: record.manufacturer,
|
|
||||||
status: Status.normal,
|
|
||||||
importedBy: "CSV Import"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
listOfPromises.push(promise);
|
|
||||||
}
|
|
||||||
Promise.all(listOfPromises).then((values) => {
|
|
||||||
console.log(values);
|
|
||||||
res.send('ok');
|
|
||||||
}).catch((err) => {
|
|
||||||
res.send('failed to create items');
|
|
||||||
log.db.error(err);
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
}).catch((err) => {
|
|
||||||
// res.send('failed to create categories');
|
|
||||||
log.db.error(err);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}).catch((err) => {
|
|
||||||
res.send('failed to find categories');
|
|
||||||
log.db.error(err);
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Render page
|
|
||||||
res.render(__path + '/src/frontend/imports/csvImport.eta.html');
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
@ -2,19 +2,26 @@ import express from 'express';
|
|||||||
|
|
||||||
// Route imports
|
// Route imports
|
||||||
import skuRoute from './:id.js';
|
import skuRoute from './:id.js';
|
||||||
|
import skuRouteDash from './itemInfo.js'
|
||||||
import testRoute from './test.js';
|
import testRoute from './test.js';
|
||||||
import dashboardRoute from './dashboard.js';
|
import dashboardRoute from './dashboard.js';
|
||||||
import csvImportRoute from './import/csvImport.js';
|
import itemsRoute from './items.js';
|
||||||
import listAllItems from './listAllItems.js';
|
import manage_routes from './manage/index.js';
|
||||||
|
import project_routes from './projects/index.js';
|
||||||
|
|
||||||
// Router base is '/'
|
// Router base is '/'
|
||||||
const Router = express.Router({ strict: false });
|
const Router = express.Router({ strict: false });
|
||||||
|
|
||||||
|
Router.route('/test').get(testRoute.get);
|
||||||
|
Router.route('/items').get(itemsRoute.get);
|
||||||
|
|
||||||
Router.use('/test', testRoute);
|
Router.use('/projects', project_routes); // has to be before skuRoute
|
||||||
Router.use('/allItems', listAllItems)
|
Router.route('/:id(\\w{8})').get(skuRoute.get); // we should probably deprecate this
|
||||||
Router.use('/import/csv', csvImportRoute);
|
Router.route('/s/:id').get(skuRouteDash.get);
|
||||||
Router.use('/:id(\\w{8})', skuRoute);
|
|
||||||
Router.use('/', dashboardRoute);
|
Router.use('/manage', manage_routes);
|
||||||
|
|
||||||
|
|
||||||
|
Router.route('/').get(dashboardRoute.get);
|
||||||
|
|
||||||
export default Router;
|
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 };
|
22
src/routes/frontend/items.ts
Normal file
22
src/routes/frontend/items.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { prisma, __path, log } from '../../index.js';
|
||||||
|
|
||||||
|
async function get(req: Request, res: Response) {
|
||||||
|
prisma.item
|
||||||
|
.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) => {
|
||||||
|
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) => {
|
||||||
|
log.db.error(err);
|
||||||
|
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { get };
|
@ -1,15 +0,0 @@
|
|||||||
import express, { Request, Response } from 'express';
|
|
||||||
import { prisma, __path } from '../../index.js';
|
|
||||||
|
|
||||||
export default (req: Request, res: Response) => {
|
|
||||||
prisma.item
|
|
||||||
.findMany({})
|
|
||||||
.then((items) => {
|
|
||||||
// Count amount of total items
|
|
||||||
res.render(__path + '/src/frontend/allItems.eta.html', { items: items });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
|
||||||
});
|
|
||||||
};
|
|
24
src/routes/frontend/manage/categoryManager.ts
Normal file
24
src/routes/frontend/manage/categoryManager.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { prisma, __path, log } from '../../../index.js';
|
||||||
|
|
||||||
|
function get(req: Request, res: Response) {
|
||||||
|
// Render the page
|
||||||
|
prisma.itemCategory
|
||||||
|
.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) => {
|
||||||
|
log.db.error(err);
|
||||||
|
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { get };
|
105
src/routes/frontend/manage/import/csvImport.ts
Normal file
105
src/routes/frontend/manage/import/csvImport.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import { prisma, __path, log } from '../../../../index.js';
|
||||||
|
import { UploadedFile } from 'express-fileupload';
|
||||||
|
import { parse } from 'csv';
|
||||||
|
import { itemStatus, itemCategory, PrismaPromise } from '@prisma/client';
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
const file: UploadedFile = req.files.formFile as UploadedFile;
|
||||||
|
const csv = file.data.toString();
|
||||||
|
parse(csv, { columns: true }, function (err, records) {
|
||||||
|
if (err) {
|
||||||
|
res.send(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Find all categories and save them into a set
|
||||||
|
const categories = new Set<string>();
|
||||||
|
records.forEach((record: any) => {
|
||||||
|
categories.add(record.category);
|
||||||
|
});
|
||||||
|
log.db.debug(categories);
|
||||||
|
// Remove categories that already exists in the database
|
||||||
|
prisma.itemCategory
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
in: Array.from(categories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((values) => {
|
||||||
|
values.forEach((value) => {
|
||||||
|
categories.delete(value.name);
|
||||||
|
});
|
||||||
|
log.db.debug(categories);
|
||||||
|
const categoryPromises: PrismaPromise<itemCategory>[] = [];
|
||||||
|
categories.forEach((category: string) => {
|
||||||
|
const promise = prisma.itemCategory.create({
|
||||||
|
data: {
|
||||||
|
name: category,
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
categoryPromises.push(promise);
|
||||||
|
});
|
||||||
|
Promise.all(categoryPromises)
|
||||||
|
.then((values) => {
|
||||||
|
// Create items
|
||||||
|
const listOfPromises = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < records.length; i++) {
|
||||||
|
const record = records[i];
|
||||||
|
const promise = prisma.item.create({
|
||||||
|
data: {
|
||||||
|
name: record.name,
|
||||||
|
amount: parseInt(record.amount),
|
||||||
|
comment: record.comment,
|
||||||
|
category: {
|
||||||
|
connect: {
|
||||||
|
name: record.category,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SKU: record.sku,
|
||||||
|
manufacturer: record.manufacturer,
|
||||||
|
status: itemStatus.normal,
|
||||||
|
createdBy: 'CSV_IMPORT'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
listOfPromises.push(promise);
|
||||||
|
}
|
||||||
|
Promise.all(listOfPromises)
|
||||||
|
.then((values) => {
|
||||||
|
console.log(values);
|
||||||
|
res.send('ok');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
res.send('failed to create items');
|
||||||
|
log.db.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// res.send('failed to create categories');
|
||||||
|
log.db.error(err);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
res.send('failed to find categories');
|
||||||
|
log.db.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(req: Request, res: Response) {
|
||||||
|
// Render page
|
||||||
|
res.render(__path + '/src/frontend/manage/imports/csvImport.eta.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { get, post };
|
102
src/routes/frontend/manage/import/jsonImport.ts
Normal file
102
src/routes/frontend/manage/import/jsonImport.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import { prisma, __path, log } from '../../../../index.js';
|
||||||
|
import { UploadedFile } from 'express-fileupload';
|
||||||
|
import { itemStatus, itemCategory, PrismaPromise } from '@prisma/client';
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const file: UploadedFile = req.files.formFile as UploadedFile;
|
||||||
|
const jsonRaw = file.data.toString();
|
||||||
|
const json = JSON.parse(jsonRaw);
|
||||||
|
// Get all unqiue categories
|
||||||
|
const categories = new Set<string>();
|
||||||
|
json.forEach((item: any) => {
|
||||||
|
categories.add(item.category);
|
||||||
|
});
|
||||||
|
log.db.debug(categories);
|
||||||
|
|
||||||
|
prisma.itemCategory
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
in: Array.from(categories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((values) => {
|
||||||
|
values.forEach((value) => {
|
||||||
|
categories.delete(value.name);
|
||||||
|
});
|
||||||
|
log.db.debug(categories);
|
||||||
|
const categoryPromises: PrismaPromise<itemCategory>[] = [];
|
||||||
|
categories.forEach((category: string) => {
|
||||||
|
const promise = prisma.itemCategory.create({
|
||||||
|
data: {
|
||||||
|
name: category,
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
categoryPromises.push(promise);
|
||||||
|
});
|
||||||
|
Promise.all(categoryPromises)
|
||||||
|
.then((values) => {
|
||||||
|
// Create items
|
||||||
|
const listOfPromises = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < json.length; i++) {
|
||||||
|
const record = json[i];
|
||||||
|
const promise = prisma.item.create({
|
||||||
|
data: {
|
||||||
|
name: record.name,
|
||||||
|
amount: parseInt(record.amount),
|
||||||
|
comment: record.comment,
|
||||||
|
category: {
|
||||||
|
connect: {
|
||||||
|
name: record.category,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SKU: record.sku,
|
||||||
|
manufacturer: record.manufacturer,
|
||||||
|
status: itemStatus.normal,
|
||||||
|
createdBy: 'CSV_IMPORT'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
listOfPromises.push(promise);
|
||||||
|
}
|
||||||
|
Promise.all(listOfPromises)
|
||||||
|
.then((values) => {
|
||||||
|
console.log(values);
|
||||||
|
res.send('ok');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
res.send('failed to create items');
|
||||||
|
log.db.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// res.send('failed to create categories');
|
||||||
|
log.db.error(err);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
res.send('failed to find categories');
|
||||||
|
log.db.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
// res.status(501).end("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(req: Request, res: Response) {
|
||||||
|
// Render page
|
||||||
|
res.render(__path + '/src/frontend/manage/imports/jsonImport.eta.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { get, post };
|
21
src/routes/frontend/manage/index.ts
Normal file
21
src/routes/frontend/manage/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
// Route imports
|
||||||
|
import testRoute from './test.js';
|
||||||
|
import csvImportRoute from './import/csvImport.js';
|
||||||
|
import jsonImportRoute from './import/jsonImport.js';
|
||||||
|
import categoryManager from './categoryManager.js';
|
||||||
|
import storageManager from './storageManager.js';
|
||||||
|
import startpageRoute from './startpage.js';
|
||||||
|
|
||||||
|
// Router base is '/manage'
|
||||||
|
const Router = express.Router({ strict: false });
|
||||||
|
|
||||||
|
Router.route('/test').get(testRoute.get);
|
||||||
|
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('/').get(startpageRoute.get);
|
||||||
|
|
||||||
|
export default Router;
|
9
src/routes/frontend/manage/startpage.ts
Normal file
9
src/routes/frontend/manage/startpage.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import { prisma, __path } from '../../../index.js';
|
||||||
|
|
||||||
|
function get(req: Request, res: Response) {
|
||||||
|
res.render(__path + '/src/frontend/manage/startpage.eta.html'); //, { items: items });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default { get };
|
20
src/routes/frontend/manage/storageManager.ts
Normal file
20
src/routes/frontend/manage/storageManager.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import { prisma, __path } from '../../../index.js';
|
||||||
|
|
||||||
|
function get(req: Request, res: Response) {
|
||||||
|
prisma.storageUnit.findMany({ include: { contactInfo: true } }).then((storUnits) => {
|
||||||
|
prisma.storageLocation
|
||||||
|
.findMany({
|
||||||
|
include: {
|
||||||
|
storageUnit: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((storLocs) => {
|
||||||
|
prisma.contactInfo.findMany().then((contactInfos) => {
|
||||||
|
res.render(__path + '/src/frontend/manage/storageManager.eta.html', { storUnits: storUnits, storLocs: storLocs, address: contactInfos });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { get };
|
7
src/routes/frontend/manage/test.ts
Normal file
7
src/routes/frontend/manage/test.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
function get(req: Request, res: Response) {
|
||||||
|
res.status(200).send('Manage Test Successful!');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { get };
|
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,4 +1,7 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
export default (req: Request, res: Response) => {
|
|
||||||
res.status(200).send("Frontend Test Successful!");
|
function get(req: Request, res: Response) {
|
||||||
};
|
res.status(200).send('Frontend Test Successful!');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { get };
|
||||||
|
@ -1,16 +1,35 @@
|
|||||||
import express, { Express } from 'express';
|
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
|
// Route imports
|
||||||
import frontend_routes from './frontend/index.js';
|
import frontend_routes from './frontend/index.js';
|
||||||
import static_routes from './static/index.js';
|
import static_routes from './static/index.js';
|
||||||
import api_routes from './api/index.js';
|
import api_routes from './api/index.js';
|
||||||
import dev_routes from './dev/index.js';
|
import auth_routes from './auth/index.js';
|
||||||
|
|
||||||
const Router = express.Router({ strict: false });
|
const Router = express.Router({ strict: false });
|
||||||
|
|
||||||
Router.use('/static', static_routes);
|
Router.use('/static', static_routes);
|
||||||
Router.use('/api', api_routes);
|
Router.use('/api', checkAuthentication, api_routes);
|
||||||
Router.use('/dev', dev_routes); // This is just for development. ToDo: Add check if we are in devmode.
|
Router.use('/auth', auth_routes);
|
||||||
Router.use('/', frontend_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(404).json({ errorcode: 'NOT_FOUND', error: 'Not Found!' });
|
||||||
|
} else {
|
||||||
|
res.status(404).render(__path + '/src/frontend/errors/404.eta.html', { url: req.originalUrl });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Router;
|
||||||
|
|
||||||
export default Router;
|
|
@ -11,7 +11,7 @@ const allowedURLs: Array<string> = JSON.parse(fs.readFileSync('allowedStaticPath
|
|||||||
const recordedURLs: Array<string> = [];
|
const recordedURLs: Array<string> = [];
|
||||||
const debugMode: boolean = JSON.parse(fs.readFileSync('allowedStaticPaths.json', 'utf8')).debugMode;
|
const debugMode: boolean = JSON.parse(fs.readFileSync('allowedStaticPaths.json', 'utf8')).debugMode;
|
||||||
|
|
||||||
Router.use('*', (req: Request, res: Response) => {
|
Router.get('*', (req: Request, res: Response) => {
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
res.sendFile(Path.join(__path, 'node_modules', req.params[0]));
|
res.sendFile(Path.join(__path, 'node_modules', req.params[0]));
|
||||||
recordedURLs.push(req.params[0]);
|
recordedURLs.push(req.params[0]);
|
||||||
|
@ -2,29 +2,58 @@ body {
|
|||||||
font-size: 0.875rem;
|
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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 1.5rem;
|
||||||
/* rtl:raw:
|
|
||||||
right: 0;
|
|
||||||
*/
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
/* rtl:remove */
|
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 100; /* Behind the navbar */
|
z-index: 100;
|
||||||
padding: 48px 0 0; /* Height of navbar */
|
/* Behind the navbar */
|
||||||
|
padding: 48px 0 0;
|
||||||
|
/* Height of navbar */
|
||||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
top: 5rem;
|
top: 5rem;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
.sidebar-sticky {
|
.sidebar-sticky {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -32,17 +61,19 @@ body {
|
|||||||
height: calc(100vh - 48px);
|
height: calc(100vh - 48px);
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
overflow-x: hidden;
|
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 {
|
.sidebar .nav-link {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #333;
|
/* color: #333; */
|
||||||
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link .feather {
|
.sidebar .nav-link .feather {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: #727272;
|
color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link.active {
|
.sidebar .nav-link.active {
|
||||||
@ -59,18 +90,20 @@ body {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sidebarMenu {
|
||||||
|
background-color: var(--bs-secondary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Navbar
|
* Navbar
|
||||||
*/
|
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
padding-top: 0.75rem;
|
padding-top: 0.75rem;
|
||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
|
||||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25);
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
.navbar .navbar-toggler {
|
.navbar .navbar-toggler {
|
||||||
top: 0.25rem;
|
top: 0.25rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
@ -83,8 +116,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-control-dark {
|
.form-control-dark {
|
||||||
color: #fff;
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +124,7 @@ body {
|
|||||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25);
|
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
.autocomplete-items {
|
.autocomplete-items {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
@ -100,3 +132,60 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
.rotate {
|
||||||
|
transform: rotate(-90deg) !important;
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate::before {
|
||||||
|
transform: rotate(-90deg) !important;
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derotate {
|
||||||
|
transform: rotate(0deg) !important;
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derotate::before {
|
||||||
|
transform: rotate(0deg) !important;
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownIndicator {
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
.loader-overlay {
|
||||||
|
border-radius: var(--bs-modal-inner-border-radius);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.678);
|
||||||
|
z-index: 9999;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaderActive {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.navShadow {
|
||||||
|
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
23
static/css/login.css
Normal file
23
static/css/login.css
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.background {
|
||||||
|
background-image: url("https://images.unsplash.com/photo-1683085809775-d9ac53fcbe21?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHx0b3BpYy1mZWVkfDE1fDZzTVZqVExTa2VRfHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&q=60");
|
||||||
|
/* fill the page height and width */
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
/* make the background image cover the whole page */
|
||||||
|
background-size: cover;
|
||||||
|
/* position the image in the center of the page */
|
||||||
|
background-position: center;
|
||||||
|
/* make the image fixed so it doesn't scroll with the page */
|
||||||
|
background-attachment: fixed;
|
||||||
|
/* make the image not repeat */
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidePanel {
|
||||||
|
/* make somewhat transparent and blurry */
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
}
|
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) },
|
||||||
|
});
|
||||||
|
}
|
80
static/js/editCategory.js
Normal file
80
static/js/editCategory.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
const FLAG_supports_new_data_loader = true;
|
||||||
|
|
||||||
|
function getDataForEdit(name) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'get',
|
||||||
|
url: `/api/v1/categories?name=${name}`,
|
||||||
|
success: function (result) {
|
||||||
|
// Get elements inside the editCategoryModal
|
||||||
|
const modal_categoryName = document.getElementById('editCategoryModalName');
|
||||||
|
const modal_categoryDescription = document.getElementById('editCategoryModalDescription');
|
||||||
|
const modal_categoryid = document.getElementById('editCategoryModalId');
|
||||||
|
|
||||||
|
modal_categoryName.value = result.name;
|
||||||
|
modal_categoryDescription.value = result.description;
|
||||||
|
modal_categoryid.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")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function primeCreateNew() {
|
||||||
|
const form = document.getElementById('CategoryModalForm');
|
||||||
|
form.setAttribute('method', 'POST');
|
||||||
|
document.getElementById('editCategoryModalLabel').innerText = 'Create a new category';
|
||||||
|
$('.form-control').val('');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function primeEdit() {
|
||||||
|
const form = document.getElementById('CategoryModalForm');
|
||||||
|
document.getElementById('editCategoryModalLabel').innerText = 'Edit category';
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
217
static/js/editStorages.js
Normal file
217
static/js/editStorages.js
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
// Javascript to enable link to tab
|
||||||
|
// 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',
|
||||||
|
function () {
|
||||||
|
var hash = location.hash.replace(/^#/, ''); // ^ means starting, meaning only match the first hash
|
||||||
|
if (hash) {
|
||||||
|
bootstrap.Tab.getOrCreateInstance(document.querySelector('#' + hash)).show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
var hash = location.hash.replace(/^#/, ''); // ^ means starting, meaning only match the first hash
|
||||||
|
if (hash) {
|
||||||
|
bootstrap.Tab.getOrCreateInstance(document.querySelector('#' + hash)).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change hash for page-reload
|
||||||
|
$('.nav-link').on('click', function (e) {
|
||||||
|
window.location.hash = e.target.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
function primeCreateNew() {
|
||||||
|
// Clear the form
|
||||||
|
$('.form-control').val('');
|
||||||
|
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';
|
||||||
|
form.setAttribute('method', 'POST');
|
||||||
|
form2.setAttribute('method', 'POST');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function primeEdit() {
|
||||||
|
const form = document.getElementById('storageUnitModalForm');
|
||||||
|
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').selectedIndex = 1;
|
||||||
|
handleSelector();
|
||||||
|
form.setAttribute('method', 'PATCH');
|
||||||
|
form2.setAttribute('method', 'PATCH');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
$('#storageUnitModalContactInfoCreator').addClass('d-none');
|
||||||
|
$('.requireOnCreate').attr('required', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataForEdit(id) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'get',
|
||||||
|
url: `/api/v1/storageUnits?id=${id}`,
|
||||||
|
success: function (result) {
|
||||||
|
// Get elements inside the editCategoryModal
|
||||||
|
const modal_unitName = document.getElementById('storageUnitModalName');
|
||||||
|
const modal_unitLocation = document.getElementById('storageUnitModalLocationSelect');
|
||||||
|
const modal_unitId = document.getElementById('storageUnitModalLocationSelectHidden');
|
||||||
|
// const modal_categoryid = document.getElementById('editCategoryModalId');
|
||||||
|
|
||||||
|
modal_unitName.value = result.name;
|
||||||
|
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');
|
||||||
|
modal_unitLocation.selectedIndex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 storage unit does no longer exist.', 'text-bg-danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataForEditLoc(id) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'get',
|
||||||
|
url: `/api/v1/storageLocations?id=${id}`,
|
||||||
|
success: function (result) {
|
||||||
|
// Get elements inside the editCategoryModal
|
||||||
|
const modal_locationName = document.getElementById('storageLocationModalName');
|
||||||
|
const modal_locationUnitSel = document.getElementById('storageLocationModalUnit');
|
||||||
|
const modal_locationId = document.getElementById('storageLocationModalIdHidden');
|
||||||
|
// const modal_categoryid = document.getElementById('editCategoryModalId');
|
||||||
|
|
||||||
|
modal_locationName.value = result.name;
|
||||||
|
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.storageUnitId) {
|
||||||
|
console.log('Found it');
|
||||||
|
modal_locationUnitSel.selectedIndex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 storage unit does no longer exist.', 'text-bg-danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
135
static/js/formHandler.js
Normal file
135
static/js/formHandler.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
var form = $(this); // Get the form
|
||||||
|
|
||||||
|
// Show overlay with spinner
|
||||||
|
$('.loader-overlay').addClass('loaderActive');
|
||||||
|
|
||||||
|
// Get the form data
|
||||||
|
formData = form.serializeArray();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: $(this).attr('method'),
|
||||||
|
url: $(this).attr('data-target'),
|
||||||
|
data: formData,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (data) {
|
||||||
|
console.log('success');
|
||||||
|
// Hide overlay with spinner
|
||||||
|
$('.loader-overlay').removeClass('loaderActive');
|
||||||
|
// Close the modal
|
||||||
|
$('.modal').modal('hide');
|
||||||
|
// Clear all fields
|
||||||
|
form.find('input, textarea').val('');
|
||||||
|
// Create toast
|
||||||
|
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');
|
||||||
|
// Hide overlay with spinner
|
||||||
|
$('.loader-overlay').removeClass('loaderActive');
|
||||||
|
|
||||||
|
// Check for response code 409 (duplicate entry)
|
||||||
|
if (data.status == 409) {
|
||||||
|
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> The element you tried to create already exists.', "text-bg-danger", 3000, false)
|
||||||
|
} else {
|
||||||
|
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. Please try again later.', "text-bg-danger", 3000, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic function to handle the result of a deletion prompt
|
||||||
|
* @param {Number} id ID of the entry to delete
|
||||||
|
* @param {String} route Route to send the delete request to will be templated as /api/v1/{route}
|
||||||
|
* @param {String} name Type of entry to delete, will be templated as {name} deleted successfully.
|
||||||
|
*/
|
||||||
|
function deleteEntryNxt(id, route, name) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'delete',
|
||||||
|
url: `/api/v1/` + route,
|
||||||
|
data: { id: id },
|
||||||
|
success: function (data) {
|
||||||
|
$('#staticBackdrop').modal('hide');
|
||||||
|
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,
|
||||||
|
ticks: 100,
|
||||||
|
gravity: 0.1,
|
||||||
|
decay: 0.94,
|
||||||
|
startVelocity: 30,
|
||||||
|
particleCount: 20,
|
||||||
|
scalar: 2,
|
||||||
|
shapes: ['text'],
|
||||||
|
shapeOptions: {
|
||||||
|
text: {
|
||||||
|
value: ['❌', '🗑️', '🚫']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function (data) {
|
||||||
|
// hide the staticBackdrop modal
|
||||||
|
$('#staticBackdrop').modal('hide');
|
||||||
|
|
||||||
|
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong. Please try again later.', "text-bg-danger", 3000, false)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic route to trigger a prefill of the delete modal
|
||||||
|
* @param {Number} id The ID of the entry to delete
|
||||||
|
* @param {String} route The endpoint to send the delete request to, will be templated as /api/v1/{route}
|
||||||
|
* @param {String} name The name of the entry to delete, will be templated as {name} deleted successfully.
|
||||||
|
*/
|
||||||
|
function preFillDeleteModalNxt(id, route, name, requestIdent='id') {
|
||||||
|
$.ajax({
|
||||||
|
type: 'get',
|
||||||
|
url: `/api/v1/${route}?${requestIdent}=${id}`,
|
||||||
|
success: function (result) {
|
||||||
|
|
||||||
|
// Get elements inside the editCategoryModal
|
||||||
|
const modal_categoryName = document.getElementById('deleteNamePlaceholder');
|
||||||
|
const modal_deleteButton = document.getElementById('deleteActionBtn');
|
||||||
|
|
||||||
|
modal_categoryName.innerText = result.name;
|
||||||
|
modal_deleteButton.setAttribute('onclick', `deleteEntryNxt(${result.id},'${route}','${name}')`);
|
||||||
|
},
|
||||||
|
error: function (data) {
|
||||||
|
console.log('!!!! ERROR !!!!', data);
|
||||||
|
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`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("Found " + amountOfForms + " forms on this page.")
|
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();
|
||||||
|
})();
|
21
static/js/handleSidebarTriangles.js
Normal file
21
static/js/handleSidebarTriangles.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const trinagles = $('.dropdownIndicator');
|
||||||
|
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 () {
|
||||||
|
$(triTar).addClass('rotate');
|
||||||
|
$(triTar).removeClass('derotate');
|
||||||
|
});
|
||||||
|
target.on('hide.bs.collapse', function () {
|
||||||
|
$(triTar).removeClass('rotate');
|
||||||
|
$(triTar).addClass('derotate');
|
||||||
|
});
|
||||||
|
});
|
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,25 +1,112 @@
|
|||||||
document.getElementById("SearchBox").addEventListener("keyup", handleSearchChange);
|
document.getElementById('SearchBoxInput').addEventListener('keyup', handleSearchChange);
|
||||||
const autocompleteBox = document.getElementById("autocomplete-items");
|
document.getElementById('searchForm').addEventListener('submit', handleSearchSubmit);
|
||||||
autocompleteBox.style.display = "none";
|
document.addEventListener('keyup', handleHotKey)
|
||||||
|
const autocompleteBox = document.getElementById('autocompletBody');
|
||||||
|
autocompleteBox.style.display = 'none';
|
||||||
|
|
||||||
|
currentBestGuessCommand = '';
|
||||||
|
|
||||||
function handleSearchChange(e) {
|
function handleSearchChange(e) {
|
||||||
console.log(e.target.value);
|
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 #)
|
// Check if known prefix is used (either > or #)
|
||||||
if(e.target.value != "" ) {
|
if (e.target.value != '') {
|
||||||
autocompleteBox.style.display = "block";
|
autocompleteBox.style.display = 'block';
|
||||||
autocompleteBox.innerHTML = "Search results will show up here soon <br> Trust me <br> Results";
|
autocompleteBox.innerHTML = 'Search results will show up here soon <br> Trust me <br> Results';
|
||||||
} else {
|
} else {
|
||||||
autocompleteBox.style.display = "none";
|
autocompleteBox.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.target.value[0] == ">") {
|
if (e.target.value[0] == '>') {
|
||||||
autocompleteBox.innerHTML = "Start typing to search for commands <br> >goto items";
|
// List of valid routes
|
||||||
if(e.target.value == ">goto items") {
|
urlList = {
|
||||||
autocompleteBox.innerHTML = "<a href='/allItems'>Goto Items</a>";
|
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
|
// 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 {
|
} else {
|
||||||
// Search for name
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
76
static/js/toastHandler.js
Normal file
76
static/js/toastHandler.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.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 && !forceSkipReload) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}, autoHideTime);
|
||||||
|
return newToast.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