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 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};
|
||||
|
||||
pub mod db_products;
|
||||
pub mod route_products;
|
||||
pub mod error;
|
||||
pub mod route_order;
|
||||
pub mod db_orders;
|
||||
|
||||
#[derive(Debug, Schema)]
|
||||
#[schema(name = "lmsposschema", collections = [Product])]
|
||||
#[schema(name = "lmsposschema", collections = [Product, Order])]
|
||||
pub struct DbSchema;
|
||||
|
||||
pub struct AppState {
|
||||
|
@ -41,6 +45,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||
.service(get_product)
|
||||
.service(delete_product)
|
||||
.service(status)
|
||||
.service(get_orders)
|
||||
.service(cash_order)
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.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",
|
||||
"@smui/button": "^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/data-table": "^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/linear-progress": "^7.0.0-beta.8",
|
||||
"@smui/paper": "^7.0.0-beta.8",
|
||||
|
|
|
@ -23,6 +23,12 @@
|
|||
<Label>Manage Env</Label>
|
||||
</Button>
|
||||
</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>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -6,15 +6,22 @@
|
|||
import Button, {Label as ButtonLabel, Group as ButtonGroup} from "@smui/button";
|
||||
import DataTable, { Head, Body, Row, Cell } from '@smui/data-table';
|
||||
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 productsCounter = {};
|
||||
|
||||
let failureSnackbar: Snackbar;
|
||||
let failureSnackbarText = "";
|
||||
let snackbar: Snackbar;
|
||||
let snackbarText = "";
|
||||
|
||||
let loaded;
|
||||
|
||||
let loadedCashIn = false;
|
||||
|
||||
const formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'});
|
||||
|
||||
// load products from the backend
|
||||
|
@ -32,14 +39,14 @@
|
|||
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();
|
||||
snackbarText = "There was an error loading the products list. Check the browser console for more information.";
|
||||
snackbar.open();
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
failureSnackbarText = "There was an error loading the products list. Check the browser console for more information.";
|
||||
failureSnackbar.open();
|
||||
snackbarText = "There was an error loading the products list. Check the browser console for more information.";
|
||||
snackbar.open();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -57,13 +64,122 @@
|
|||
|
||||
onMount(() => {
|
||||
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>
|
||||
|
||||
<Header selected="pos" />
|
||||
|
||||
<Snackbar bind:this={failureSnackbar}>
|
||||
<SnackbarLabel>{failureSnackbarText}</SnackbarLabel>
|
||||
<Snackbar bind:this={snackbar}>
|
||||
<SnackbarLabel>{snackbarText}</SnackbarLabel>
|
||||
</Snackbar>
|
||||
|
||||
<DataTable table$aria-label="Product list" style="width: 100%;">
|
||||
|
@ -91,10 +207,10 @@
|
|||
<Cell>{formatter.format(productsCounter[product.id] * product.price_usd)}</Cell>
|
||||
<Cell>
|
||||
<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>
|
||||
</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>
|
||||
</Button>
|
||||
<Button variant="outlined" on:click={() => {productsCounter[product.id] = 0}}>
|
||||
|
@ -115,10 +231,10 @@
|
|||
</Cell>
|
||||
<Cell>
|
||||
<ButtonGroup>
|
||||
<Button variant="outlined">
|
||||
<Button disabled={!hasProducts} variant="outlined" on:click={() => {cashIn = total.toFixed(2); cashDialogOpen = true;}}>
|
||||
<ButtonLabel>Cash In</ButtonLabel>
|
||||
</Button>
|
||||
<Button variant="outlined">
|
||||
<Button disabled={!hasProducts} variant="outlined">
|
||||
<ButtonLabel>Stripe</ButtonLabel>
|
||||
</Button>
|
||||
<Button variant="unelevated" on:click={() => {for (let i = 0; i < products.length; i++) {productsCounter[products[i].id] = 0}}}>
|
||||
|
@ -131,3 +247,50 @@
|
|||
|
||||
<LinearProgress indeterminate bind:closed={loaded} aria-label="Data is being loaded..." slot="progress" />
|
||||
</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/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":
|
||||
version "14.0.0"
|
||||
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"
|
||||
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":
|
||||
version "7.0.0-beta.8"
|
||||
resolved "https://registry.yarnpkg.com/@smui/icon-button/-/icon-button-7.0.0-beta.8.tgz#27a3015597710e655df5e2c9f309557cd288c254"
|
||||
|
|
Loading…
Reference in New Issue