order pages all done and stock pages coming
Former-commit-id: da90fc25deecf2843a6269d877387ce07fdb2728
This commit is contained in:
@@ -49,7 +49,6 @@ export async function InitAuth0() {
|
|||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
const user = await auth0.getUser();
|
const user = await auth0.getUser();
|
||||||
localStorage.setItem('user', user.given_name || user.nickname);
|
localStorage.setItem('user', user.given_name || user.nickname);
|
||||||
NotifyNavbar('login', user);
|
|
||||||
localStorage.setItem('loggedIn', true);
|
localStorage.setItem('loggedIn', true);
|
||||||
ready = true;
|
ready = true;
|
||||||
|
|
||||||
@@ -67,6 +66,7 @@ export async function InitAuth0() {
|
|||||||
const res = await fetch('/api/auth/login', fetchOptions).then(res => res.json());
|
const res = await fetch('/api/auth/login', fetchOptions).then(res => res.json());
|
||||||
|
|
||||||
localStorage.setItem('admin', res.user.admin);
|
localStorage.setItem('admin', res.user.admin);
|
||||||
|
NotifyNavbar('login', user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
70
client/public/components/css/stock-audit.css
Normal file
70
client/public/components/css/stock-audit.css
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
.stock-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-header {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 2em;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-menu {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: #1A1A1A solid 1px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-header-arrow {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
margin-left: 0.5em;
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rotate the arrow down when the details are open */
|
||||||
|
.menu-header-arrow-down {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content {
|
||||||
|
max-width: fit-content;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-width: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-open {
|
||||||
|
display: flex;
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details-content-item {
|
||||||
|
padding-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
@@ -54,8 +54,8 @@ class NavBar extends Component {
|
|||||||
<a class="nav-link" href="#">${localStorage.user}▾</a>
|
<a class="nav-link" href="#">${localStorage.user}▾</a>
|
||||||
<ul class="sub-nav" >
|
<ul class="sub-nav" >
|
||||||
<li><a class="sub-nav-link" href="/orders">My Orders</a></li>
|
<li><a class="sub-nav-link" href="/orders">My Orders</a></li>
|
||||||
<li><a class="sub-nav-link" href="">Add or Remove Stock</a></li>
|
<li><a class="sub-nav-link" href="/staff/stock">Add or Remove Stock</a></li>
|
||||||
<li><a class="sub-nav-link" href="">Review Open Orders</a></li>
|
<li><a class="sub-nav-link" href="/staff/revieworders">Review Open Orders</a></li>
|
||||||
<li><a class="sub-nav-link logout-button" href="#">Log Out</a></li>
|
<li><a class="sub-nav-link logout-button" href="#">Log Out</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -9,30 +9,48 @@ class OrderList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async OnMount() {
|
async OnMount() {
|
||||||
|
const doStaffList = this.state.staff !== undefined;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Authorization: `Bearer ${await Auth.GetToken()}` },
|
headers: { Authorization: `Bearer ${await Auth.GetToken()}` },
|
||||||
};
|
};
|
||||||
|
if (doStaffList) {
|
||||||
|
const res = await fetch('/api/auth/staff/orders', options).then(res => res.json());
|
||||||
|
|
||||||
const res = await fetch('/api/auth/orders', options).then(res => res.json());
|
this.setState({
|
||||||
|
...this.getState,
|
||||||
|
orders: res.data,
|
||||||
|
title: 'Orders left to fufill',
|
||||||
|
none: 'All done :)',
|
||||||
|
}, false);
|
||||||
|
} else {
|
||||||
|
const res = await fetch('/api/auth/orders', options).then(res => res.json());
|
||||||
|
|
||||||
console.log(res);
|
this.setState({
|
||||||
|
...this.getState,
|
||||||
this.setState({
|
orders: res.data,
|
||||||
...this.getState,
|
title: 'Your Orders',
|
||||||
orders: res.data,
|
none: 'You have no orders',
|
||||||
}, false);
|
}, false);
|
||||||
console.log(this.state);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Render() {
|
Render() {
|
||||||
return {
|
return {
|
||||||
template: /* html */`
|
template: /* html */`
|
||||||
<div class="order-header">
|
<div class="order-header">
|
||||||
<span class="order-header-title">Your Orders</span>
|
<span class="order-header-title">{this.state.title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="orders-list-body">
|
<div class="orders-list-body">
|
||||||
|
${this.state.orders.length === 0
|
||||||
|
? /* html */`
|
||||||
|
<div class="orders-list-item">
|
||||||
|
<span class="order-list-item-header-title">{this.state.none}</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
${this.state.orders.map(order => /* html */`
|
${this.state.orders.map(order => /* html */`
|
||||||
<div class="orders-list-item">
|
<div class="orders-list-item">
|
||||||
<a href="/orders/order?id=${order.id}"><div class="order-list-item">
|
<a href="/orders/order?id=${order.id}"><div class="order-list-item">
|
||||||
@@ -43,6 +61,12 @@ class OrderList extends Component {
|
|||||||
<div class="order-list-item-body">
|
<div class="order-list-item-body">
|
||||||
<span class="order-list-item-body-item-title">Paid: £${parseFloat(order.subtotal_paid).toFixed(2)}</span>
|
<span class="order-list-item-body-item-title">Paid: £${parseFloat(order.subtotal_paid).toFixed(2)}</span>
|
||||||
<span class="order-list-item-body-item-title">Shipped? ${order.shipped ? 'Yes' : 'No'}</span>
|
<span class="order-list-item-body-item-title">Shipped? ${order.shipped ? 'Yes' : 'No'}</span>
|
||||||
|
${this.state.staff !== undefined
|
||||||
|
? /* html */`
|
||||||
|
<span class="order-list-item-ship">Posted? <input type="checkbox" class="order-list-item-shipped-checker" ${order.shipped ? 'checked disabled' : ''} /></span>
|
||||||
|
<span class="order-list-item-done">Done & Recieved? <input type="checkbox" class="order-list-item-done-checker" ${order.recieved ? 'checked disabled' : ''} /></span>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
</div></a>
|
</div></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,6 +78,69 @@ class OrderList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnRender() {
|
OnRender() {
|
||||||
|
this.root.querySelectorAll('.order-list-item-shipped-checker').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('click', async (event) => {
|
||||||
|
const orderID = checkbox.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector('.order-list-item-header-title').innerText.split('#')[1];
|
||||||
|
const options = {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${await Auth.GetToken()}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: {
|
||||||
|
shipped: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const orderUpdate = await fetch(`/api/auth/staff/order/${orderID}`, options).then(res => res.json());
|
||||||
|
|
||||||
|
const getOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Authorization: `Bearer ${await Auth.GetToken()}` },
|
||||||
|
};
|
||||||
|
const res = await fetch('/api/auth/staff/orders', getOptions).then(res => res.json());
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.getState,
|
||||||
|
orders: res.data,
|
||||||
|
title: 'Orders left to fufill',
|
||||||
|
none: 'All done :)',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.root.querySelectorAll('.order-list-item-done-checker').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('click', async (event) => {
|
||||||
|
const orderID = checkbox.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector('.order-list-item-header-title').innerText.split('#')[1];
|
||||||
|
const options = {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${await Auth.GetToken()}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: {
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const orderUpdate = await fetch(`/api/auth/staff/order/${orderID}`, options).then(res => res.json());
|
||||||
|
|
||||||
|
const getOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Authorization: `Bearer ${await Auth.GetToken()}` },
|
||||||
|
};
|
||||||
|
const res = await fetch('/api/auth/staff/orders', getOptions).then(res => res.json());
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.getState,
|
||||||
|
orders: res.data,
|
||||||
|
title: 'Orders left to fufill',
|
||||||
|
none: 'All done :)',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Order extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async OnMount() {
|
async OnMount() {
|
||||||
// get order id from search param
|
// get order id from search param
|
||||||
const query = new URLSearchParams(window.location.search);
|
const query = new URLSearchParams(window.location.search);
|
||||||
const id = query.get('id');
|
const id = query.get('id');
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ class Order extends Component {
|
|||||||
...res,
|
...res,
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
console.log(this.state);
|
|
||||||
localStorage.setItem('viewing-order', JSON.stringify({ items: res.items }));
|
localStorage.setItem('viewing-order', JSON.stringify({ items: res.items }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,22 +85,22 @@ class Order extends Component {
|
|||||||
</div>
|
</div>
|
||||||
<div class="order-track-step">
|
<div class="order-track-step">
|
||||||
<span class="order-track-status">
|
<span class="order-track-status">
|
||||||
<div class="order-track-step-icon"></div>
|
<div class="order-track-step-icon ${this.state.shipped ? 'completed' : ''}"></div>
|
||||||
<div class="order-track-step-line"></div>
|
<div class="order-track-step-line ${this.state.shipped ? 'completed' : ''}"></div>
|
||||||
</span>
|
</span>
|
||||||
<div class="order-track-text">
|
<div class="order-track-text">
|
||||||
<span class="order-body-status-title">Posted</span>
|
<span class="order-body-status-title">Posted</span>
|
||||||
<span class="when"></span>
|
<span class="when">${this.state.shipped ? new Date(this.state.date_shipped).toDateString() : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-track-step">
|
<div class="order-track-step">
|
||||||
<span class="order-track-status">
|
<span class="order-track-status">
|
||||||
<div class="order-track-step-icon"></div>
|
<div class="order-track-step-icon ${this.state.recieved ? 'completed' : ''}"></div>
|
||||||
<div class="order-track-step-line"></div>
|
<div class="order-track-step-line ${this.state.recieved ? 'completed' : ''}"></div>
|
||||||
</span>
|
</span>
|
||||||
<div class="order-track-text">
|
<div class="order-track-text">
|
||||||
<span class="order-body-status-title">Delivered</span>
|
<span class="order-body-status-title">Delivered</span>
|
||||||
<span class="when"></span>
|
<span class="when">${this.state.recieved ? new Date(this.state.date_recieved).toDateString() : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +117,6 @@ class Order extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnRender() {
|
OnRender() {
|
||||||
// todo: add order tracking, the data is already there
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OnUnMount() {
|
OnUnMount() {
|
||||||
|
|||||||
72
client/public/components/stock-audit.mjs
Normal file
72
client/public/components/stock-audit.mjs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { RegisterComponent, Component, SideLoad } from './components.mjs';
|
||||||
|
|
||||||
|
class StockEditor extends Component {
|
||||||
|
static __IDENTIFY() { return 'stock-editor'; }
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(StockEditor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Render() {
|
||||||
|
return {
|
||||||
|
template: /* html */`
|
||||||
|
<div class="stock-editor">
|
||||||
|
<div class="stock-header">Stock Editor</div>
|
||||||
|
<div class="collapsible-menu">
|
||||||
|
<div class="menu-header">
|
||||||
|
<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 class="menu-content">
|
||||||
|
<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-type-input" type="text" placeholder="Item Type (brick/set)">
|
||||||
|
<button class="menu-content-item-button stock-lookup-button">Lookup</button>
|
||||||
|
<div id="remove-preview">
|
||||||
|
<div class="preview-text">
|
||||||
|
<span class="preview-text-title">Preview</span>
|
||||||
|
<super-compact-listing-component class="stock-remove-preview"></super-compact-listing-component>
|
||||||
|
</div>
|
||||||
|
<button class="menu-content-item-button remove-stock-button">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-menu">
|
||||||
|
<div class="menu-header">
|
||||||
|
<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 class="menu-content">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
style: SideLoad('/components/css/stock-audit.css'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
const removeStockLookup = this.root.querySelector('.stock-lookup-button');
|
||||||
|
removeStockLookup.addEventListener('click', () => {
|
||||||
|
const preview = this.root.querySelector('.stock-remove-preview');
|
||||||
|
const id = this.root.querySelector('.menu-content-item-id-input').value;
|
||||||
|
const type = this.root.querySelector('.menu-content-item-type-input').value;
|
||||||
|
|
||||||
|
preview.setAttribute('id', id);
|
||||||
|
preview.setAttribute('type', type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterComponent(StockEditor);
|
||||||
@@ -8,7 +8,7 @@ class SuperCompactProductListing extends Component {
|
|||||||
super(SuperCompactProductListing);
|
super(SuperCompactProductListing);
|
||||||
}
|
}
|
||||||
|
|
||||||
async OnMount() {
|
async Update() {
|
||||||
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,7 +28,12 @@ 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,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>LegoLog Basket</title>
|
<title>LegoLog Your Orders</title>
|
||||||
<meta name="viewport">
|
<meta name="viewport">
|
||||||
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
|
||||||
<link rel="stylesheet" type="text/css" href="/global.css">
|
<link rel="stylesheet" type="text/css" href="/global.css">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>LegoLog Basket</title>
|
<title>LegoLog Order</title>
|
||||||
<meta name="viewport">
|
<meta name="viewport">
|
||||||
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
|
||||||
<link rel="stylesheet" type="text/css" href="/global.css">
|
<link rel="stylesheet" type="text/css" href="/global.css">
|
||||||
|
|||||||
37
client/public/staff/revieworders/index.html
Normal file
37
client/public/staff/revieworders/index.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>LegoLog Logistical Fufillment Service</title>
|
||||||
|
<meta name="viewport">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/global.css">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Londrina+Solid&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Josefin+Sans&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Auth0 - a library for authentication -->
|
||||||
|
<script src="https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js"></script>
|
||||||
|
|
||||||
|
<!-- Components used on this page - they must be included to work -->
|
||||||
|
<script type="module" src="/components/components.mjs"></script>
|
||||||
|
<script type="module" src="/components/navbar.mjs"></script>
|
||||||
|
<script type="module" src="/components/search.mjs"></script>
|
||||||
|
<script type="module" src="/components/order-list.mjs"></script>
|
||||||
|
<script type="module" src="/components/basket-popout.mjs"></script>
|
||||||
|
<script type="module" src="/components/immutable-list.mjs"></script>
|
||||||
|
<script type="module" src="/components/accessability-popout.mjs"></script>
|
||||||
|
<script type="module" src="/components/notificationbar.mjs"></script>
|
||||||
|
<script type="module" src="/components/tag.mjs"></script>
|
||||||
|
<script type="module" src="/components/super-compact-listing.mjs"></script>
|
||||||
|
|
||||||
|
<script type="module" src="/index.mjs"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<notificationbar-component></notificationbar-component>
|
||||||
|
<navbar-component></navbar-component>
|
||||||
|
<limited-margin>
|
||||||
|
<order-list-component staff="true"></order-list-component>
|
||||||
|
</limited-margin>
|
||||||
|
</body>
|
||||||
37
client/public/staff/stock/index.html
Normal file
37
client/public/staff/stock/index.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>LegoLog Stock Editor</title>
|
||||||
|
<meta name="viewport">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/global.css">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Londrina+Solid&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Josefin+Sans&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Auth0 - a library for authentication -->
|
||||||
|
<script src="https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js"></script>
|
||||||
|
|
||||||
|
<!-- Components used on this page - they must be included to work -->
|
||||||
|
<script type="module" src="/components/components.mjs"></script>
|
||||||
|
<script type="module" src="/components/navbar.mjs"></script>
|
||||||
|
<script type="module" src="/components/search.mjs"></script>
|
||||||
|
<script type="module" src="/components/stock-audit.mjs"></script>
|
||||||
|
<script type="module" src="/components/basket-popout.mjs"></script>
|
||||||
|
<script type="module" src="/components/immutable-list.mjs"></script>
|
||||||
|
<script type="module" src="/components/accessability-popout.mjs"></script>
|
||||||
|
<script type="module" src="/components/notificationbar.mjs"></script>
|
||||||
|
<script type="module" src="/components/tag.mjs"></script>
|
||||||
|
<script type="module" src="/components/super-compact-listing.mjs"></script>
|
||||||
|
|
||||||
|
<script type="module" src="/index.mjs"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<notificationbar-component></notificationbar-component>
|
||||||
|
<navbar-component></navbar-component>
|
||||||
|
<limited-margin>
|
||||||
|
<stock-editor-component></stock-editor-component>
|
||||||
|
</limited-margin>
|
||||||
|
</body>
|
||||||
33
docs/API.md
33
docs/API.md
@@ -10,27 +10,30 @@ automatically every request
|
|||||||
|
|
||||||
| Type | Route | Queries | Auth? | Notes |
|
| Type | Route | Queries | Auth? | Notes |
|
||||||
| --- | --- | --- | - | --- |
|
| --- | --- | --- | - | --- |
|
||||||
| GET | /api/special/ | | ❌ | |
|
| GET | /api/special/ | | ❌ | |
|
||||||
| GET | /api/search/ | query (q), page | ❌ | Query endpoint |
|
| GET | /api/search/ | query (q), page | ❌ | Query endpoint |
|
||||||
| GET | /api/sets/featured | page | ❌ | Query endpoint |
|
| GET | /api/sets/featured | page | ❌ | Query endpoint |
|
||||||
| GET | /api/brick/:id | | ❌ | |
|
| GET | /api/brick/:id | | ❌ | |
|
||||||
| POST | /api/bulk/brick | array | ❌ | POST due to bulk nature |
|
| POST | /api/bulk/brick | array | ❌ | POST due to bulk nature |
|
||||||
| GET | /api/set/:id | | ❌ | |
|
| GET | /api/set/:id | | ❌ | |
|
||||||
| GET | /api/cdn/:id | | ❌ | |
|
| GET | /api/cdn/:id | | ❌ | |
|
||||||
| GET | /api/basket/price/ | | ❌ | |
|
| GET | /api/basket/price/ | | ❌ | |
|
||||||
| GET | /api/discount/ | offer code | ❌ | |
|
| GET | /api/discount/ | offer code | ❌ | |
|
||||||
| POST | /api/order/ | | ❌ | IF user is authenticated, auth/bearer will be sent and done manually without middleware |
|
| POST | /api/order/ | | ❌ | IF user is authenticated, auth/bearer will be sent and done manually without middleware |
|
||||||
| GET | /api/auth/order/:id | | ❌ | Security By Obscurity |
|
| GET | /api/auth/order/:id | | ❌ | Security By Obscurity |
|
||||||
| GET | /api/auth/login/ | | ✔️ | |
|
| GET | /api/auth/login/ | | ✔️ | |
|
||||||
| GET | /api/auth/orders/ | | ✔️ | |
|
| GET | /api/auth/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/stock/:type/:id | | ✔️ | Update stock on item |
|
||||||
|
| POST | /api/auth/staff/stock/ | | ✔️ | Add item to inventory |
|
||||||
|
| DEL | /api/auth/staff/stock/:type/:id | | ✔️ | 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
|
||||||
|
|
||||||
## Query structure
|
## Query structure
|
||||||
|
|
||||||
## Query parameters
|
|
||||||
|
|
||||||
For all endpoints that query, the following parameters are supported:
|
For all endpoints that query, the following parameters are supported:
|
||||||
|
|
||||||
q: string to search for (fuzzy)
|
q: string to search for (fuzzy)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function LevenshteinDistance(s, t) {
|
|||||||
// TODO: get this working properly
|
// TODO: get this working properly
|
||||||
|
|
||||||
function SanatiseQuery(query) {
|
function SanatiseQuery(query) {
|
||||||
|
if (!query) return '';
|
||||||
query = query.trim();
|
query = query.trim();
|
||||||
query = query.replace(/[^a-zA-Z0-9,&/\s]/g, '');
|
query = query.replace(/[^a-zA-Z0-9,&/\s]/g, '');
|
||||||
query = escape(query);
|
query = escape(query);
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ async function GetOrdersByUser(userId) {
|
|||||||
discount, date_placed, shipped
|
discount, date_placed, shipped
|
||||||
FROM order_log
|
FROM order_log
|
||||||
WHERE order_log.user_id = $1
|
WHERE order_log.user_id = $1
|
||||||
|
ORDER BY date_placed DESC
|
||||||
`, [userId]).catch(() => {
|
`, [userId]).catch(() => {
|
||||||
return {
|
return {
|
||||||
error: 'Database error',
|
error: 'Database error',
|
||||||
@@ -133,14 +134,76 @@ async function GetOrdersByUser(userId) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function GetUnFinishedOrders() {
|
||||||
|
await Database.Query('BEGIN TRANSACTION;');
|
||||||
|
const dbres = await Database.Query(`
|
||||||
|
SELECT order_log.id, order_log.user_id, subtotal_paid,
|
||||||
|
date_placed, shipped, recieved
|
||||||
|
FROM order_log
|
||||||
|
WHERE order_log.recieved = FALSE
|
||||||
|
ORDER BY date_placed DESC
|
||||||
|
`).catch(() => {
|
||||||
|
return {
|
||||||
|
error: 'Database error',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (dbres.error) {
|
||||||
|
Database.Query('ROLLBACK TRANSACTION;');
|
||||||
|
Logger.Error(dbres.error);
|
||||||
|
return {
|
||||||
|
error: 'Database error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Database.Query('COMMIT TRANSACTION;');
|
||||||
|
|
||||||
|
const result = dbres.rows;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// U
|
// U
|
||||||
|
|
||||||
async function OrderShipped(orderid) {
|
async function OrderShipped(orderid) {
|
||||||
|
await Database.Query('BEGIN TRANSACTION;');
|
||||||
|
const dbres = await Database.Query(`
|
||||||
|
UPDATE order_log
|
||||||
|
SET shipped = TRUE, date_shipped = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
`, [orderid]).catch(() => {
|
||||||
|
return {
|
||||||
|
error: 'Database error',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (dbres.error) {
|
||||||
|
Database.Query('ROLLBACK TRANSACTION;');
|
||||||
|
Logger.Error(dbres.error);
|
||||||
|
return {
|
||||||
|
error: 'Database error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Database.Query('COMMIT TRANSACTION;');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function OrderRecieved(orderid) {
|
async function OrderRecieved(orderid) {
|
||||||
|
await Database.Query('BEGIN TRANSACTION;');
|
||||||
|
const dbres = await Database.Query(`
|
||||||
|
UPDATE order_log
|
||||||
|
SET recieved = TRUE, date_recieved = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
`, [orderid]).catch(() => {
|
||||||
|
return {
|
||||||
|
error: 'Database error',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (dbres.error) {
|
||||||
|
Database.Query('ROLLBACK TRANSACTION;');
|
||||||
|
Logger.Error(dbres.error);
|
||||||
|
return {
|
||||||
|
error: 'Database error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Database.Query('COMMIT TRANSACTION;');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// D
|
// D
|
||||||
@@ -149,6 +212,7 @@ module.exports = {
|
|||||||
NewOrder,
|
NewOrder,
|
||||||
GetOrderById,
|
GetOrderById,
|
||||||
GetOrdersByUser,
|
GetOrdersByUser,
|
||||||
|
GetUnFinishedOrders,
|
||||||
OrderShipped,
|
OrderShipped,
|
||||||
OrderRecieved,
|
OrderRecieved,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ function Init() {
|
|||||||
Server.App.post('/api/auth/order/', Auth0.JWTMiddleware, Order.ProcessNew);
|
Server.App.post('/api/auth/order/', Auth0.JWTMiddleware, Order.ProcessNew);
|
||||||
Server.App.get('/api/auth/orders/', Auth0.JWTMiddleware, Order.GetOrders);
|
Server.App.get('/api/auth/orders/', Auth0.JWTMiddleware, Order.GetOrders);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
Logger.Module('API', 'API Routes Initialized');
|
Logger.Module('API', 'API Routes Initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const AUTH0CONFIG = {
|
|||||||
audience: 'localhost:8080/api',
|
audience: 'localhost:8080/api',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Auth0 was rate limiting me a LOT, so I'm going to use a cache to
|
||||||
|
// prevent that from happening again
|
||||||
|
const Auth0UserCache = [];
|
||||||
|
|
||||||
const JWTChecker = OAuth2JWTBearer.auth({
|
const JWTChecker = OAuth2JWTBearer.auth({
|
||||||
audience: AUTH0CONFIG.audience,
|
audience: AUTH0CONFIG.audience,
|
||||||
issuerBaseURL: `https://${AUTH0CONFIG.domain}`,
|
issuerBaseURL: `https://${AUTH0CONFIG.domain}`,
|
||||||
@@ -33,6 +37,25 @@ function JWTMiddleware(req, res, next) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function AdminOnlyEndpoint(req, res, next) {
|
||||||
|
const user = await Auth0GetUser(req);
|
||||||
|
if (!user) {
|
||||||
|
return res.send({
|
||||||
|
error: 'No user found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const localUser = await Controller.GetUserByID(user.sub.split('|')[1]);
|
||||||
|
|
||||||
|
if (!localUser.admin) {
|
||||||
|
return res.send({
|
||||||
|
error: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
async function Auth0GetUser(req) {
|
async function Auth0GetUser(req) {
|
||||||
if (!req.auth) {
|
if (!req.auth) {
|
||||||
return null;
|
return null;
|
||||||
@@ -40,14 +63,21 @@ async function Auth0GetUser(req) {
|
|||||||
|
|
||||||
if (!req.auth || !req.auth.token) return null;
|
if (!req.auth || !req.auth.token) return null;
|
||||||
|
|
||||||
|
const token = req.auth.token;
|
||||||
|
if (Auth0UserCache[token]) {
|
||||||
|
return Auth0UserCache[token];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await Axios.get(`https://${AUTH0CONFIG.domain}/userinfo`, {
|
const response = await Axios.get(`https://${AUTH0CONFIG.domain}/userinfo`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${req.auth.token}`,
|
authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Auth0UserCache[token] = response.data;
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.Error('error getting auth profile', req.auth, err);
|
Logger.Error('error getting auth profile', req.auth, err);
|
||||||
@@ -58,6 +88,11 @@ async function Auth0GetUser(req) {
|
|||||||
async function Login(req, res) {
|
async function Login(req, res) {
|
||||||
// tell the database the user is new if they don't already exist
|
// tell the database the user is new if they don't already exist
|
||||||
const user = await Auth0GetUser(req);
|
const user = await Auth0GetUser(req);
|
||||||
|
if (!user) {
|
||||||
|
return res.send({
|
||||||
|
error: 'No user found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const id = user.sub.split('|')[1];
|
const id = user.sub.split('|')[1];
|
||||||
|
|
||||||
@@ -91,6 +126,7 @@ async function Login(req, res) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
JWTMiddleware,
|
JWTMiddleware,
|
||||||
|
AdminOnlyEndpoint,
|
||||||
Auth0GetUser,
|
Auth0GetUser,
|
||||||
Login,
|
Login,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ async function ProcessNew(req, res) {
|
|||||||
discount: 0,
|
discount: 0,
|
||||||
};
|
};
|
||||||
if (discountCode !== null) {
|
if (discountCode !== null) {
|
||||||
const sanatisedCode = ControllerMaster.SanatiseQuery(req.query.code);
|
const sanatisedCode = ControllerMaster.SanatiseQuery(discountCode);
|
||||||
|
|
||||||
discount = await MiscController.GetDiscount(sanatisedCode);
|
discount = await MiscController.GetDiscount(sanatisedCode);
|
||||||
|
|
||||||
@@ -188,8 +188,76 @@ async function GetOrders(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function GetUnFinishedOrders(req, res) {
|
||||||
|
const orders = await OrderController.GetUnFinishedOrders();
|
||||||
|
|
||||||
|
if (orders.error) {
|
||||||
|
return res.send({
|
||||||
|
error: orders.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
data: orders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function UpdateOrderStatus(req, res) {
|
||||||
|
const orderId = req.params.id;
|
||||||
|
const status = req.body.status;
|
||||||
|
|
||||||
|
if (!orderId) {
|
||||||
|
return res.send({
|
||||||
|
error: 'No order id in request',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return res.send({
|
||||||
|
error: 'No status in request',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shipped = status.shipped;
|
||||||
|
if (!shipped) {
|
||||||
|
const completed = status.completed;
|
||||||
|
if (!completed) {
|
||||||
|
return res.send({
|
||||||
|
error: 'No status in request',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const orderRecieved = await OrderController.OrderRecieved(orderId);
|
||||||
|
if (orderRecieved.error) {
|
||||||
|
return res.send({
|
||||||
|
error: orderRecieved.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderShipped = await OrderController.OrderShipped(orderId);
|
||||||
|
if (orderShipped.error) {
|
||||||
|
return res.send({
|
||||||
|
error: orderShipped.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ProcessNew,
|
ProcessNew,
|
||||||
GetOrder,
|
GetOrder,
|
||||||
GetOrders,
|
GetOrders,
|
||||||
|
GetUnFinishedOrders,
|
||||||
|
UpdateOrderStatus,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user