order pages all done and stock pages coming

Former-commit-id: da90fc25deecf2843a6269d877387ce07fdb2728
This commit is contained in:
Ben
2022-04-29 15:04:30 +01:00
parent b7cf3d44aa
commit d5bf2f03cd
17 changed files with 525 additions and 44 deletions

View File

@@ -49,7 +49,6 @@ export async function InitAuth0() {
if (isAuthenticated) {
const user = await auth0.getUser();
localStorage.setItem('user', user.given_name || user.nickname);
NotifyNavbar('login', user);
localStorage.setItem('loggedIn', true);
ready = true;
@@ -67,6 +66,7 @@ export async function InitAuth0() {
const res = await fetch('/api/auth/login', fetchOptions).then(res => res.json());
localStorage.setItem('admin', res.user.admin);
NotifyNavbar('login', user);
}
}

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

View File

@@ -54,8 +54,8 @@ class NavBar extends Component {
<a class="nav-link" href="#">${localStorage.user}▾</a>
<ul class="sub-nav" >
<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="">Review Open Orders</a></li>
<li><a class="sub-nav-link" href="/staff/stock">Add or Remove Stock</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>
</ul>
`;

View File

@@ -9,30 +9,48 @@ class OrderList extends Component {
}
async OnMount() {
const doStaffList = this.state.staff !== undefined;
const options = {
method: 'GET',
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,
orders: res.data,
}, false);
console.log(this.state);
this.setState({
...this.getState,
orders: res.data,
title: 'Your Orders',
none: 'You have no orders',
}, false);
}
}
Render() {
return {
template: /* html */`
<div class="order-header">
<span class="order-header-title">Your Orders</span>
<span class="order-header-title">{this.state.title}</span>
</div>
<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 */`
<div class="orders-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">
<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>
${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></a>
</div>
@@ -54,6 +78,69 @@ class OrderList extends Component {
}
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 :)',
});
});
});
}
}

View File

@@ -8,7 +8,7 @@ class Order extends Component {
}
async OnMount() {
// get order id from search param
// get order id from search param
const query = new URLSearchParams(window.location.search);
const id = query.get('id');
@@ -20,7 +20,6 @@ class Order extends Component {
...res,
}, false);
console.log(this.state);
localStorage.setItem('viewing-order', JSON.stringify({ items: res.items }));
}
@@ -86,22 +85,22 @@ class Order extends Component {
</div>
<div class="order-track-step">
<span class="order-track-status">
<div class="order-track-step-icon"></div>
<div class="order-track-step-line"></div>
<div class="order-track-step-icon ${this.state.shipped ? 'completed' : ''}"></div>
<div class="order-track-step-line ${this.state.shipped ? 'completed' : ''}"></div>
</span>
<div class="order-track-text">
<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 class="order-track-step">
<span class="order-track-status">
<div class="order-track-step-icon"></div>
<div class="order-track-step-line"></div>
<div class="order-track-step-icon ${this.state.recieved ? 'completed' : ''}"></div>
<div class="order-track-step-line ${this.state.recieved ? 'completed' : ''}"></div>
</span>
<div class="order-track-text">
<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>
@@ -118,7 +117,6 @@ class Order extends Component {
}
OnRender() {
// todo: add order tracking, the data is already there
}
OnUnMount() {

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

View File

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

View File

@@ -1,6 +1,6 @@
<html>
<head>
<title>LegoLog Basket</title>
<title>LegoLog Your Orders</title>
<meta name="viewport">
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
<link rel="stylesheet" type="text/css" href="/global.css">

View File

@@ -1,6 +1,6 @@
<html>
<head>
<title>LegoLog Basket</title>
<title>LegoLog Order</title>
<meta name="viewport">
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
<link rel="stylesheet" type="text/css" href="/global.css">

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

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

View File

@@ -10,27 +10,30 @@ automatically every request
| Type | Route | Queries | Auth? | Notes |
| --- | --- | --- | - | --- |
| GET | /api/special/ | | ❌ | |
| GET | /api/search/ | query (q), page | ❌ | Query endpoint |
| GET | /api/sets/featured | page | ❌ | Query endpoint |
| GET | /api/brick/:id | | ❌ | |
| POST | /api/bulk/brick | array | ❌ | POST due to bulk nature |
| GET | /api/set/:id | | ❌ | |
| GET | /api/cdn/:id | | ❌ | |
| GET | /api/basket/price/ | | ❌ | |
| GET | /api/discount/ | offer code | ❌ | |
| 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/login/ | | ✔️ | |
| GET | /api/auth/orders/ | | ✔️ | |
| GET | /api/special/ | | ❌ | |
| GET | /api/search/ | query (q), page | ❌ | Query endpoint |
| GET | /api/sets/featured | page | ❌ | Query endpoint |
| GET | /api/brick/:id | | ❌ | |
| POST | /api/bulk/brick | array | ❌ | POST due to bulk nature |
| GET | /api/set/:id | | ❌ | |
| GET | /api/cdn/:id | | ❌ | |
| GET | /api/basket/price/ | | ❌ | |
| GET | /api/discount/ | offer code | ❌ | |
| 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/login/ | | ✔️ | |
| 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
a subset for product listing pages
## Query structure
## Query parameters
For all endpoints that query, the following parameters are supported:
q: string to search for (fuzzy)

View File

@@ -54,6 +54,7 @@ function LevenshteinDistance(s, t) {
// TODO: get this working properly
function SanatiseQuery(query) {
if (!query) return '';
query = query.trim();
query = query.replace(/[^a-zA-Z0-9,&/\s]/g, '');
query = escape(query);

View File

@@ -115,6 +115,7 @@ async function GetOrdersByUser(userId) {
discount, date_placed, shipped
FROM order_log
WHERE order_log.user_id = $1
ORDER BY date_placed DESC
`, [userId]).catch(() => {
return {
error: 'Database error',
@@ -133,14 +134,76 @@ async function GetOrdersByUser(userId) {
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
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) {
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
@@ -149,6 +212,7 @@ module.exports = {
NewOrder,
GetOrderById,
GetOrdersByUser,
GetUnFinishedOrders,
OrderShipped,
OrderRecieved,
};

View File

@@ -31,6 +31,9 @@ function Init() {
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/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');
}

View File

@@ -13,6 +13,10 @@ const AUTH0CONFIG = {
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({
audience: AUTH0CONFIG.audience,
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) {
if (!req.auth) {
return null;
@@ -40,14 +63,21 @@ async function Auth0GetUser(req) {
if (!req.auth || !req.auth.token) return null;
const token = req.auth.token;
if (Auth0UserCache[token]) {
return Auth0UserCache[token];
}
try {
const response = await Axios.get(`https://${AUTH0CONFIG.domain}/userinfo`, {
method: 'GET',
headers: {
authorization: `Bearer ${req.auth.token}`,
authorization: `Bearer ${token}`,
},
});
Auth0UserCache[token] = response.data;
return response.data;
} catch (err) {
Logger.Error('error getting auth profile', req.auth, err);
@@ -58,6 +88,11 @@ async function Auth0GetUser(req) {
async function Login(req, res) {
// tell the database the user is new if they don't already exist
const user = await Auth0GetUser(req);
if (!user) {
return res.send({
error: 'No user found',
});
}
const id = user.sub.split('|')[1];
@@ -91,6 +126,7 @@ async function Login(req, res) {
module.exports = {
JWTMiddleware,
AdminOnlyEndpoint,
Auth0GetUser,
Login,
};

View File

@@ -115,7 +115,7 @@ async function ProcessNew(req, res) {
discount: 0,
};
if (discountCode !== null) {
const sanatisedCode = ControllerMaster.SanatiseQuery(req.query.code);
const sanatisedCode = ControllerMaster.SanatiseQuery(discountCode);
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 = {
ProcessNew,
GetOrder,
GetOrders,
GetUnFinishedOrders,
UpdateOrderStatus,
};