From d1d717a98866c5aa2af0441b41dde8436165b6bc Mon Sep 17 00:00:00 2001 From: grey Date: Tue, 2 May 2023 18:23:47 +0200 Subject: [PATCH] - added csv import - added somewhat real stats to dashboard - basic search logic --- package-lock.json | 86 ++++++++++++++++ package.json | 3 + prisma/schema.prisma | 5 +- src/frontend/demopage.eta.html | 35 +++---- src/frontend/imports/csvImport.eta.html | 28 ++++++ src/frontend/partials/controls.eta.html | 3 +- src/frontend/partials/controlsFoot.eta.html | 1 + src/frontend/partials/head.eta.html | 2 +- src/index.ts | 2 + src/routes/frontend/etaTest.ts | 14 ++- src/routes/frontend/import/csvImport.ts | 106 ++++++++++++++++++++ src/routes/frontend/index.ts | 2 + static/js/searchBox.js | 12 +++ 13 files changed, 270 insertions(+), 29 deletions(-) create mode 100644 src/frontend/imports/csvImport.eta.html create mode 100644 src/routes/frontend/import/csvImport.ts create mode 100644 static/js/searchBox.js diff --git a/package-lock.json b/package-lock.json index 830866d..24f1cfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,10 @@ "@prisma/client": "^4.13.0", "bootstrap": "^5.3.0-alpha3", "bootstrap-icons": "^1.10.5", + "csv": "^6.2.11", "eta": "^2.0.1", "express": "^4.18.2", + "express-fileupload": "^1.4.0", "jquery": "^3.6.4", "lodash": "^4.17.21", "prisma": "^4.13.0", @@ -22,6 +24,7 @@ }, "devDependencies": { "@types/express": "^4.17.17", + "@types/express-fileupload": "^1.4.1", "@types/lodash": "^4.14.194", "@types/signale": "^1.4.4", "eslint": "^8.39.0", @@ -249,6 +252,15 @@ "@types/node": "*" } }, + "node_modules/@types/busboy": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.0.tgz", + "integrity": "sha512-ncOOhwmyFDW76c/Tuvv9MA9VGYUCn8blzyWmzYELcNGDb0WXWLSmFi7hJq25YdRBYJrmMBB5jZZwUjlJe9HCjQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -270,6 +282,16 @@ "@types/serve-static": "*" } }, + "node_modules/@types/express-fileupload": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@types/express-fileupload/-/express-fileupload-1.4.1.tgz", + "integrity": "sha512-sbl865h1Sser6SF+efpw2F/+roGISj+PRIbMcGXbtzgJQCBAeeBmoSo7sPge/mBa22ymCHfFPtHFsag/wUxwfg==", + "dev": true, + "dependencies": { + "@types/busboy": "*", + "@types/express": "*" + } + }, "node_modules/@types/express-serve-static-core": { "version": "4.17.34", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz", @@ -467,6 +489,17 @@ "concat-map": "0.0.1" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -552,6 +585,35 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/csv": { + "version": "6.2.11", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.11.tgz", + "integrity": "sha512-MZR5oNS0UvNne7nWsI+VaEI21RuZVrjfmhfihCalIz1n2jcst5h+mUCEgDWSR3sy5lVJhETp5g0s9SXWBLFH8w==", + "dependencies": { + "csv-generate": "^4.2.5", + "csv-parse": "^5.3.9", + "csv-stringify": "^6.3.3", + "stream-transform": "^3.2.5" + }, + "engines": { + "node": ">= 0.1.90" + } + }, + "node_modules/csv-generate": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.5.tgz", + "integrity": "sha512-CS4JQizdciP9dMGYz/wRm70+hbVGIYP+Hcx/kNR8eicAPZ3MUeq9G6AE8KqMZcZdcFv53m7gLpOaw2eg7nPP/w==" + }, + "node_modules/csv-parse": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.9.tgz", + "integrity": "sha512-Nuh09OE1+wG6x5Lu2T+woxlupPAnWJ6Wj9XVYK74gP646e5gDrUsrCws1zz5NbckpQ+jygnxb8xDLj3gfBxi3w==" + }, + "node_modules/csv-stringify": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.3.3.tgz", + "integrity": "sha512-bL926F/b6oJieWPt0njp3vfu8jySbPsDxCL4VUd9xHsJt3H/YJXLdzqOkh8y5l+1cBYOCEYbHNZ7ipVCfWzYSw==" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1015,6 +1077,17 @@ "node": ">= 0.10.0" } }, + "node_modules/express-fileupload": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.4.0.tgz", + "integrity": "sha512-RjzLCHxkv3umDeZKeFeMg8w7qe0V09w3B7oGZprr/oO2H/ISCgNzuqzn7gV3HRWb37GjRk429CCpSLS2KNTqMQ==", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2094,6 +2167,19 @@ "node": ">= 0.8" } }, + "node_modules/stream-transform": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.5.tgz", + "integrity": "sha512-B0p1gG7P0pFgVo4/xGACKC4IqcK+9DNCpkt+Jy/d9wVnbRxxJX1zQbJelBCsJ6H1mcfBt598T+JFP0DuA7NiHg==" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index eca0116..f4e9b1d 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,10 @@ "@prisma/client": "^4.13.0", "bootstrap": "^5.3.0-alpha3", "bootstrap-icons": "^1.10.5", + "csv": "^6.2.11", "eta": "^2.0.1", "express": "^4.18.2", + "express-fileupload": "^1.4.0", "jquery": "^3.6.4", "lodash": "^4.17.21", "prisma": "^4.13.0", @@ -30,6 +32,7 @@ }, "devDependencies": { "@types/express": "^4.17.17", + "@types/express-fileupload": "^1.4.1", "@types/lodash": "^4.14.194", "@types/signale": "^1.4.4", "eslint": "^8.39.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9a4177..cf8a593 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,7 +19,7 @@ enum Status { model Item { id Int @id @default(autoincrement()) - SKU String @unique + SKU String? @unique Amount Int Comment String? name String @@ -31,6 +31,7 @@ model Item { storageLocationId Int? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + importedBy String? } model StorageLocation { @@ -54,7 +55,7 @@ model StorageBuilding { model Category { id Int @id @default(autoincrement()) - name String + name String @unique description String? Item Item[] } diff --git a/src/frontend/demopage.eta.html b/src/frontend/demopage.eta.html index 03ecb57..7d1a223 100644 --- a/src/frontend/demopage.eta.html +++ b/src/frontend/demopage.eta.html @@ -15,8 +15,8 @@
-
10
-

Uncategorized items

+
<%= it.stats.total %>
+

Items in total

@@ -44,27 +44,16 @@ - - 1 - Samsung 65W USB-C Ladegerät - Normal - DB8031E8 - Edit - - - 2 - 2.5mm Klinken Verlängerung 40mm - Normal - 25A1E9DE - Edit - - - 3 - Elgato Streamdeck XL - Stolen - 9AF2388E - Edit - + + <% it.recents.forEach(function(user){ %> + + <%= user.id %> + <%= user.name %> + <%= user.status %> + <%= user.SKU %> + Edit + + <% }) %>
diff --git a/src/frontend/imports/csvImport.eta.html b/src/frontend/imports/csvImport.eta.html new file mode 100644 index 0000000..82e58c5 --- /dev/null +++ b/src/frontend/imports/csvImport.eta.html @@ -0,0 +1,28 @@ +<%~ E.includeFile("../partials/head.eta.html", {"title": "Bootstrap Demo Page" }) %> + <%~ E.includeFile("../partials/controls.eta.html", {"active": "Orders" }) %> + +

Import A CSV File

+ Upload a CSV file to import into the database. The CSV file must have the following columns: + + The following columns are optional: + +
+ +
+ + + +
+ + + <%~ E.includeFile("../partials/controlsFoot.eta.html") %> + <%~ E.includeFile("../partials/foot.eta.html") %> \ No newline at end of file diff --git a/src/frontend/partials/controls.eta.html b/src/frontend/partials/controls.eta.html index 0e9a137..7ff996b 100644 --- a/src/frontend/partials/controls.eta.html +++ b/src/frontend/partials/controls.eta.html @@ -10,14 +10,13 @@ aria-label="Toggle navigation"> - + -
+ diff --git a/src/frontend/partials/head.eta.html b/src/frontend/partials/head.eta.html index 4e4e809..ce9578c 100644 --- a/src/frontend/partials/head.eta.html +++ b/src/frontend/partials/head.eta.html @@ -15,7 +15,7 @@ - + \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c790a70..ed18f3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { Signale } from 'signale'; import ConfigHandler from './assets/configHandler'; import express, { 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'; @@ -43,6 +44,7 @@ export const prisma = new PrismaClient({ export const app = express(); app.engine("html", eta.renderFile) +app.use(fileUpload()); // Configure static https://expressjs.com/de/starter/static-files.html // app.use('/static', express.static('public')); app.use(express.static(__path + '/static')); diff --git a/src/routes/frontend/etaTest.ts b/src/routes/frontend/etaTest.ts index c3eb0af..9d8f614 100644 --- a/src/routes/frontend/etaTest.ts +++ b/src/routes/frontend/etaTest.ts @@ -2,5 +2,17 @@ import express, { Request, Response } from 'express'; import { prisma, __path } from '../../index.js'; export default (req: Request, res: Response) => { - res.render(__path + '/src/frontend/demopage.eta.html'); + 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/demopage.eta.html', { recents: items, stats: { total: count } }); + }); + }); + // res.render(__path + '/src/frontend/demopage.eta.html'); }; \ No newline at end of file diff --git a/src/routes/frontend/import/csvImport.ts b/src/routes/frontend/import/csvImport.ts new file mode 100644 index 0000000..8118577 --- /dev/null +++ b/src/routes/frontend/import/csvImport.ts @@ -0,0 +1,106 @@ +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(); + 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); + }); + // Log categories + log.web.debug(categories); + + const categoryPromises: PrismaPromise[] = []; + 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'); + } + +}; \ No newline at end of file diff --git a/src/routes/frontend/index.ts b/src/routes/frontend/index.ts index 489385c..f20cfe3 100644 --- a/src/routes/frontend/index.ts +++ b/src/routes/frontend/index.ts @@ -4,6 +4,7 @@ import express from 'express'; import skuRoute from './:id.js'; import testRoute from './test.js'; import etaTestRoute from './etaTest.js'; +import csvImportRoute from './import/csvImport.js'; // Router base is '/' const Router = express.Router(); @@ -11,6 +12,7 @@ const Router = express.Router(); Router.use('/etaTest', etaTestRoute); Router.use('/:id(\\w{8})', skuRoute); Router.use('/test', testRoute); +Router.use('/import/csv', csvImportRoute); export default Router; diff --git a/static/js/searchBox.js b/static/js/searchBox.js new file mode 100644 index 0000000..4a821e4 --- /dev/null +++ b/static/js/searchBox.js @@ -0,0 +1,12 @@ +document.getElementById("SearchBox").addEventListener("keyup", handleSearchChange); +function handleSearchChange(e) { + console.log(e.target.value); + // Check if known prefix is used (either > or #) + if (e.target.value[0] == ">") { + // Search for commands + } else if (e.target.value[0] == "#") { + // Search for SKU + } else { + // Search for name + } +} \ No newline at end of file