134 Commits

Author SHA1 Message Date
db1d29f510 Updated boostrap to mitigate missing icons 2024-07-02 20:14:16 +02:00
f3e306b084 AFLOW-46-ui-duplication
Added AFLOW-46

Reviewed-on: #1
2024-06-19 18:34:40 +02:00
c23b1b306c updated and properly implemented auth middleware AFLOW-32
Co-authored-by: Spacelord <git@spacelord.de>
2023-11-01 20:04:19 +01:00
2371089f88 updated config handler to autogenerate secrets and default user structure
Co-authored-by: Spacelord <Spacelord09@users.noreply.github.com>
2023-11-01 20:03:28 +01:00
6fa2797903 improved login screen (fixed layout, added error msg) 2023-08-27 19:04:55 +02:00
af896a6688 Added local authentication (AFLOW-32) 2023-08-26 20:59:46 +02:00
347979bb10 Remove login demo page route 2023-08-26 20:57:16 +02:00
ddfdfc3092 Remove old demo login page 2023-08-26 20:56:48 +02:00
56cbebb36b Add form to login page 2023-08-26 20:56:26 +02:00
e307ff97ac Add passport.js dependencies 2023-08-26 20:56:03 +02:00
f52897fd4d added todo to search 2023-07-11 16:25:58 +02:00
b0b47e04f8 introduction of contactInfo route 2023-07-11 16:24:57 +02:00
660c9c092e make sure all tooltips are hidden before showing new ones to prevent stuck tooltips 2023-07-10 18:18:38 +02:00
6b092b34b3 fix AFLOW-28 2023-07-10 18:11:24 +02:00
421085a8d5 implemented AFLOW-27 2023-07-10 18:00:24 +02:00
ea80b4bf2b - fixed issues with jumpy actions for item view
- added AFLOW-23 support for categories and rewrote interface
2023-07-10 17:50:31 +02:00
94186a3a18 removed redundant getAll attribute 2023-07-10 17:36:59 +02:00
db6df2fdc6 another fix for AFLOW-24 2023-07-10 16:06:54 +02:00
9b2db6eed7 possible fix for AFLOW-24 2023-07-10 16:05:08 +02:00
cdbd4c3c10 Merge branch 'master' of https://git.project-name-here.de/Project-Name-Here/assetflow 2023-07-10 15:51:54 +02:00
bc9d395e77 improved /version route to include git info 2023-07-10 15:51:52 +02:00
16da321177 „README.md“ ändern 2023-07-10 15:47:03 +02:00
6f7f65fa36 fix error with unsortable relations 2023-07-10 15:36:19 +02:00
1605987952 update all storagelocation / unit routes to support new tables (AFLOW-23) 2023-07-10 15:34:04 +02:00
a79a1eab81 improve /version api route to only call git once on startup 2023-07-10 15:09:30 +02:00
45a4935190 added tool for load-testing many items 2023-07-09 23:37:48 +02:00
ff07698f16 fix bug where delete would reload the page even if new data loading is enabled 2023-07-09 23:35:59 +02:00
5aeec6fb28 Fixed AFLOW-26 and AFLOW-14 in items API route 2023-07-09 23:03:15 +02:00
3be376b214 Fixed AFLOW-26 and AFLOW-14 in categories API route 2023-07-09 22:29:52 +02:00
3f55b22ede Fixed AFLOW-26 and AFLOW-14 in storageUnit API route 2023-07-09 21:59:35 +02:00
534cc3055f Fixed AFLOW-26 and AFLOW-14 in storageLocation API route 2023-07-09 21:28:31 +02:00
abb7e7bab3 introduction of proper text length limiting 2023-07-09 20:17:05 +02:00
09e74f9eb6 Fix AFLOW-22 2023-07-09 18:20:27 +02:00
720a969484 respond with proper error page when the /s/:id sku fails 2023-07-09 18:14:29 +02:00
5524f14e1a fixed error with duplicate stringify operation in api routes
Co-authored-by: Spacelord <Spacelord09@users.noreply.github.com>
2023-07-09 18:07:28 +02:00
58a2d2ad19 comment function 2023-07-08 16:18:28 +02:00
c50aa8990c - fixed bug with item route
- fixed visual bug with ids in item view
2023-07-08 00:45:22 +02:00
8d954052f2 - added loader to sort operations 2023-07-08 00:25:33 +02:00
0e4bc7669a - improved behavior when updating and searching 2023-07-08 00:22:09 +02:00
0233453084 - introduced table view with client side loading
- improved /items/ with support for pagination
- improved helper functions for tooltips and popovers
- removed console log residue from handleSidebarTriangles
- introduction of version route
2023-07-08 00:09:54 +02:00
c026b5f1a8 fix greeting to finally respect the time 2023-07-07 22:15:50 +02:00
5d99baea8e added proper version information
Co-authored-by: Spacelord <Spacelord09@users.noreply.github.com>
2023-07-07 22:09:17 +02:00
587dac99c5 - fixed visual error where empty entries where shown as null 2023-07-07 15:43:02 +02:00
45bec04007 - made sidenavbar logo lead to dashboard
- moved easter egg somewhere else
2023-07-07 15:35:01 +02:00
d38713e7ed - introduced version number
- introduced option to disable reload for this session
2023-07-07 15:29:46 +02:00
5584cc5c41 - introduced a version inducator in the sidebar 2023-07-07 15:25:06 +02:00
57513da827 - fixed item creation, which was broken by last update 2023-07-07 15:13:17 +02:00
185d563ac0 - sort item page by SKU 2023-07-07 15:11:33 +02:00
e0ac509007 Attempt to fix AFLOW-18 while fixing Firefox (again) 2023-07-07 14:15:25 +02:00
90924aa30d - added method to force disable reload 2023-07-07 14:02:54 +02:00
f4d6ed4d8f fix on safari 2023-07-07 13:59:24 +02:00
1b7b8af118 smaller logo on safari 2023-07-07 13:59:23 +02:00
eb3b97240d fix for safari 2023-07-07 13:56:03 +02:00
6ba0716cfc small scale fix 2023-07-07 13:50:46 +02:00
b66367c34a - fixed rendering bug for browsers other then firefox 2023-07-07 13:25:57 +02:00
a7864b3c11 improved item information page 2023-07-05 20:18:52 +02:00
b785dd8ca7 Mitigated frontend selection issues 2023-06-27 23:47:00 +02:00
4c0be6d87b Implemented api validation and item route patch
- Implemented api field validation in item api routes
- Implemented PATCH api route for item endpoint
2023-06-27 23:45:58 +02:00
87e1c55553 Added comment 2023-06-27 23:43:23 +02:00
a4d697265b Added middleware to handle empty strings as null 2023-06-27 23:42:29 +02:00
ad84e6a3a0 ALT+CTRL+C Keybind!
Don't try to stop me writing in a textbox with the keybind C again....
2023-06-27 23:40:52 +02:00
656ca2f74a Removed unneccesary comment in prisma schema 2023-06-27 23:39:01 +02:00
ac7ebbbf5e Added api search route for sku 2023-06-27 20:56:04 +02:00
137da0e31e Added helper logger 2023-06-27 20:54:47 +02:00
db0e8c2047 Added search api endpoint / Implemented search 2023-06-27 20:54:27 +02:00
efe36fc60a Aded helper func to parse int for prisma relations 2023-06-27 20:52:20 +02:00
9ab12118a0 Replaced console.log calls with custom logger ones 2023-06-27 19:03:38 +02:00
578b21d4b5 Add parsing infrastructure for prisma schema 2023-06-27 18:51:15 +02:00
24a9deae62 Fixed some Bugs in configHandler 2023-06-27 16:35:24 +02:00
f249a4552c Frontend update 2023-06-06 23:49:16 +02:00
3b1e4a7cde Unified error messages/structure / Added item api route.
- Introduce new error output structure to api routes.
- It is now possible to create new items via api.
2023-06-06 23:48:59 +02:00
ce31beb1a8 manufacturer is now optional in DB schema 2023-06-06 23:02:43 +02:00
e11bea21ea Fixed default route 2023-06-06 23:02:19 +02:00
2dd52a0c1d Centered modal 2023-05-23 22:45:00 +02:00
1f85dd5710 Added status dropdown to item-create/edit modal 2023-05-23 22:43:51 +02:00
50d98c0894 Added shadow to navbar 2023-05-23 22:42:22 +02:00
74db923058 Removed unnecessary comment / Fixed formatting 2023-05-23 22:18:42 +02:00
d5fcf94455 Added branding / Fixed navbar 2023-05-23 22:04:56 +02:00
584b00c878 Fiexed layout in dbError 2023-05-23 21:58:46 +02:00
2b5831fccb Renamed importedBy to createdBy 2023-05-23 21:57:32 +02:00
ce04e4ff1c Renamed inportedBy to createdBy in DB Schema 2023-05-23 21:56:12 +02:00
7562f7005b Update prisma to the latest version 2023-05-23 21:55:47 +02:00
cfc28c5959 Current (non-working-frontend) state 2023-05-22 18:39:47 +02:00
b29550f429 Added contactInfo to item in DB Schema 2023-05-21 01:53:52 +02:00
37649ec98e Fixed formatting in toastHandler 2023-05-21 01:45:27 +02:00
a8b0374d5e Fix variable declaration in toastHandler 2023-05-21 01:42:16 +02:00
90dbadac24 Fixed the redirection loop in the pagination of /items
A redirection loop occurred when there were no items in the database.
2023-05-21 01:31:35 +02:00
4145dafb7d Fixed toastHandler script-tag in frontend 2023-05-21 00:59:37 +02:00
037d03cc50 Fixed triangles 2023-05-21 00:42:37 +02:00
b6ebda8fb5 Renamed normalizeToast to toastHandler 2023-05-21 00:42:05 +02:00
1076c03f2c Removed unnecessary import 2023-05-21 00:03:39 +02:00
713cadcba1 Fixed theme switcher 2023-05-20 22:04:46 +02:00
9411f1ad72 Renamed items to contents 2023-05-20 22:03:39 +02:00
259ec997c8 Removed sass 2023-05-20 22:02:49 +02:00
524feee54d - added toggel for dark/auto/white mode
- added color
2023-05-17 22:07:36 +02:00
e6238e80e8 Unified deletion modal in frontend 2023-05-17 19:06:38 +02:00
e98e46e1a2 Improved UI Toast Tooling 2023-05-17 17:46:51 +02:00
bd9f629690 The BIG frontend update 2023-05-16 22:58:08 +02:00
04b5bd60f2 Implement storageLocation api 2023-05-16 22:19:36 +02:00
90fc8068a0 Returning StorageLocations in storageUnit api. 2023-05-16 21:15:17 +02:00
533bc1744d Add created id to response of category api 2023-05-16 18:01:10 +02:00
64c14db183 Add created id to response of storageUnit api 2023-05-16 17:58:53 +02:00
e4295493f2 Implemented storageUnit api / Added error handling
- AFLOW-13
- Added check if an category entry already exists.
- Update precheck if all IDs are existing
2023-05-16 17:47:32 +02:00
b514e81764 Added some error handling to categories api
- AFLOW-13
- Added check if an category entry already exists.
- Update prechek if all IDs are existing
2023-05-16 17:41:55 +02:00
e82d16af3e Added vsls config. 2023-05-16 17:17:26 +02:00
2285b3dd33 Fix formatting 2023-05-16 15:29:54 +02:00
532b7b9fe2 Add current state 2023-05-16 00:05:39 +02:00
3819d007a5 Removed sentry error handler 2023-05-16 00:03:54 +02:00
d7abadf6a6 Added stroageUnit to contactType enum in db Schema
- contactInfo in StorageUnit is not optional anymore.
2023-05-16 00:03:05 +02:00
d51b063918 Added Sentry prisma integration 2023-05-15 22:57:00 +02:00
0c7c294823 Unified Category modal 2023-05-15 22:10:32 +02:00
4bfd71f09f Added Sentry / Added comment 2023-05-15 21:58:01 +02:00
6afdb4fcdd Added validation for category api route. 2023-05-15 19:07:13 +02:00
3b9813a680 current state 2023-05-15 18:49:02 +02:00
b5314cb552 itemStatus is now optional in Prisma Schema 2023-05-15 18:48:37 +02:00
9a05743cb3 Renamed pages 2023-05-15 15:34:28 +02:00
38cd29943f Migrated route / Fixed import
- Migrate categoryManager to new route schema.
- Fiex csv Import description.
2023-05-15 15:27:55 +02:00
b1a73ebd4a Default route(404) is now Content-Type aware(json)
- Cleanup
2023-05-15 02:08:43 +02:00
9c4eb3200e Removed unnecessary debug message. 2023-05-15 01:39:10 +02:00
ee61e94853 Fixed routing and publicInfoPage. 2023-05-15 01:37:51 +02:00
1f2eb78333 Current state 2023-05-15 00:21:53 +02:00
6344134a9e Added some prisma schema changes. (untested)
- Not fully implemented yet.
- Renamed ToDo -> TODO
2023-05-14 13:33:15 +02:00
75a5580366 Renamed "All items" to "Items". 2023-05-14 13:11:01 +02:00
55ae8d4c8f Moved nav items to right place.
- Applied conventions.
2023-05-13 01:02:11 +02:00
d7794da74b Added TODO -> Center table content. 2023-05-13 01:01:03 +02:00
739ab7b9ee Changed printWidth to 225 in prettierrc. 2023-05-13 01:00:29 +02:00
5ade4891b5 Added tooltips to SKU on Dashboard+Items
- Globally enabled all bootstrap tooltips.
2023-05-13 00:42:15 +02:00
7a26537903 Cleanup
- Renamed All Items nav element to Items.
- Removed unnecessary comment.
- Disabled placeholder nav elements.
2023-05-13 00:06:50 +02:00
3569c9ed4b Updated styles and Added storageManager.
- Dark/White-mode support
- Collapsible navs
- Renamed items template.
- StorageBuilding's are now StorageUnit's
- Formatting, general cleanup, some bug fixing.
2023-05-13 00:06:35 +02:00
96853debe2 Moved to new route structure. 2023-05-10 23:05:55 +02:00
c6fb84759f - added some (non-working) category edit 2023-05-10 21:28:11 +02:00
7cfca9abac Add (partially-) working categoryManager.
- Added Body-Parser.
2023-05-08 23:30:19 +02:00
43ef7fd395 Merge branch 'master' of https://git.project-name-here.de/Project-Name-Here/assetflow 2023-05-08 20:05:08 +02:00
4ce9dae7ab - category wip 2023-05-08 20:05:07 +02:00
83 changed files with 5446 additions and 493 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ node_modules
config.json
dist
docs
demoData.json

View File

@ -10,7 +10,7 @@
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 200,
"printWidth": 225,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,

6
.vsls.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "http://json.schemastore.org/vsls",
"gitignore": "hide",
"excludeFiles": ["!node_modules"],
"hideFiles": ["node_modules"]
}

View File

@ -1,4 +1,16 @@
# Assetflow
## Theme?
https://bootswatch.com/darkly/
Assetflow is an inventory management solution targeted at the event industry.
## 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

View File

@ -4,12 +4,15 @@
"/bootstrap/dist/css/bootstrap.min.css",
"/bootstrap/dist/js/bootstrap.bundle.min.js",
"/jquery/dist/jquery.min.js",
"/darkreader/darkreader.js",
"/bootstrap-icons/font/fonts/bootstrap-icons.woff2",
"/bootstrap/dist/css/bootstrap.min.css.map",
"/@popperjs/core/dist/umd/popper.min.js",
"/@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
}

1289
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,25 +18,37 @@
"license": "GPL-3.0",
"dependencies": {
"@popperjs/core": "^2.11.7",
"@prisma/client": "^4.13.0",
"@prisma/client": "^4.14.1",
"@sentry/node": "^7.52.1",
"@sentry/tracing": "^7.52.1",
"body-parser": "^1.20.2",
"bootstrap": "^5.3.0-alpha3",
"bootstrap-icons": "^1.10.5",
"bootstrap-icons": "^1.11.0",
"bootstrap-table": "^1.22.1",
"csv": "^6.2.11",
"eta": "^2.0.1",
"express": "^4.18.2",
"express-fileupload": "^1.4.0",
"express-session": "^1.17.3",
"jquery": "^3.6.4",
"lodash": "^4.17.21",
"prisma": "^4.13.0",
"signale": "^1.4.0"
"passport": "^0.6.0",
"passport-local": "^1.0.0",
"signale": "^1.4.0",
"tsparticles-confetti": "^2.9.3"
},
"devDependencies": {
"@loancrate/prisma-schema-parser": "^2.0.0",
"@types/express": "^4.17.17",
"@types/express-fileupload": "^1.4.1",
"@types/express-session": "^1.17.7",
"@types/lodash": "^4.14.194",
"@types/passport": "^1.0.12",
"@types/passport-local": "^1.0.35",
"@types/signale": "^1.4.4",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"prisma": "^4.14.1",
"prisma-dbml-generator": "^0.10.0",
"prisma-docs-generator": "^0.7.0",
"typescript": "^5.0.4"

View File

@ -13,64 +13,101 @@ datasource db {
// https://github.com/pantharshit00/prisma-docs-generator
generator docs {
provider = "node node_modules/prisma-docs-generator"
output = "../docs"
output = "../docs"
}
// https://github.com/notiz-dev/prisma-dbml-generator
// Viewer: https://dbdiagram.io/d
generator dbml {
provider = "prisma-dbml-generator"
output = "../docs"
provider = "prisma-dbml-generator"
output = "../docs"
outputName = "schema.dbml"
projectName = "AssetFlow"
}
enum Status {
enum itemStatus {
normal
borrowed
stolen
lost
}
// comments and descriptions -> @db.VarChar(2048)
model Item {
id Int @id @default(autoincrement())
SKU String? @unique
Amount Int
Comment String?
name String
manufacturer String
category Category @relation(fields: [categoryId], references: [id])
categoryId Int
status Status
StorageLocation StorageLocation? @relation(fields: [storageLocationId], references: [id])
id Int @id @unique @default(autoincrement())
SKU String? @unique
amount Int @default(1)
name String
comment String? @db.VarChar(2048)
status itemStatus @default(normal) /// TODO: Would it be better to create a separate model for this as well instead of providing several static statuses to choose from(enum)?
contactInfo contactInfo? @relation(fields: [contactInfoId], references: [id])
contactInfoId Int?
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?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
importedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String?
}
model StorageLocation {
id Int @id @default(autoincrement())
name String
storageBuilding StorageBuilding? @relation(fields: [storageBuildingId], references: [id])
storageBuildingId Int?
Item Item[]
id Int @id @default(autoincrement())
name String @unique /// This is our LocationID for external use prefixed with: '%StorageUnit%_'
storageUnit StorageUnit? @relation(fields: [storageUnitId], references: [id])
storageUnitId Int?
Item Item[]
}
model StorageBuilding {
id Int @id @default(autoincrement())
name String
street String
houseNumber String
zipCode String
city String
country String
/// A StorageUnit is the base and can hold multiple StorageLocations.
model StorageUnit {
id Int @id @default(autoincrement())
name String @unique
contactInfo contactInfo @relation(fields: [contactInfoId], references: [id])
contactInfoId Int
StorageLocation StorageLocation[]
}
model Category {
model itemCategory {
id Int @id @default(autoincrement())
name String @unique
description String?
name String @unique
description String? @db.VarChar(2048)
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[]
}
/// TODO: Allow multiple types to be used?
enum contactType {
storageUnit
owner
person
customer
company
partner
enemy
}

View File

@ -1,7 +1,8 @@
import fs from 'node:fs';
import _ from 'lodash';
import { randomUUID, randomBytes } from 'crypto';
export type configObject = Record<any, any>
export type configObject = Record<any, any>;
/**
* This class is responsible to save/edit config files.
@ -13,7 +14,8 @@ export type configObject = Record<any, any>
export default class config {
#configPath: string;
//global = {[key: string] : string}
global: configObject
global: configObject;
replaceSecrets: boolean;
/**
* Creates an instance of config.
@ -22,9 +24,10 @@ export default class config {
* @param {string} configPath Path to config file.
* @param {object} configPreset Default config object with default values.
*/
constructor(configPath: string, configPreset: object) {
constructor(configPath: string, replaceSecrets: boolean, configPreset: object) {
this.#configPath = configPath;
this.global = configPreset;
this.replaceSecrets = replaceSecrets;
try {
// Read config
@ -35,7 +38,15 @@ export default class config {
// Save config.
this.save_config();
} catch (err) {
console.error('Could not read config file at ' + this.#configPath + ' due to: ' + err);
// If file does not exist, create it.
if (err.code === 'ENOENT') {
console.log(`Config file does not exist. Creating it at ${this.#configPath} now.`);
this.save_config();
return;
}
console.error(`Could not read config file at ${this.#configPath} due to: ${err}`);
// Exit process.
process.exit(1);
}
}
@ -44,40 +55,86 @@ export default class config {
*/
save_config() {
try {
// If enabled replace tokens defines as "gen" with random token
if (this.replaceSecrets) {
// Replace tokens with value "gen"
this.generate_secrets(this.global, 'gen')
}
fs.writeFileSync(this.#configPath, JSON.stringify(this.global, null, 8));
} catch (err) {
console.error('Could not write config file at ' + this.#configPath + ' due to: ' + err);
console.error(`Could not write config file at ${this.#configPath} due to: ${err}`);
return;
}
console.log('Successfully written config file to ' + this.#configPath);
console.log(`Successfully written config file to ${this.#configPath}`);
}
/**
* Replaces each item matching the value of placeholder with a random UUID.
* Thanks to https://stackoverflow.com/questions/8085004/iterate-through-nested-javascript-objects
* @param {configObject} obj
*/
generate_secrets(obj: configObject, placeholder: string) {
const stack = [obj];
while (stack?.length > 0) {
const currentObj = stack.pop();
Object.keys(currentObj).forEach((key) => {
if (currentObj[key] === placeholder) {
console.log('Generating secret: ' + key);
currentObj[key] = randomBytes(48).toString('base64').replace(/\W/g, '');
}
if (typeof currentObj[key] === 'object' && currentObj[key] !== null) {
stack.push(currentObj[key]);
}
});
}
}
}
// BUG: If file does'nt exist -> fail.
// ToDo: Check for SyntaxError on fileread and ask if the user wants to continue -> overwrite everything. This behavior is currently standard.
/*
**** Example ****
const default_config = {
token: 'your-token-goes-here',
clientId: '',
devserverID: '',
devmode: true
};
import ConfigHandlerNG from './assets/configHandlerNG.js';
// Create a new config instance.
export const config = new ConfigHandler(__path + '/config.json', true, {
test1: 't1',
test2: 't2',
test3: 'gen',
test4: 't4',
test5: 'gen',
testObj: {
local: {
active: true,
users: {
user1: 'gen',
user2: 'gen',
user3: 'gen',
user4: 'gen',
import configHandler from './assets/config.js';
const config = new configHandler(__path + '/config.json', default_config);
}
},
oidc: {
active: false
}
}
});
console.log('Base Config:');
console.log(config.global);
console.log('Add some new key to config and call save_config.');
console.log('Add some new key to config and call save_config().');
config.global.NewKey = 'ThisIsANewKey!'
config.save_config()
console.log('This will add a new key with value gen, but gen gets replaced with a random UUID when save_config() is called.');
config.global.someSecret = 'gen'
config.save_config() // global.someSecret is getting replaced with some random UUID since it was set to 'gen'.
console.log('Complete Config:');
console.log(config.global);
*/

113
src/assets/helper.ts Normal file
View 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}" }`);
}

View File

@ -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") %>

View 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>

View File

@ -1,6 +1,19 @@
<%~ E.includeFile("partials/head.eta.html", {"title": "Dashboard"}) %> <%~ E.includeFile("partials/controls.eta.html", {"active": "Dashboard"}) %>
<h1>Good evening, ${user}</h1>
<h1 onclick="doTheConfetti()" class="user-select-none" id="greeting">Good evening, ${user}</h1>
<script>
// Handle greeting
var today = new Date();
var curHr = today.getHours();
if (curHr < 12) {
document.getElementById("greeting").innerHTML = "Good morning";
} else if (curHr < 18) {
document.getElementById("greeting").innerHTML = "Good afternoon";
} else {
document.getElementById("greeting").innerHTML = "Good evening";
}
</script>
<div class="container text-center">
<div class="row">
<div class="card col m-2">
@ -23,28 +36,36 @@
</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>
<div class="container">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">SKU</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">SKU</th>
<th scope="col">Actions</th>
<!--<th scope="col">Actions</th>-->
</tr>
</thead>
<tbody>
<% it.recents.forEach(function(user){ %>
<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>
<% if(user.status == "normal") { %>
<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>
<% } else if(user.status == "stolen") { %>
<td><span class="badge text-bg-danger"><%= user.status %></span></td>
<% } else if(user.status == "lost") { %>
<td><span class="badge text-bg-warning"><%= user.status %></span></td>
<% } else if(user.status == "borrowed") { %>
<td><span class="badge text-bg-info"><%= user.status %></span></td>
<% } %>
<!--<td><a href="#" class="btn btn-primary">Edit</a></td>-->
</tr>
<% }) %>
</tbody>

View 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") %>

View File

@ -3,11 +3,11 @@
<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>
<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>
<div class="collapse" id="collapseExample">
<div class="card card-body">
<pre><code><%= error %></code></pre>
<div class="card card-body text-start">
<pre><code><%= it.error %></code></pre>
</div>
</div>
</div>

View File

@ -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") %>

View 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
View 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") %>

View 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") %>

View 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") %>

View 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") %>

View 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") %>

View 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") %>

View File

@ -1,99 +1,200 @@
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 test-white-50" href="#">AssetFlow</a>
<button
class="navbar-toggler position-absolute d-md-none collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#sidebarMenu"
aria-controls="sidebarMenu"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<input class="form-control form-control-dark w-100 bg-secondary" type="text" placeholder="Search" aria-label="Search" id="SearchBox" />
<div class="autocomplete-items bg-secondary w-75 border-primary me-5 p-2" id="autocomplete-items" style="left: 16.7%">
</div>
<div class="navbar-nav">
<div class="nav-item text-nowrap">
<a class="nav-link px-3" id="logoutButton">Sign out</a>
<nav class="navbar navbar-expand-lg bg-body-tertiary sticky-top navShadow" style="z-index: 999">
<div class="container-fluid">
<a class="navbar-brand user-select-none ms-2" style="cursor: default" href="/">
<img alt="AssetFlow Logo" draggable="false" class="me-2 headLogo" src="/logo/Design_icon.svg"/> AssetFlow</a>
<button
class="navbar-toggler position-absolute d-md-none collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#sidebarMenu"
aria-controls="sidebarMenu"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"></ul>
<form class="d-flex" role="search">
<button type="button" class="btn btn-primary" data-bs-toggle="modal"
data-bs-target="#search_modal"><i class="bi bi-search"></i></button>
</form>
</div>
</div>
</header>
<div class="toast-container position-fixed bottom-0 end-0 p-3 ">
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">Notification</strong>
<small>Just now</small>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" id="toastText">
The button you just pressed is very useless.
</nav>
<div class="modal" id="search_modal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modalSearchBar">
<div class="input-group mb-3">
<form id="searchForm">
<input type="text" id="SearchBoxInput" class="form-control focus" placeholder="Start typing to search..." aria-label="Search" autocomplete="off">
</form>
</div>
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="autocompletBody">
</div>
</div>
</div>
</div>
<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() {
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
toastBootstrap.show()
setTimeout(function(){ toastBootstrap.hide(); document.getElementById("toastText").innerHTML = alltexts[texti] }, 3000);
}
</script>
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" id="masterToast" style="z-index: 2000">
<div class="d-flex">
<div class="toast-body">Hello, world! This is a toast message.</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toastMainController">
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" id="generalToast" style="z-index: 2000">
<div class="d-flex">
<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="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">
<ul class="nav flex-column">
<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 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 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 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 class="nav-item">
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>"" href="#"> Reports </a>
</li>
<li class="nav-item">
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>"" href="#"> Integrations </a>
</li>
<a class="nav-link <%= it.active == 'placeholder' ? 'active' : ''%>" href="#"> Integrations </a>
</li> -->
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Importer</span>
<a class="link-secondary" href="#" aria-label="Add a new report"> </a>
<a href="/manage/" class="nav-link"
>Settings
<span class="badge rounded-pill bg-danger invisible">
2
<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 == 'CSV_import' ? 'active' : ''%>" href="/import/csv"><i class="bi bi-filetype-csv"></i> CSV Import </a>
</li>
<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 == '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>
</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}`);
},
error: function (data) {
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Unable to load version information', "text-bg-danger", 3000, false)
}
});
</script>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4" style="min-height: 100%;">
<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! -->

View File

@ -1,4 +1,22 @@
</main>
</div>
</div>
<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>

View 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>

View File

@ -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>
</html>

View File

@ -8,13 +8,20 @@
<title>AssetFlow - <%= it.title %></title>
<meta name="author" content="[Project-Name-Here]" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/logo/Design_icon.svg" type="image/svg+xml" />
<script src="/js/handleColorMode.js"></script>
<script src="/static/jquery/dist/jquery.min.js"></script>
<link href="/static/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<link href="/css/dashboard.css" rel="stylesheet">
<script src="/js/toastHandler.js"></script>
<script src="/js/confettiHeader.js"></script>
<link href="/static/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="/static/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet" />
<link href="/css/dashboard.css" rel="stylesheet" />
<script src="/static/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="/static/tsparticles-confetti/tsparticles.confetti.bundle.min.js"></script>
<link rel="stylesheet" href="/static/bootstrap-table/dist/bootstrap-table.min.css">
<script src="/static/bootstrap-table/dist/bootstrap-table.min.js"></script>
</head>
<body>
<!-- The body and html tag need to be left open! -->

View File

@ -2,10 +2,20 @@
<div class="text-center">
<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>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>

View File

View File

@ -1,10 +1,17 @@
import { Signale } from 'signale';
import ConfigHandler from './assets/configHandler';
import express, { Request, Response } from 'express';
import express, { NextFunction, Request, Response } from 'express';
import fileUpload from 'express-fileupload';
import { PrismaClient } from '@prisma/client';
import { Status, Category } from '@prisma/client';
import * as eta from 'eta';
import bodyParser from 'body-parser';
import session from 'express-session';
import passport from 'passport';
import _ from 'lodash';
// Sentry
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import routes from './routes/index.js';
@ -16,24 +23,48 @@ const logger_settings = {
logLevel: 'info',
scope: 'Core',
stream: process.stdout,
displayFilename: true
displayFilename: false
};
const coreLogger = new Signale(logger_settings);
export const log = {
core: coreLogger,
db: coreLogger.scope('DB'),
web: coreLogger.scope('WEB')
web: coreLogger.scope('WEB'),
auth: coreLogger.scope('AUTH'),
helper: coreLogger.scope('HELPER')
};
// Create a new config instance.
export const config = new ConfigHandler(__path + '/config.json', {
export const config = new ConfigHandler(__path + '/config.json', true, {
db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE',
http_listen_address: '127.0.0.1',
http_port: 3000,
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({
datasources: {
db: {
@ -43,18 +74,65 @@ export const prisma = new PrismaClient({
});
export const app = express();
app.set('x-powered-by', false);
app.set('strict routing', true);
app.engine('html', eta.renderFile);
app.use(fileUpload());
// Configure static https://expressjs.com/de/starter/static-files.html
// app.use('/static', express.static('public'));
Sentry.init({
dsn: config.global.sentry_dsn,
integrations: [
// enable HTTP calls tracing
new Sentry.Integrations.Http({ tracing: true }),
// 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((req, res, next) => {
res.status(404).send("Sorry can't find that!");
}); */
//routes(app);
app.use(routes);
app.listen(config.global.http_port, config.global.http_listen_address, () => {

21
src/middleware/auth.mw.ts Normal file
View 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()
// }

View File

@ -1,11 +1,11 @@
import express from 'express';
// Route imports
import testRoute from './test.js';
import v1_routes from './v1/index.js';
// 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;

View File

@ -1,5 +0,0 @@
import express, { Request, Response } from 'express';
export default (req: Request, res: Response) => {
res.status(200).send('API Test Successful!');
};

View 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 };

View 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 };

View 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
View 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 };

View 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;

View 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 };

View 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 };

View 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 };

View 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 };

View 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
View 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
View 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
View 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 };

View File

@ -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;

View File

@ -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');
};

View File

@ -2,12 +2,25 @@ import { Request, Response } from 'express';
import { prisma, __path } from '../../index.js';
import * as Eta from 'eta';
export default (req: Request, res: Response) => {
// retrieve data from database using id from url
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) => {
@ -19,4 +32,6 @@ export default (req: Request, res: Response) => {
res.send('Item not found');
}
});
};
}
export default { get };

View File

@ -1,28 +1,25 @@
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) => {
// 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!)
if (req.originalUrl !== '/') {
res.status(404).render(__path + '/src/frontend/errors/404.eta.html', { url: req.originalUrl });
} else {
prisma.item
.findMany({
orderBy: {
updatedAt: 'desc'
},
// Limit to 10 items
take: 10
})
.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 });
function get(req: Request, res: Response) {
prisma.item
.findMany({
orderBy: {
updatedAt: 'desc'
},
// Limit to 10 items
take: 10
})
.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) => {
log.db.error(err);
res.status(500).render(__path + '/src/frontend/errors/dbError.eta.html', { error: err });
});
}
export default { get };

View File

@ -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');
}
};

View File

@ -2,19 +2,23 @@ import express from 'express';
// Route imports
import skuRoute from './:id.js';
import skuRouteDash from './itemInfo.js'
import testRoute from './test.js';
import dashboardRoute from './dashboard.js';
import csvImportRoute from './import/csvImport.js';
import listAllItems from './listAllItems.js';
import itemsRoute from './items.js';
import manage_routes from './manage/index.js';
// Router base is '/'
const Router = express.Router({ strict: false });
Router.route('/test').get(testRoute.get);
Router.route('/items').get(itemsRoute.get);
Router.use('/test', testRoute);
Router.use('/allItems', listAllItems)
Router.use('/import/csv', csvImportRoute);
Router.use('/:id(\\w{8})', skuRoute);
Router.use('/', dashboardRoute);
Router.route('/:id(\\w{8})').get(skuRoute.get);
Router.route('/s/:id').get(skuRouteDash.get);
Router.use('/manage', manage_routes);
Router.route('/').get(dashboardRoute.get);
export default Router;

View 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 };

View 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 };

View File

@ -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 });
});
};

View 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 };

View 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 };

View 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 };

View 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;

View 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 };

View 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 };

View 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 };

View File

@ -1,4 +1,7 @@
import express, { Request, Response } from 'express';
export default (req: Request, res: Response) => {
res.status(200).send("Frontend Test Successful!");
};
import { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.status(200).send('Frontend Test Successful!');
}
export default { get };

View File

@ -1,16 +1,35 @@
import express, { Express } from 'express';
import { __path, prisma } from '../index.js';
import * as Sentry from '@sentry/node';
// Middleware Imports
import { checkAuthentication } from '../middleware/auth.mw.js'
// Route imports
import frontend_routes from './frontend/index.js';
import static_routes from './static/index.js';
import api_routes from './api/index.js';
import dev_routes from './dev/index.js';
import auth_routes from './auth/index.js';
const Router = express.Router({ strict: false });
Router.use('/static', static_routes);
Router.use('/api', api_routes);
Router.use('/dev', dev_routes); // This is just for development. ToDo: Add check if we are in devmode.
Router.use('/', frontend_routes);
Router.use('/api', checkAuthentication, api_routes);
Router.use('/auth', auth_routes);
Router.use('/', checkAuthentication, frontend_routes);
// The error handler must be before any other error middleware and after all controllers
Router.use(Sentry.Handlers.errorHandler());
// Default route.
Router.all('*', function (req, res) {
// TODO: Respond based on content-type (with req.is('application/json'))
if (req.is('application/json')) {
res.status(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;

View File

@ -11,7 +11,7 @@ const allowedURLs: Array<string> = JSON.parse(fs.readFileSync('allowedStaticPath
const recordedURLs: Array<string> = [];
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) {
res.sendFile(Path.join(__path, 'node_modules', req.params[0]));
recordedURLs.push(req.params[0]);

View File

@ -2,29 +2,58 @@ body {
font-size: 0.875rem;
}
.headLogo {
width: 5%;
}
/* Give the logo a dark shadow to make it pop out */
.headLogo {
filter: drop-shadow(0 0 0.85rem rgba(0, 0, 0, 0.35));
}
@-moz-document url-prefix() {
.headLogo {
width: 40%;
}
}
.versionInfo {
font-size: 0.75rem;
}
/** Safari */
@media not all and (min-resolution:.001dpcm) {
@supports (-webkit-appearance:none) and (stroke-color:transparent) {
.headLogo {
width: 1%;
}
}
}
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 0;
/* rtl:raw:
right: 0;
*/
top: 1.5rem;
bottom: 0;
/* rtl:remove */
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
z-index: 100;
/* Behind the navbar */
padding: 48px 0 0;
/* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
}
/*
@media (max-width: 767.98px) {
.sidebar {
top: 5rem;
}
}
}*/
.sidebar-sticky {
position: relative;
@ -32,17 +61,19 @@ body {
height: calc(100vh - 48px);
padding-top: 0.5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
overflow-y: auto;
/* Scrollable contents if viewport is shorter than content. */
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
/* color: #333; */
color: var(--bs-body-color);
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #727272;
color: var(--bs-body-color);
}
.sidebar .nav-link.active {
@ -59,18 +90,20 @@ body {
text-transform: uppercase;
}
#sidebarMenu {
background-color: var(--bs-secondary-bg);
}
/*
* Navbar
*/
.navbar-brand {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 1rem;
background-color: rgba(0, 0, 0, 0.25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25);
}
*/
.navbar .navbar-toggler {
top: 0.25rem;
right: 1rem;
@ -83,8 +116,6 @@ body {
}
.form-control-dark {
color: #fff;
background-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);
}
/*
.autocomplete-items {
position: absolute;
z-index: 99;
@ -100,3 +132,60 @@ body {
left: 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
View 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;
}

View File

@ -0,0 +1,13 @@
function randomInRange(min, max) {
return Math.random() * (max - min) + min;
}
function doTheConfetti() {
// Create confetti
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
View 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()

99
static/js/editItems.js Normal file
View File

@ -0,0 +1,99 @@
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 triggerDuplicationDialog(sourceItemId) {
// Clear the form
$('.form-control').val('');
const form = document.getElementById('ItemModalForm');
document.getElementById('itemModifyModalLabel').innerText= "Duplicate an item";
form.setAttribute('method', 'POST');
getDataForEdit(sourceItemId);
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
View 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
View 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.")

View 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();
})();

View 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');
});
});

View File

@ -0,0 +1,68 @@
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-primary" data-bs-toggle="modal" data-bs-target="#itemModifyModal" onclick="triggerDuplicationDialog('${item.id}')">
<i class="bi bi-copy"></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()

View File

@ -1,25 +1,112 @@
document.getElementById("SearchBox").addEventListener("keyup", handleSearchChange);
const autocompleteBox = document.getElementById("autocomplete-items");
autocompleteBox.style.display = "none";
document.getElementById('SearchBoxInput').addEventListener('keyup', handleSearchChange);
document.getElementById('searchForm').addEventListener('submit', handleSearchSubmit);
document.addEventListener('keyup', handleHotKey)
const autocompleteBox = document.getElementById('autocompletBody');
autocompleteBox.style.display = 'none';
currentBestGuessCommand = '';
function handleSearchChange(e) {
console.log(e.target.value);
// document.getElementById("SearchBox").setAttribute("data-bs-content", "Search results will show up here soon")
// return; // No you won't. I'm not done yet.
// Check if known prefix is used (either > or #)
if(e.target.value != "" ) {
autocompleteBox.style.display = "block";
autocompleteBox.innerHTML = "Search results will show up here soon <br> Trust me <br> Results";
if (e.target.value != '') {
autocompleteBox.style.display = 'block';
autocompleteBox.innerHTML = 'Search results will show up here soon <br> Trust me <br> Results';
} else {
autocompleteBox.style.display = "none";
autocompleteBox.style.display = 'none';
}
if (e.target.value[0] == ">") {
autocompleteBox.innerHTML = "Start typing to search for commands <br> >goto items";
if(e.target.value == ">goto items") {
autocompleteBox.innerHTML = "<a href='/allItems'>Goto Items</a>";
if (e.target.value[0] == '>') {
// List of valid routes
urlList = {
items: { url: '/items?page=1', alias: ['item'] },
'storage locations': { url: '/manage/storages', alias: ['locations', 'storage'] },
'storage units': { url: '/manage/storages#storage-unit-tab', alias: ['units'] },
categories: { url: '/manage/categories', alias: ['category'] }
};
autocompleteBox.innerHTML = 'Start typing to search for commands <br> >goto items';
const args = e.target.value.split(' ');
console.log(args);
if (args.length > 1) {
if (args[0] == '>goto' || args[0] == '>g') {
console.log('Handling >goto');
autocompleteBox.innerHTML = 'Start typing to search for commands <br>' + Object.keys(urlList).join('<br>') + '<br>';
if (args.length >= 2) {
console.log("Autocomplete for 'goto' command with " + args[1] + " as the second argument")
// Check if the second argument matches the urlList or any of its aliases
for (const [key, value] of Object.entries(urlList)) {
console.log(key, value)
if (args[1] == key || value.alias.includes(args[1])) {
// Match found
console.log('Match found');
autocompleteBox.innerHTML = `Go to <a href="${value.url}">${key}</a>`;
currentBestGuessCommand = "open;" + value.url;
break;
} else {
currentBestGuessCommand = '';
}
}
}
}
}
} else if (e.target.value[0] == "#") {
} else if (e.target.value[0] == '#') {
// Search for SKU
autocompleteBox.innerHTML = "Start typing to search for items by SKU";
const searchedSKU = e.target.value.substring(1);
if(searchedSKU == '') {
autocompleteBox.innerHTML = 'Start typing to search for commands <br> #SKU';
return;
}
const baseURI = window.location.origin; // move to new fancy route
const url = baseURI + '/api/v1/search/sku?sku=' + searchedSKU;
$.ajax({
type: 'get',
url: url,
success: function (result) {
let htmlResult = ""
result.forEach(element => {
console.log(element);
htmlResult += `<a href="/s/${element.SKU}">${element.name}</a><br>`
});
autocompleteBox.innerHTML = htmlResult;
},
error: function (data) {
createNewToast('<i class="bi bi-exclamation-triangle-fill"></i> Something went wrong while searching...', "text-bg-danger", autoHideTime = 3000, autoReload = false)
}
});
} else {
// Search for name
}
}
function handleSearchSubmit(e) {
console.log('Search submitted');
if(currentBestGuessCommand != '') {
console.log('Submitting command ' + currentBestGuessCommand);
cmdArgs = currentBestGuessCommand.split(';');
if(cmdArgs[0] == 'open') {
// Open the url in the current tab
setTimeout(() => {
window.location.replace(cmdArgs[1]);
}, 200);
return false;
}
}
return false;
}
function handleHotKey(e) {
// If c is pressed, focus on the search box
if(e.key == 'c' && e.altKey && e.ctrlKey) {
// Show search_modal modal
bootstrap.Modal.getOrCreateInstance($('#search_modal')).show()
document.getElementById('SearchBoxInput').focus();
}
}

76
static/js/toastHandler.js Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

35
tools/generate.js Normal file
View 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))