cash orders
This commit is contained in:
parent
43b98d6d0d
commit
8a044653f7
|
@ -0,0 +1,17 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use bonsaidb::core::schema::Collection;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Collection)]
|
||||||
|
#[collection(name = "orders")]
|
||||||
|
pub struct Order {
|
||||||
|
pub order_type: OrderType,
|
||||||
|
pub total_usd: f64,
|
||||||
|
pub products: HashMap<u64, u64>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub enum OrderType {
|
||||||
|
Cash,
|
||||||
|
Stripe
|
||||||
|
}
|
|
@ -7,14 +7,18 @@ use bonsaidb::local::AsyncDatabase;
|
||||||
use bonsaidb::local::config::{Builder, StorageConfiguration};
|
use bonsaidb::local::config::{Builder, StorageConfiguration};
|
||||||
|
|
||||||
use crate::db_products::Product;
|
use crate::db_products::Product;
|
||||||
|
use crate::db_orders::Order;
|
||||||
|
use crate::route_order::{cash_order, get_orders};
|
||||||
use crate::route_products::{create_product, delete_product, get_product, get_products, update_product};
|
use crate::route_products::{create_product, delete_product, get_product, get_products, update_product};
|
||||||
|
|
||||||
pub mod db_products;
|
pub mod db_products;
|
||||||
pub mod route_products;
|
pub mod route_products;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod route_order;
|
||||||
|
pub mod db_orders;
|
||||||
|
|
||||||
#[derive(Debug, Schema)]
|
#[derive(Debug, Schema)]
|
||||||
#[schema(name = "lmsposschema", collections = [Product])]
|
#[schema(name = "lmsposschema", collections = [Product, Order])]
|
||||||
pub struct DbSchema;
|
pub struct DbSchema;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
|
@ -41,6 +45,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
.service(get_product)
|
.service(get_product)
|
||||||
.service(delete_product)
|
.service(delete_product)
|
||||||
.service(status)
|
.service(status)
|
||||||
|
.service(get_orders)
|
||||||
|
.service(cash_order)
|
||||||
})
|
})
|
||||||
.bind(("127.0.0.1", 8080))?
|
.bind(("127.0.0.1", 8080))?
|
||||||
.run()
|
.run()
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use actix_web::{get, HttpResponse, post};
|
||||||
|
use actix_web::web::{Data, Json};
|
||||||
|
use bonsaidb::core::schema::SerializedCollection;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::db_orders::{Order, OrderType};
|
||||||
|
use crate::db_products::Product;
|
||||||
|
use crate::error::APIError;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CashOrderRequest {
|
||||||
|
pub products: HashMap<u64, u64>,
|
||||||
|
pub adjust_stock: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct CashOrderResponse {
|
||||||
|
pub order_id: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/cash_order")]
|
||||||
|
pub async fn cash_order(req: Json<CashOrderRequest>, db: Data<AppState>) -> HttpResponse {
|
||||||
|
// load product data
|
||||||
|
let mut products = match Product::get_multiple_async(req.products.keys(), &db.db).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
return HttpResponse::InternalServerError().json(APIError {
|
||||||
|
code: "DB_ERROR".to_string(),
|
||||||
|
message: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut price_total: f64 = 0.0;
|
||||||
|
|
||||||
|
for product in &products {
|
||||||
|
price_total += req.products[&product.header.id] as f64 * product.contents.price_usd;
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc = Order {
|
||||||
|
order_type: OrderType::Cash,
|
||||||
|
total_usd: price_total,
|
||||||
|
products: req.products.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if req.adjust_stock {
|
||||||
|
for product in &mut products {
|
||||||
|
if product.contents.stock < req.products[&product.header.id] {
|
||||||
|
return HttpResponse::BadRequest().json(APIError {
|
||||||
|
code: "NOT_ENOUGH_STOCK".to_string(),
|
||||||
|
message: "Cannot sell more items than are in stock backend".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let order = match doc.push_into_async(&db.db).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
return HttpResponse::InternalServerError().json(APIError {
|
||||||
|
code: "DB_ERROR".to_string(),
|
||||||
|
message: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if req.adjust_stock {
|
||||||
|
for product in &mut products {
|
||||||
|
product.contents.stock -= req.products[&product.header.id];
|
||||||
|
match product.update_async(&db.db).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
return HttpResponse::InternalServerError().json(APIError {
|
||||||
|
code: "DB_ERROR".to_string(),
|
||||||
|
message: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::Ok().json(CashOrderResponse {
|
||||||
|
order_id: order.header.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct OrdersResponse {
|
||||||
|
pub orders: Vec<OrderResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct OrderResponse {
|
||||||
|
pub id: u64,
|
||||||
|
pub order_type: OrderType,
|
||||||
|
pub total_usd: f64,
|
||||||
|
pub products: HashMap<u64, u64>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/orders")]
|
||||||
|
pub async fn get_orders(db: Data<AppState>) -> HttpResponse {
|
||||||
|
let orders = match Order::list_async(0.., &db.db).await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
return HttpResponse::InternalServerError().json(APIError {
|
||||||
|
code: "DB_ERROR".to_string(),
|
||||||
|
message: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp: Vec<OrderResponse> = orders.iter().map(|u| OrderResponse {
|
||||||
|
id: u.header.id,
|
||||||
|
order_type: u.contents.order_type.clone(),
|
||||||
|
total_usd: u.contents.total_usd,
|
||||||
|
products: u.contents.products.clone(),
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
HttpResponse::Ok().json(OrdersResponse { orders: resp })
|
||||||
|
}
|
|
@ -16,9 +16,11 @@
|
||||||
"@material/typography": "^14.0.0",
|
"@material/typography": "^14.0.0",
|
||||||
"@smui/button": "^7.0.0-beta.8",
|
"@smui/button": "^7.0.0-beta.8",
|
||||||
"@smui/card": "^7.0.0-beta.8",
|
"@smui/card": "^7.0.0-beta.8",
|
||||||
|
"@smui/checkbox": "^7.0.0-beta.8",
|
||||||
"@smui/circular-progress": "^7.0.0-beta.8",
|
"@smui/circular-progress": "^7.0.0-beta.8",
|
||||||
"@smui/data-table": "^7.0.0-beta.8",
|
"@smui/data-table": "^7.0.0-beta.8",
|
||||||
"@smui/dialog": "^7.0.0-beta.8",
|
"@smui/dialog": "^7.0.0-beta.8",
|
||||||
|
"@smui/form-field": "^7.0.0-beta.8",
|
||||||
"@smui/icon-button": "^7.0.0-beta.8",
|
"@smui/icon-button": "^7.0.0-beta.8",
|
||||||
"@smui/linear-progress": "^7.0.0-beta.8",
|
"@smui/linear-progress": "^7.0.0-beta.8",
|
||||||
"@smui/paper": "^7.0.0-beta.8",
|
"@smui/paper": "^7.0.0-beta.8",
|
||||||
|
|
|
@ -23,6 +23,12 @@
|
||||||
<Label>Manage Env</Label>
|
<Label>Manage Env</Label>
|
||||||
</Button>
|
</Button>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|
||||||
|
<Wrapper>
|
||||||
|
<Button on:click={() => {window.location.href = "/manage/orders"}} disabled={selected === "manage/orders"} class={selected === "manage/orders" ? "underline" : ""}>
|
||||||
|
<Label>Manage Orders</Label>
|
||||||
|
</Button>
|
||||||
|
</Wrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -6,15 +6,22 @@
|
||||||
import Button, {Label as ButtonLabel, Group as ButtonGroup} from "@smui/button";
|
import Button, {Label as ButtonLabel, Group as ButtonGroup} from "@smui/button";
|
||||||
import DataTable, { Head, Body, Row, Cell } from '@smui/data-table';
|
import DataTable, { Head, Body, Row, Cell } from '@smui/data-table';
|
||||||
import LinearProgress from "@smui/linear-progress";
|
import LinearProgress from "@smui/linear-progress";
|
||||||
|
import Dialog, { Title as DialogTitle, Content as DialogContent, Actions as DialogActions } from "@smui/dialog";
|
||||||
|
import Textfield from "@smui/textfield";
|
||||||
|
import HelperText from "@smui/textfield/helper-text";
|
||||||
|
import Checkbox from "@smui/checkbox";
|
||||||
|
import FormField from "@smui/form-field";
|
||||||
|
|
||||||
let products = [];
|
let products = [];
|
||||||
let productsCounter = {};
|
let productsCounter = {};
|
||||||
|
|
||||||
let failureSnackbar: Snackbar;
|
let snackbar: Snackbar;
|
||||||
let failureSnackbarText = "";
|
let snackbarText = "";
|
||||||
|
|
||||||
let loaded;
|
let loaded;
|
||||||
|
|
||||||
|
let loadedCashIn = false;
|
||||||
|
|
||||||
const formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'});
|
const formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'});
|
||||||
|
|
||||||
// load products from the backend
|
// load products from the backend
|
||||||
|
@ -32,14 +39,14 @@
|
||||||
loaded = true;
|
loaded = true;
|
||||||
} else {
|
} else {
|
||||||
console.error(await resp.text());
|
console.error(await resp.text());
|
||||||
failureSnackbarText = "There was an error loading the products list. Check the browser console for more information.";
|
snackbarText = "There was an error loading the products list. Check the browser console for more information.";
|
||||||
failureSnackbar.open();
|
snackbar.open();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
failureSnackbarText = "There was an error loading the products list. Check the browser console for more information.";
|
snackbarText = "There was an error loading the products list. Check the browser console for more information.";
|
||||||
failureSnackbar.open();
|
snackbar.open();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,13 +64,122 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadProducts();
|
loadProducts();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
let cashDialogOpen = false;
|
||||||
|
|
||||||
|
let cashInDirty = false;
|
||||||
|
let cashInInvalid = false;
|
||||||
|
let cashInFocused = false;
|
||||||
|
let cashIn = 0.0;
|
||||||
|
|
||||||
|
function change(total, cashin) {
|
||||||
|
let value_b = cashin - total;
|
||||||
|
|
||||||
|
let value = Math.trunc(Number(Number(value_b).toFixed(2)) * 100);
|
||||||
|
|
||||||
|
// 5s, 1s, quarters, dimes, nickels, pennies
|
||||||
|
let fives = 0;
|
||||||
|
let ones = 0;
|
||||||
|
let quarters = 0;
|
||||||
|
let dimes = 0;
|
||||||
|
let nickels = 0;
|
||||||
|
let pennies = 0;
|
||||||
|
|
||||||
|
if (value % 500 >= 0) {
|
||||||
|
const _temp = value - (value % 500);
|
||||||
|
fives = _temp / 500;
|
||||||
|
value -= _temp;
|
||||||
|
}
|
||||||
|
if (value % 100 >= 0) {
|
||||||
|
const _temp = value - (value % 100);
|
||||||
|
ones = _temp / 100;
|
||||||
|
value -= _temp;
|
||||||
|
}
|
||||||
|
if (value % 25 >= 0) {
|
||||||
|
const _temp = value - (value % 25);
|
||||||
|
quarters = _temp / 25;
|
||||||
|
value -= _temp;
|
||||||
|
}
|
||||||
|
if (value % 10 >= 0) {
|
||||||
|
const _temp = value - (value % 10);
|
||||||
|
dimes = _temp / 10;
|
||||||
|
value -= _temp;
|
||||||
|
}
|
||||||
|
if (value % 5 >= 0) {
|
||||||
|
const _temp = value - (value % 5);
|
||||||
|
nickels = _temp / 5;
|
||||||
|
value -= _temp;
|
||||||
|
}
|
||||||
|
pennies = value;
|
||||||
|
|
||||||
|
return [fives, ones, quarters, dimes, nickels, pennies];
|
||||||
|
}
|
||||||
|
|
||||||
|
let changeGiven = [];
|
||||||
|
|
||||||
|
function reChange() {
|
||||||
|
changeGiven = change(total, cashIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: cashIn, reChange();
|
||||||
|
$: total, reChange();
|
||||||
|
|
||||||
|
let adjustStockOnOrderFinish = true;
|
||||||
|
|
||||||
|
let hasProducts = false;
|
||||||
|
|
||||||
|
function updateHasProducts() {
|
||||||
|
for (let i = 0; i < products.length; i++) {
|
||||||
|
if (productsCounter[products[i].id] !== 0) {
|
||||||
|
hasProducts = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasProducts = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: productsCounter, updateHasProducts();
|
||||||
|
|
||||||
|
async function cashOrder() {
|
||||||
|
loadedCashIn = false;
|
||||||
|
try {
|
||||||
|
let resp = await fetch(`${$serverUrl}/cash_order`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: [
|
||||||
|
['Content-Type', 'application/json']
|
||||||
|
],
|
||||||
|
body: JSON.stringify({
|
||||||
|
products: productsCounter,
|
||||||
|
adjust_stock: adjustStockOnOrderFinish
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (resp.ok) {
|
||||||
|
let json = await resp.json();
|
||||||
|
loadedCashIn = true;
|
||||||
|
snackbarText = "Order complete! Order ID: " + json.order_id;
|
||||||
|
snackbar.open();
|
||||||
|
loadProducts();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.error(await resp.text());
|
||||||
|
snackbarText = "There was an error creating the order. Check the browser console for more information.";
|
||||||
|
snackbar.open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
snackbarText = "There was an error creating the order. Check the browser console for more information.";
|
||||||
|
snackbar.open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header selected="pos" />
|
<Header selected="pos" />
|
||||||
|
|
||||||
<Snackbar bind:this={failureSnackbar}>
|
<Snackbar bind:this={snackbar}>
|
||||||
<SnackbarLabel>{failureSnackbarText}</SnackbarLabel>
|
<SnackbarLabel>{snackbarText}</SnackbarLabel>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
|
|
||||||
<DataTable table$aria-label="Product list" style="width: 100%;">
|
<DataTable table$aria-label="Product list" style="width: 100%;">
|
||||||
|
@ -91,10 +207,10 @@
|
||||||
<Cell>{formatter.format(productsCounter[product.id] * product.price_usd)}</Cell>
|
<Cell>{formatter.format(productsCounter[product.id] * product.price_usd)}</Cell>
|
||||||
<Cell>
|
<Cell>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button variant="outlined" on:click={() => {productsCounter[product.id]++}}>
|
<Button disabled={productsCounter[product.id] === product.stock} variant="outlined" on:click={() => {if (productsCounter[product.id] < product.stock) productsCounter[product.id]++}}>
|
||||||
<ButtonLabel>+</ButtonLabel>
|
<ButtonLabel>+</ButtonLabel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outlined" on:click={() => {if (productsCounter[product.id] >= 1) {productsCounter[product.id]--}}}>
|
<Button disabled={productsCounter[product.id] === 0} variant="outlined" on:click={() => {if (productsCounter[product.id] >= 1) {productsCounter[product.id]--}}}>
|
||||||
<ButtonLabel>-</ButtonLabel>
|
<ButtonLabel>-</ButtonLabel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outlined" on:click={() => {productsCounter[product.id] = 0}}>
|
<Button variant="outlined" on:click={() => {productsCounter[product.id] = 0}}>
|
||||||
|
@ -115,10 +231,10 @@
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>
|
<Cell>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button variant="outlined">
|
<Button disabled={!hasProducts} variant="outlined" on:click={() => {cashIn = total.toFixed(2); cashDialogOpen = true;}}>
|
||||||
<ButtonLabel>Cash In</ButtonLabel>
|
<ButtonLabel>Cash In</ButtonLabel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outlined">
|
<Button disabled={!hasProducts} variant="outlined">
|
||||||
<ButtonLabel>Stripe</ButtonLabel>
|
<ButtonLabel>Stripe</ButtonLabel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="unelevated" on:click={() => {for (let i = 0; i < products.length; i++) {productsCounter[products[i].id] = 0}}}>
|
<Button variant="unelevated" on:click={() => {for (let i = 0; i < products.length; i++) {productsCounter[products[i].id] = 0}}}>
|
||||||
|
@ -130,4 +246,51 @@
|
||||||
</Body>
|
</Body>
|
||||||
|
|
||||||
<LinearProgress indeterminate bind:closed={loaded} aria-label="Data is being loaded..." slot="progress" />
|
<LinearProgress indeterminate bind:closed={loaded} aria-label="Data is being loaded..." slot="progress" />
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
|
<Dialog bind:open={cashDialogOpen} aria-labelledby="cash-title" aria-describedby="cash-content">
|
||||||
|
<DialogTitle id="cash-title">Cash In</DialogTitle>
|
||||||
|
<DialogContent id="cash-content">
|
||||||
|
<p>Total Price: {formatter.format(total)}</p>
|
||||||
|
<Textfield type="number" input$step="0.01" input$min="{total.toFixed(2)}" bind:dirty={cashInDirty} bind:invalid={cashInInvalid} updateInvalid bind:value={cashIn} label="Cash in" on:focus={() => {cashInFocused = true}} on:blur={() => {cashInFocused = false}}>
|
||||||
|
<HelperText validationMsg slot="helper">
|
||||||
|
That's not a valid price - must be a number and above {total.toFixed(2)}
|
||||||
|
</HelperText>
|
||||||
|
</Textfield>
|
||||||
|
<p>Change:</p>
|
||||||
|
<DataTable>
|
||||||
|
<Head>
|
||||||
|
<Row>
|
||||||
|
<Cell>5s</Cell>
|
||||||
|
<Cell>1s</Cell>
|
||||||
|
<Cell>Quarters</Cell>
|
||||||
|
<Cell>Dimes</Cell>
|
||||||
|
<Cell>Nickels</Cell>
|
||||||
|
<Cell>Pennies</Cell>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Cell>{changeGiven[0]}</Cell>
|
||||||
|
<Cell>{changeGiven[1]}</Cell>
|
||||||
|
<Cell>{changeGiven[2]}</Cell>
|
||||||
|
<Cell>{changeGiven[3]}</Cell>
|
||||||
|
<Cell>{changeGiven[4]}</Cell>
|
||||||
|
<Cell>{changeGiven[5]}</Cell>
|
||||||
|
</Row>
|
||||||
|
</Head>
|
||||||
|
</DataTable>
|
||||||
|
<p>Total Change: {formatter.format(changeGiven[0]*5 + changeGiven[1] + changeGiven[2] * 0.25 + changeGiven[3] * 0.10 + changeGiven[4] * 0.05 + changeGiven[5] * 0.01)}</p>
|
||||||
|
<p>Total: {formatter.format(changeGiven[0]*5 + changeGiven[1] + changeGiven[2] * 0.25 + changeGiven[3] * 0.10 + changeGiven[4] * 0.05 + changeGiven[5] * 0.01 + total)}</p>
|
||||||
|
<FormField>
|
||||||
|
<Checkbox bind:checked={adjustStockOnOrderFinish} />
|
||||||
|
<span slot="label">Adjust stock?</span>
|
||||||
|
</FormField>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button bind:disabled={cashInInvalid} on:click={cashOrder}>
|
||||||
|
<ButtonLabel>Finish</ButtonLabel>
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<ButtonLabel>Cancel</ButtonLabel>
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
|
@ -0,0 +1,173 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Header from "$lib/components/Header.svelte";
|
||||||
|
import DataTable, { Head, Body, Row, Cell } from '@smui/data-table';
|
||||||
|
import LinearProgress from '@smui/linear-progress';
|
||||||
|
import Snackbar, {Label as SnackbarLabel} from "@smui/snackbar";
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import {serverUrl} from "$lib/stores/ServerStore";
|
||||||
|
import Paper, { Title as PaperTitle, Content as PaperContent } from "@smui/paper";
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
|
let orders = [];
|
||||||
|
|
||||||
|
let failureSnackbar: Snackbar;
|
||||||
|
let failureSnackbarText = "";
|
||||||
|
|
||||||
|
const formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'});
|
||||||
|
|
||||||
|
async function loadOrders() {
|
||||||
|
loaded = false;
|
||||||
|
try {
|
||||||
|
let resp = await fetch(`${$serverUrl}/orders`);
|
||||||
|
if (resp.ok) {
|
||||||
|
let json = await resp.json();
|
||||||
|
console.log(json);
|
||||||
|
orders = json.orders;
|
||||||
|
loaded = true;
|
||||||
|
} else {
|
||||||
|
console.error(await resp.text());
|
||||||
|
failureSnackbarText = "There was an error loading the orders list. Check the browser console for more information.";
|
||||||
|
failureSnackbar.open();
|
||||||
|
loaded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
failureSnackbarText = "There was an error loading the orders list. Check the browser console for more information.";
|
||||||
|
failureSnackbar.open();
|
||||||
|
loaded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let products = [];
|
||||||
|
|
||||||
|
async function loadProducts() {
|
||||||
|
loaded = false;
|
||||||
|
try {
|
||||||
|
let resp = await fetch(`${$serverUrl}/products`);
|
||||||
|
if (resp.ok) {
|
||||||
|
let json = await resp.json();
|
||||||
|
console.log(json);
|
||||||
|
products = json.products;
|
||||||
|
loaded = true;
|
||||||
|
} else {
|
||||||
|
console.error(await resp.text());
|
||||||
|
failureSnackbarText = "There was an error loading the products list. Check the browser console for more information.";
|
||||||
|
failureSnackbar.open();
|
||||||
|
loaded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
failureSnackbarText = "There was an error loading the products list. Check the browser console for more information.";
|
||||||
|
failureSnackbar.open();
|
||||||
|
loaded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await loadProducts();
|
||||||
|
await loadOrders();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getProduct(id) {
|
||||||
|
for (let i = 0; i < products.length; i++) {
|
||||||
|
if (products[i].id == id) {
|
||||||
|
return products[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProductsString(order) {
|
||||||
|
let resp = [];
|
||||||
|
|
||||||
|
let iter = Object.entries(order.products);
|
||||||
|
|
||||||
|
for (let i = 0; i < iter.length; i++) {
|
||||||
|
if (iter[i][1] === 0) continue;
|
||||||
|
resp.push(`${iter[i][1]}x ${getProduct(iter[i][0]).name} (#${iter[i][0]})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = 0.0;
|
||||||
|
|
||||||
|
function updateTotal() {
|
||||||
|
total = 0.0;
|
||||||
|
for (let i = 0; i < orders.length; i++) {
|
||||||
|
total += orders[i].total_usd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: orders, updateTotal();
|
||||||
|
|
||||||
|
let stripe_orders = 0;
|
||||||
|
let cash_orders = 0;
|
||||||
|
|
||||||
|
function updateStats() {
|
||||||
|
for (let i = 0; i < orders.length; i++) {
|
||||||
|
if (orders[i].order_type === "Cash") {
|
||||||
|
cash_orders += 1;
|
||||||
|
}
|
||||||
|
if (orders[i].order_type === "Stripe") {
|
||||||
|
stripe_orders += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: orders, updateStats();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Header selected="manage/orders"></Header>
|
||||||
|
|
||||||
|
<DataTable table$aria-label="Order list" style="width: 100%;">
|
||||||
|
<Head>
|
||||||
|
<Row>
|
||||||
|
<Cell numeric>ID</Cell>
|
||||||
|
<Cell style="width: 70%;">Products</Cell>
|
||||||
|
<Cell>Order Type</Cell>
|
||||||
|
<Cell>Revenue</Cell>
|
||||||
|
</Row>
|
||||||
|
</Head>
|
||||||
|
<Body>
|
||||||
|
{#each orders as order (order.id)}
|
||||||
|
<Row>
|
||||||
|
<Cell numeric>{order.id}</Cell>
|
||||||
|
<Cell>
|
||||||
|
{buildProductsString(order)}
|
||||||
|
</Cell>
|
||||||
|
<Cell>{order.order_type}</Cell>
|
||||||
|
<Cell>{formatter.format(order.total_usd)}</Cell>
|
||||||
|
</Row>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Cell numeric></Cell>
|
||||||
|
<Cell></Cell>
|
||||||
|
<Cell><b>Total Revenue</b></Cell>
|
||||||
|
<Cell>{formatter.format(total.toFixed(2))}</Cell>
|
||||||
|
</Row>
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
<LinearProgress indeterminate bind:closed={loaded} aria-label="Data is being loaded..." slot="progress" />
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<Paper>
|
||||||
|
<PaperTitle>Statistics</PaperTitle>
|
||||||
|
<PaperContent>
|
||||||
|
<p>Total number of orders: {orders.length}</p>
|
||||||
|
<p>order_type = CASH: {cash_orders}</p>
|
||||||
|
<p>order_type = STRIPE: {stripe_orders}</p>
|
||||||
|
<p>Percentage of orders using CASH: {(cash_orders / orders.length) * 100}%</p>
|
||||||
|
<p>Percentage of orders using STRIPE: {(stripe_orders / orders.length) * 100}%</p>
|
||||||
|
</PaperContent>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Snackbar bind:this={failureSnackbar}>
|
||||||
|
<SnackbarLabel>{failureSnackbarText}</SnackbarLabel>
|
||||||
|
</Snackbar>
|
|
@ -0,0 +1,8 @@
|
||||||
|
@use '@material/typography/mdc-typography';
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
float: right;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -313,6 +313,19 @@
|
||||||
"@material/feature-targeting" "^14.0.0"
|
"@material/feature-targeting" "^14.0.0"
|
||||||
"@material/rtl" "^14.0.0"
|
"@material/rtl" "^14.0.0"
|
||||||
|
|
||||||
|
"@material/form-field@^14.0.0":
|
||||||
|
version "14.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@material/form-field/-/form-field-14.0.0.tgz#a1d773f0d8d25cc7c59201ff62d2c11e5cfb7122"
|
||||||
|
integrity sha512-k1GNBj6Sp8A7Xsn5lTMp5DkUkg60HX7YkQIRyFz1qCDCKJRWh/ou7Z45GMMgKmG3aF6LfjIavc7SjyCl8e5yVg==
|
||||||
|
dependencies:
|
||||||
|
"@material/base" "^14.0.0"
|
||||||
|
"@material/feature-targeting" "^14.0.0"
|
||||||
|
"@material/ripple" "^14.0.0"
|
||||||
|
"@material/rtl" "^14.0.0"
|
||||||
|
"@material/theme" "^14.0.0"
|
||||||
|
"@material/typography" "^14.0.0"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
"@material/icon-button@^14.0.0":
|
"@material/icon-button@^14.0.0":
|
||||||
version "14.0.0"
|
version "14.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@material/icon-button/-/icon-button-14.0.0.tgz#cdfc7e7b967abe81d537fd7db916c9113c3a09b7"
|
resolved "https://registry.yarnpkg.com/@material/icon-button/-/icon-button-14.0.0.tgz#cdfc7e7b967abe81d537fd7db916c9113c3a09b7"
|
||||||
|
@ -681,6 +694,17 @@
|
||||||
"@smui/common" "^7.0.0-beta.8"
|
"@smui/common" "^7.0.0-beta.8"
|
||||||
svelte2tsx "^0.6.10"
|
svelte2tsx "^0.6.10"
|
||||||
|
|
||||||
|
"@smui/form-field@^7.0.0-beta.8":
|
||||||
|
version "7.0.0-beta.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@smui/form-field/-/form-field-7.0.0-beta.8.tgz#485e905ae15e19992b3216cc4433d586a5e233a5"
|
||||||
|
integrity sha512-B+8QJulCY13qiWiwLn9Fso3ZzYLCRBpq6MHE0o/4ZI0g5tI4yzyU1YNSXA3zWg6FJx7XukqOqoOA2sNCd00cjA==
|
||||||
|
dependencies:
|
||||||
|
"@material/feature-targeting" "^14.0.0"
|
||||||
|
"@material/form-field" "^14.0.0"
|
||||||
|
"@material/rtl" "^14.0.0"
|
||||||
|
"@smui/common" "^7.0.0-beta.8"
|
||||||
|
svelte2tsx "^0.6.10"
|
||||||
|
|
||||||
"@smui/icon-button@^7.0.0-beta.8":
|
"@smui/icon-button@^7.0.0-beta.8":
|
||||||
version "7.0.0-beta.8"
|
version "7.0.0-beta.8"
|
||||||
resolved "https://registry.yarnpkg.com/@smui/icon-button/-/icon-button-7.0.0-beta.8.tgz#27a3015597710e655df5e2c9f309557cd288c254"
|
resolved "https://registry.yarnpkg.com/@smui/icon-button/-/icon-button-7.0.0-beta.8.tgz#27a3015597710e655df5e2c9f309557cd288c254"
|
||||||
|
|
Loading…
Reference in New Issue