- 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
This commit is contained in:
2023-07-08 00:09:54 +02:00
parent c026b5f1a8
commit 0233453084
15 changed files with 196 additions and 96 deletions

View File

@ -88,6 +88,26 @@ export function parseIntRelation(data: string, relation_name: string = 'id', doN
}`);
}
export function parseIntOrUndefined(data: string) {
/**
* 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

@ -93,13 +93,13 @@
<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">
<table class="table align-middle" id="itemList" data-sortable="true" data-search-highlight="true" data-pagination="true" data-page-size="25" data-remember-order="true">
<thead>
<tr>
<th scope="col">SKU</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
<th scope="col" data-field="SKU" class="sku" data-sortable="true">SKU</th>
<th scope="col" data-field="name" data-sortable="true">Name</th>
<th scope="col" data-field="status" data-sortable="true">Status</th>
<th scope="col" data-field="actions" data-searchable="false">Actions</th>
</tr>
</thead>
<% if(it.items.length == 0) { %>
@ -109,49 +109,8 @@
</tr>
</tbody>
<% } %>
<tbody>
<% it.items.forEach(function(user){ %>
<tr>
<td scope="row" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="ID: <%= user.id %>">
<% if (user.SKU == null) { %>
<i>No SKU assigned</i>
<% } else { %> <%= user.SKU %> <% } %></td>
<td><%= user.name %></td>
<% if(user.status == "normal") { %>
<td><span class="badge text-bg-success"><%= user.status %></span></td>
<% } else if(user.status == "stolen") { %>
<td><span class="badge text-bg-danger"><%= user.status %></span></td>
<% } else if(user.status == "lost") { %>
<td><span class="badge text-bg-warning"><%= user.status %></span></td>
<% } else if(user.status == "borrowed") { %>
<td><span class="badge text-bg-info"><%= user.status %></span></td>
<% } %>
<td>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#itemModifyModal" onclick="primeEdit(); getDataForEdit('<%= user.id %>')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-danger" onclick="preFillDeleteModalNxt('<%= user.id %>','items','Item')" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<% }) %>
</tbody>
</table>
<br />
<% if(it.maxPages > 1) { %>
<nav aria-label="Page selector">
<ul class="pagination justify-content-center">
<li class="page-item <%= it.currentPage-1 < 1 ? 'disabled' : ''%>"><a class="page-link" href="?page=<%= it.currentPage - 1 %>">Previous</a></li>
<% for (var i = 1; i <= it.maxPages; i++) { %>
<li class="page-item <%= it.currentPage == i ? 'active' : ''%>"><a class="page-link" href="?page=<%= i %>"><%= i %></a></li>
<% } %>
<li class="page-item <%= it.currentPage+1 > it.maxPages ? 'disabled' : ''%>"><a class="page-link" href="?page=<%= it.currentPage + 1 %>">Next</a></li>
</ul>
</nav>
<% } %>
</div>
<script src="/js/editItems.js"></script>
<script src="/js/itemPageHandler.js"></script>
<%~ E.includeFile("partials/controlsFoot.eta.html") %> <%~ E.includeFile("partials/foot.eta.html") %>

View File

@ -6,6 +6,17 @@
<script src="/js/handleSidebarTriangles.js"></script>
<script src="/js/formHandler.js"></script>
<script>
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
</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

@ -1,8 +1,2 @@
<script>
// Enable all bootstrap tooltips.
// https://getbootstrap.com/docs/5.3/components/tooltips/#enable-tooltips
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
</script>
</body>
</html>

View File

@ -8,7 +8,6 @@
<title>AssetFlow - <%= it.title %></title>
<meta name="author" content="[Project-Name-Here]" />
<!--<link rel="icon" href="/favicon.ico" />-->
<link rel="icon" href="/logo/Design_icon.svg" type="image/svg+xml" />
<script src="/js/handleColorMode.js"></script>
@ -21,7 +20,8 @@
<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

@ -6,6 +6,7 @@ import itemRoute from './items.js';
import categoryRoute from './categories.js';
import storageUnitRoute from './storageUnits.js';
import storageLocationRoute from './storageLocations.js';
import versionRoute from './version.js'
import search_routes from './search/index.js';
@ -27,7 +28,7 @@ Router.route('/categories').get(categoryRoute.get).post(categoryRoute.post).patc
// 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('/version').get(versionRoute.get);
Router.use('/search', search_routes);
Router.route('/test').get(testRoute.get);

View File

@ -1,16 +1,18 @@
import { Request, Response } from 'express';
import { prisma, __path, log } from '../../../index.js';
import { itemStatus } from '@prisma/client';
import { parseIntRelation, parseIntOrUndefined } from '../../../assets/helper.js';
import { parseIntRelation, parseIntOrUndefined, parseDynamicSortBy } from '../../../assets/helper.js';
// Get item.
function get(req: Request, res: Response) {
if (req.query.getAll === undefined) {
// Check if required fields are present
if (!req.query.id) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'One or more required fields are missing' });
return;
}
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.id) {
// Check if number is a valid integer
if (!Number.isInteger(parseInt(req.query.id.toString()))) {
res.status(400).json({ errorcode: 'VALIDATION_ERROR', error: 'The id field must be an integer' });
@ -48,8 +50,53 @@ function get(req: Request, res: Response) {
res.status(500).json({ errorcode: 'DB_ERROR', error: err });
});
} 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,
@ -67,7 +114,7 @@ function get(req: Request, res: Response) {
})
.then((items) => {
if (items) {
res.status(200).json(JSON.stringify(items));
res.status(200).json(JSON.stringify({ total: itemCountFiltered, totalNotFiltered: itemCountNotFiltered, items: items }));
} else {
res.status(410).json({ errorcode: 'NOT_EXISTING', error: 'Item does not exist' });
}
@ -97,7 +144,7 @@ function post(req: Request, res: Response) {
.create({
data: {
SKU: req.body.sku,
amount: parseIntOrUndefined(req.body.ammount), // FIXME: This is silently failing if NaN..
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
@ -167,7 +214,7 @@ async function patch(req: Request, res: Response) {
},
data: {
SKU: req.body.sku,
amount: parseIntOrUndefined(req.body.ammount), // FIXME: This is silently failing if NaN..
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

View File

@ -2,33 +2,13 @@ import { Request, Response } from 'express';
import { prisma, __path, log } from '../../index.js';
async function get(req: Request, res: Response) {
// If no page is provided redirect to first
if (req.query.page === undefined) {
res.redirect('?page=1');
return;
}
let page = parseInt(req.query.page.toString());
const itemCount = await prisma.item.count({}); // Count all items in the DB
const takeSize = 25; // Amount of times per page
const pageSize = Math.ceil(itemCount / takeSize); // Amount of pages, always round up
// If page is less then 1 or more then the max page size redirect to first or last page. If itemCount is 0 do not redirect.
if (page < 1) {
res.redirect('?page=1');
return;
} else if (page > pageSize && itemCount !== 0) {
res.redirect('?page=' + pageSize);
return;
}
prisma.item
.findMany({ skip: (page - 1) * takeSize, take: takeSize, orderBy: { SKU: "asc" } }) // Skip the amount of items per page times the page number minus 1; skip has to be (page-1)*takeSize because skip is 0 indexed
.findMany({}) // Skip the amount of items per page times the page number minus 1; skip has to be (page-1)*takeSize because skip is 0 indexed
.then((items) => {
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, currentPage: page, maxPages: pageSize, storeLocs: locations, categories: categories, contactInfo: contactInfo });
res.render(__path + '/src/frontend/items.eta.html', { items: items, storeLocs: locations, categories: categories, contactInfo: contactInfo });
})
});
});