stock mode

Former-commit-id: 4d13d0bb063d9c69ab8e581f7ecc16e82236d24a
This commit is contained in:
Ben
2022-04-29 16:59:33 +01:00
parent d5bf2f03cd
commit a375acf587
15 changed files with 346 additions and 55 deletions

BIN
README.md

Binary file not shown.

View File

@@ -73,7 +73,7 @@ class Checkout extends Component {
</div> </div>
<div class="checkout-place-order"> <div class="checkout-place-order">
<button class="checkout-place-order-button">Buy £${this.state.subtotal - this.state.discount}</button> <button class="checkout-place-order-button">Buy £${parseFloat(this.state.subtotal - this.state.discount).toFixed(2)}</button>
</div> </div>
</div> </div>
<div class="checkout-body-right"> <div class="checkout-body-right">

View File

@@ -29,7 +29,7 @@ export class Component extends HTMLElement {
// Override these // Override these
OnMount() { } OnMount() { }
Update() { } Update(attributeChanged, newState) { }
Render() { Component.__WARN('Render'); } Render() { Component.__WARN('Render'); }
OnRender() { } OnRender() { }
OnUnMount() { } OnUnMount() { }
@@ -78,9 +78,12 @@ export class Component extends HTMLElement {
attributeChangedCallback(name, newValue) { attributeChangedCallback(name, newValue) {
console.log(`attribute changed: ${name} ${newValue}`); console.log(`attribute changed: ${name} ${newValue}`);
this.setState({ ...this.state, [name]: newValue }); this.Update(Object.bind(this, name, {
this.Update(); ...this.state,
this.__INVOKE_RENDER(); [name]: newValue,
}));
this.setState({ ...this.state, [name]: newValue }, false);
this.__INVOKE_RENDER(Object.bind(this));
} }
get getState() { get getState() {

View File

@@ -47,17 +47,11 @@
.menu-content { .menu-content {
max-width: fit-content; max-width: fit-content;
display: none; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
min-width: 0;
margin-top: 1em;
}
.details-open {
display: flex;
position: static;
width: auto; width: auto;
margin-top: 1em;
} }
.product-details-content-item { .product-details-content-item {
@@ -67,4 +61,4 @@
.menu-content-item { .menu-content-item {
width: 100%; width: 100%;
margin-bottom: 1em; margin-bottom: 1em;
} }

View File

@@ -93,6 +93,12 @@ class NavBar extends Component {
const admin = e.value; const admin = e.value;
if (admin) { if (admin) {
this.root.querySelector('.stock-mode').style.display = 'flex'; this.root.querySelector('.stock-mode').style.display = 'flex';
if (localStorage.getItem('stock-mode') === 'true') {
this.root.querySelector('.stock-slider').checked = true;
} else {
this.root.querySelector('.stock-slider').checked = false;
}
} else { } else {
this.root.querySelector('.stock-mode').style.display = 'none'; this.root.querySelector('.stock-mode').style.display = 'none';
} }

View File

@@ -38,20 +38,22 @@ class ProductList extends Component {
if (!augmentable) { if (!augmentable) {
return ''; return '';
} }
return /* html */` // next time :(
<div class="augment-panel"> // return /* html */`
<div class="augment-panel-header"> // <div class="augment-panel">
<span class="augment-panel-header-text">Refine results</span> // <div class="augment-panel-header">
</div> // <span class="augment-panel-header-text">Refine results</span>
<div class="augment-panel-body"> // </div>
<div class="augment-panel-body-row"> // <div class="augment-panel-body">
<span class="augment-panel-body-row-text">Weight</span> // <div class="augment-panel-body-row">
<span class="augment-panel-body-row-text">Tag</span> // <span class="augment-panel-body-row-text">Weight</span>
<span class="augment-panel-body-row-text"></span> // <span class="augment-panel-body-row-text">Tag</span>
</div> // <span class="augment-panel-body-row-text"></span>
</div> // </div>
</div> // </div>
`; // </div>
// `;
return '';
} }
Render() { Render() {

View File

@@ -1,5 +1,7 @@
import { RegisterComponent, Component, SideLoad } from './components.mjs'; import { RegisterComponent, Component, SideLoad } from './components.mjs';
import { AddProductToBasket } from '../basket.mjs'; import { AddProductToBasket } from '../basket.mjs';
import * as LocalStorageListener from '../localstorage-listener.mjs';
import * as Auth from '../auth.mjs';
class ProductListing extends Component { class ProductListing extends Component {
static __IDENTIFY() { return 'product-listing'; } static __IDENTIFY() { return 'product-listing'; }
@@ -142,11 +144,22 @@ class ProductListing extends Component {
<div class="product-quantity-selector"> <div class="product-quantity-selector">
<button class="product-quantity-button reduce-quantity" type="button">-</button> <button class="product-quantity-button reduce-quantity" type="button">-</button>
<input class="quantity-input" type="number" value="1" min="1" max="{this.state.stock}"> <input class="quantity-input" type="number" value="${this.state.stock < 1 ? '0' : '1'}" min="0" max="{this.state.stock}">
<button class="product-quantity-button increase-quantity" type="button">+</button> <button class="product-quantity-button increase-quantity" type="button">+</button>
<span class="product-quantity">&nbsp;{this.state.stock} in stock</span> <span class="product-quantity">&nbsp;{this.state.stock} in stock</span>
</div> </div>
${localStorage.getItem('stock-mode') === 'true'
? /* html */`
<div class="product-stock-mode">
<span class="product-stock-mode-text">Change Stock</span>
<input class="product-stock-mode-input" type="number" value="{this.state.stock}" min="0">
<button class="product-stock-mode-button" type="button">Update</button>
</div>
`
: ''
}
<div class="product-add-to-basket"> <div class="product-add-to-basket">
<button class="add-to-basket-button">Add to Basket</button> <button class="add-to-basket-button">Add to Basket</button>
<!-- <img class="add-to-favorites-button" src="https://www.svgrepo.com/show/25921/heart.svg" width="45px" stroke="#222" stroke-width="2px" alt="Add to Favorites" title="Add to Favorites"> --> <!-- <img class="add-to-favorites-button" src="https://www.svgrepo.com/show/25921/heart.svg" width="45px" stroke="#222" stroke-width="2px" alt="Add to Favorites" title="Add to Favorites"> -->
@@ -178,6 +191,37 @@ class ProductListing extends Component {
} }
OnRender() { OnRender() {
LocalStorageListener.ListenOnKey('stock-mode', () => {
this.__INVOKE_RENDER();
});
if (localStorage.getItem('stock-mode') === 'true') {
const updateStockButton = this.root.querySelector('.product-stock-mode-button');
updateStockButton.addEventListener('click', async () => {
const input = this.root.querySelector('.product-stock-mode-input');
const stock = parseInt(input.value);
this.state.stock = stock;
this.__INVOKE_RENDER();
// tell the server to update the stock
const productId = this.state.id;
const productType = this.state.type;
const params = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await Auth.GetToken()}`,
},
body: JSON.stringify({
new_stock_level: stock,
}),
};
fetch(`/api/auth/staff/stock/${productType}/${productId}`, params).then(response => response.json()).then(data => console.log(data));
});
}
const backButton = this.root.querySelector('.back-button-svg'); const backButton = this.root.querySelector('.back-button-svg');
backButton.addEventListener('click', () => { backButton.addEventListener('click', () => {
@@ -188,8 +232,16 @@ class ProductListing extends Component {
const quantityInput = this.root.querySelector('.quantity-input'); const quantityInput = this.root.querySelector('.quantity-input');
const increaseQuantityButton = this.root.querySelector('.increase-quantity'); const increaseQuantityButton = this.root.querySelector('.increase-quantity');
const reduceQuantityButton = this.root.querySelector('.reduce-quantity'); const reduceQuantityButton = this.root.querySelector('.reduce-quantity');
const addToBasket = this.root.querySelector('.add-to-basket-button');
quantityInput.value = 1; quantityInput.value = 1;
if (this.state.stock === 0) {
quantityInput.value = 0;
increaseQuantityButton.disabled = true;
reduceQuantityButton.disabled = true;
addToBasket.disabled = true;
addToBasket.innerText = 'Out of Stock';
}
quantityInput.addEventListener('change', () => { quantityInput.addEventListener('change', () => {
quantityInput.value = parseInt(quantityInput.value); quantityInput.value = parseInt(quantityInput.value);
@@ -225,8 +277,6 @@ class ProductListing extends Component {
})); }));
// add quantity to basket and then update the basket count // add quantity to basket and then update the basket count
const addToBasket = this.root.querySelector('.add-to-basket-button');
addToBasket.addEventListener('click', () => { addToBasket.addEventListener('click', () => {
// if it is a brick, get potential modifier from the drop down menu // if it is a brick, get potential modifier from the drop down menu
const brick = this.state.type === 'brick'; const brick = this.state.type === 'brick';

View File

@@ -12,14 +12,12 @@ class StockEditor extends Component {
template: /* html */` template: /* html */`
<div class="stock-editor"> <div class="stock-editor">
<div class="stock-header">Stock Editor</div> <div class="stock-header">Stock Editor</div>
<div class="collapsible-menu"> <div class="menu">
<div class="menu-header"> <div class="menu-header">
<span class="menu-header-text">Remove Item</span> <span class="menu-header-text">Remove Item</span>
<img class="menu-header-arrow" src="/res/back-arrow.svg" height="30em" alt="down-arrow">
</div> </div>
<div class="menu-content"> <div class="menu-content">
<div class="menu-content-item"> <div class="menu-content-item">
<span class="menu-content-item-text">Remove Item</span>
<input class="menu-content-item-id-input" type="text" placeholder="Item ID (1010)"> <input class="menu-content-item-id-input" type="text" placeholder="Item ID (1010)">
<input class="menu-content-item-type-input" type="text" placeholder="Item Type (brick/set)"> <input class="menu-content-item-type-input" type="text" placeholder="Item Type (brick/set)">
<button class="menu-content-item-button stock-lookup-button">Lookup</button> <button class="menu-content-item-button stock-lookup-button">Lookup</button>
@@ -32,12 +30,15 @@ class StockEditor extends Component {
</div> </div>
</div> </div>
</div> </div>
<div class="collapsible-menu"> <div class="menu">
<div class="menu-header"> <div class="menu-header">
<span class="menu-header-text">Add Item</span> <span class="menu-header-text">Add Item</span>
<img class="menu-header-arrow" src="/res/back-arrow.svg" height="30em" alt="down-arrow">
</div> </div>
<div class="menu-content"> <div class="menu-content">
<div class="menu-content-item drop-area">
<input type="file" class="file-select" id="file" accept="image/*">
<div class="selected-img"></div>
<div class="preview-img"></div>
</div> </div>
</div> </div>
`, `,
@@ -46,16 +47,6 @@ class StockEditor extends Component {
} }
OnRender() { OnRender() {
const collapseButton = this.root.querySelectorAll('.menu-header');
collapseButton.forEach(el => el.addEventListener('click', (e) => {
const parent = e.path[2].querySelector('.collapsible-menu') ? e.path[1] : e.path[2];
const collapseContent = parent.querySelector('.menu-content');
const collapseArrow = parent.querySelector('.menu-header-arrow');
collapseContent.classList.toggle('details-open');
collapseArrow.classList.toggle('menu-header-arrow-down');
}));
// remove // remove
const removeStockLookup = this.root.querySelector('.stock-lookup-button'); const removeStockLookup = this.root.querySelector('.stock-lookup-button');
removeStockLookup.addEventListener('click', () => { removeStockLookup.addEventListener('click', () => {
@@ -66,6 +57,76 @@ class StockEditor extends Component {
preview.setAttribute('id', id); preview.setAttribute('id', id);
preview.setAttribute('type', type); preview.setAttribute('type', type);
}); });
const removeStockButton = this.root.querySelector('.remove-stock-button');
removeStockButton.addEventListener('click', () => {
const preview = this.root.querySelector('.stock-remove-preview');
const id = preview.getAttribute('id');
const type = preview.getAttribute('type');
preview.removeAttribute('id');
preview.removeAttribute('type');
});
// add stock
const dropArea = this.root.querySelector('.drop-area');
const selectedImage = this.root.querySelector('.selected-img');
const previewImage = this.root.querySelector('.preview-img');
const fileSelect = this.root.querySelector('.file-select');
let file = null;
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
};
const updateSelected = () => {
selectedImage.innerText = 'Selected image: ' + file.name;
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => {
const img = this.root.createElement('img');
img.src = reader.result;
previewImage.append(img);
};
};
const handleSelect = (images) => {
console.log(images);
if (images.length) {
file = images[0];
updateSelected();
return;
}
file = images;
updateSelected();
};
document.onpaste = (event) => {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
let blob = null;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') === 0) {
blob = items[i].getAsFile();
}
}
handleSelect(blob);
};
const handleDrop = (e) => {
const dt = e.dataTransfer;
file = dt.files[0];
updateSelected();
};
fileSelect.addEventListener('onchange', handleSelect, false);
dropArea.addEventListener('dragenter', handleDrag, false);
dropArea.addEventListener('dragleave', handleDrag, false);
dropArea.addEventListener('dragover', handleDrag, false);
dropArea.addEventListener('drop', handleDrag, false);
dropArea.addEventListener('drop', handleDrop, false);
} }
} }

View File

@@ -8,7 +8,7 @@ class SuperCompactProductListing extends Component {
super(SuperCompactProductListing); super(SuperCompactProductListing);
} }
async Update() { async OnMount() {
if (!this.state.name || !this.state.price) { if (!this.state.name || !this.state.price) {
const product = (await fetch(`/api/${this.state.type}/${this.state.id}`).then(res => res.json())).data; const product = (await fetch(`/api/${this.state.type}/${this.state.id}`).then(res => res.json())).data;
const name = product.name; const name = product.name;
@@ -28,12 +28,7 @@ class SuperCompactProductListing extends Component {
colours, colours,
quantity: product.quantity, quantity: product.quantity,
}, false); }, false);
} } else if (this.state.tags) {
if (this.state.tags) {
if (this.state.tags.length >= 1) {
return;
}
const tags = JSON.parse(this.state.tags); const tags = JSON.parse(this.state.tags);
this.setState({ this.setState({
...this.getState, ...this.getState,
@@ -42,6 +37,16 @@ class SuperCompactProductListing extends Component {
} }
} }
Update(attributeChanged, newState) {
// console.log(attributeChanged, newState);
// if (attributeChanged === 'stock') {
// return;
// }
// if (newState.id !== this.state.id || newState.type !== this.state.type) {
// this.OnMount();
// }
}
Render() { Render() {
let modifierPreview = ''; let modifierPreview = '';
if (this.state.modifier) { if (this.state.modifier) {

View File

@@ -26,8 +26,8 @@ automatically every request
| GET | /api/auth/staff/orders/ | | ✔️ | All unshipped orders | | GET | /api/auth/staff/orders/ | | ✔️ | All unshipped orders |
| PUT | /api/auth/staff/order/:id | | ✔️ | Update order to shipped, recieved (carrier) | | PUT | /api/auth/staff/order/:id | | ✔️ | Update order to shipped, recieved (carrier) |
| PUT | /api/auth/staff/stock/:type/:id | | ✔️ | Update stock on item | | PUT | /api/auth/staff/stock/:type/:id | | ✔️ | Update stock on item |
| POST | /api/auth/staff/stock/ | | ✔️ | Add item to inventory | | POST | /api/auth/staff/stock/ | NOT IMPLEMENTED | ✔️ | Add item to inventory |
| DEL | /api/auth/staff/stock/:type/:id | | ✔️ | Remove item from inventory | | DEL | /api/auth/staff/stock/:type/:id | NOT IMPLEMENTED | ✔️ | Remove item from inventory |
Query endpoints do not return the full data on a brick/set, they return Query endpoints do not return the full data on a brick/set, they return
a subset for product listing pages a subset for product listing pages

View File

@@ -215,6 +215,48 @@ async function GetBrick(brickId) {
// U // U
async function RemoveBrickStock(brickIdList, quantityList) {
await Database.Query('BEGIN TRANSACTION;');
for (let i = 0; i < brickIdList.length; i++) {
const dbres = await Database.Query(`
UPDATE lego_brick_inventory
SET stock = stock - $2
WHERE brick_id = $1
`, [brickIdList[i], quantityList[i]]).catch(() => {
return {
error: 'Database error',
};
});
if (dbres.error) {
Database.Query('ROLLBACK TRANSACTION;');
Logger.Error(dbres.error);
return dbres;
}
}
Database.Query('COMMIT TRANSACTION;');
return true;
}
async function UpdateStock(brickId, newStock) {
await Database.Query('BEGIN TRANSACTION;');
const dbres = await Database.Query(`
UPDATE lego_brick_inventory
SET stock = $2
WHERE brick_id = $1
`, [brickId, newStock]).catch(() => {
return {
error: 'Database error',
};
});
if (dbres.error) {
Database.Query('ROLLBACK TRANSACTION;');
Logger.Error(dbres.error);
return dbres;
}
Database.Query('COMMIT TRANSACTION;');
return true;
}
// D // D
module.exports = { module.exports = {
@@ -222,4 +264,6 @@ module.exports = {
SumPrices, SumPrices,
GetBulkBricks, GetBulkBricks,
GetBrick, GetBrick,
RemoveBrickStock,
UpdateStock,
}; };

View File

@@ -229,6 +229,48 @@ async function GetSets(page, resPerPage) {
// U // U
async function RemoveSetStock(setIdList, quantityList) {
await Database.Query('BEGIN TRANSACTION;');
for (let i = 0; i < setIdList.length; i++) {
const dbres = await Database.Query(`
UPDATE lego_set_inventory
SET stock = stock - $1
WHERE set_id = $2;
`, [quantityList[i], setIdList[i]]).catch(() => {
return {
error: 'Database error',
};
});
if (dbres.error) {
Database.Query('ROLLBACK TRANSACTION;');
Logger.Error(dbres.error);
return dbres;
}
}
Database.Query('COMMIT TRANSACTION;');
return true;
}
async function UpdateStock(setId, newStock) {
await Database.Query('BEGIN TRANSACTION;');
const dbres = await Database.Query(`
UPDATE lego_set_inventory
SET stock = $1
WHERE set_id = $2;
`, [newStock, setId]).catch(() => {
return {
error: 'Database error',
};
});
if (dbres.error) {
Database.Query('ROLLBACK TRANSACTION;');
Logger.Error(dbres.error);
return dbres;
}
Database.Query('COMMIT TRANSACTION;');
return true;
}
// D // D
module.exports = { module.exports = {
@@ -236,4 +278,6 @@ module.exports = {
SumPrices, SumPrices,
GetSet, GetSet,
GetSets, GetSets,
RemoveSetStock,
UpdateStock,
}; };

View File

@@ -5,6 +5,7 @@ const Helpers = require('./helpers.js');
const CDN = require('./cdn.js'); const CDN = require('./cdn.js');
const Bricks = require('./bricks-router.js'); const Bricks = require('./bricks-router.js');
const Sets = require('./sets-router.js'); const Sets = require('./sets-router.js');
const Stock = require('./stock-router.js');
const Query = require('./query-router.js'); const Query = require('./query-router.js');
const Auth0 = require('./auth0-router.js'); const Auth0 = require('./auth0-router.js');
const Order = require('./order-router.js'); const Order = require('./order-router.js');
@@ -33,6 +34,7 @@ function Init() {
Server.App.get('/api/auth/staff/orders/', Auth0.JWTMiddleware, Auth0.AdminOnlyEndpoint, Order.GetUnFinishedOrders); Server.App.get('/api/auth/staff/orders/', Auth0.JWTMiddleware, Auth0.AdminOnlyEndpoint, Order.GetUnFinishedOrders);
Server.App.put('/api/auth/staff/order/:id', Auth0.JWTMiddleware, Auth0.AdminOnlyEndpoint, Order.UpdateOrderStatus); Server.App.put('/api/auth/staff/order/:id', Auth0.JWTMiddleware, Auth0.AdminOnlyEndpoint, Order.UpdateOrderStatus);
Server.App.put('/api/auth/staff/stock/:type/:id', Auth0.JWTMiddleware, Auth0.AdminOnlyEndpoint, Stock.Update);
Logger.Module('API', 'API Routes Initialized'); Logger.Module('API', 'API Routes Initialized');
} }

View File

@@ -136,6 +136,23 @@ async function ProcessNew(req, res) {
const order = await OrderController.NewOrder(userID, total, basket, discountCode, discount.discount); const order = await OrderController.NewOrder(userID, total, basket, discountCode, discount.discount);
if (newBrickList.length > 0) {
const stockRemovalBrick = await BrickController.RemoveBrickStock(newBrickList, newBrickQuantities);
if (stockRemovalBrick.error) {
return res.send({
error: stockRemovalBrick.error,
});
}
}
if (setList.length > 0) {
const stockRemovalSet = await SetController.RemoveSetStock(setList, setQuantities);
if (stockRemovalSet.error) {
return res.send({
error: stockRemovalSet.error,
});
}
}
if (order.error) { if (order.error) {
return res.send({ return res.send({
error: order.error, error: order.error,

View File

@@ -0,0 +1,63 @@
const BrickController = require('../controllers/brick-controller');
const SetController = require('../controllers/set-controller');
async function Update(req, res) {
const type = req.params.type;
const id = req.params.id;
console.log(req.params);
if (!type) {
return res.send({
error: 'No type in request',
});
}
if (!id) {
return res.send({
error: 'No id in request',
});
}
const data = req.body;
if (!data || !data.new_stock_level) {
return res.send({
error: 'No data in request',
});
}
if (type === 'brick') {
const stock = await BrickController.UpdateStock(id, data.new_stock_level);
if (stock.error) {
return res.send({
error: stock.error,
});
}
return res.send({
data: stock,
});
}
if (type === 'set') {
const stock = await SetController.UpdateStock(id, data.new_stock_level);
if (stock.error) {
return res.send({
error: stock.error,
});
}
return res.send({
data: stock,
});
}
return res.send({
error: 'Invalid type',
});
}
module.exports = {
Update,
};