Add sorting and scanner checkout
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# Welcome to Remix + Vite!
|
||||
|
||||
📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/guides/vite) for details on supported features.
|
||||
# Beer inventory
|
||||
|
||||
## Development
|
||||
|
||||
Run the Vite dev server:
|
||||
|
||||
```shellscript
|
||||
npm zenstack generate
|
||||
npm prisma db push
|
||||
npm run dev
|
||||
```
|
||||
|
||||
@@ -24,13 +24,10 @@ Then run the app in production mode:
|
||||
npm start
|
||||
```
|
||||
|
||||
Now you'll need to pick a host to deploy it to.
|
||||
## Todo
|
||||
- Filtering
|
||||
- Easy inventory updating for admin
|
||||
- Mobile styling frontpage
|
||||
- Undo checkout for admin
|
||||
|
||||
### DIY
|
||||
|
||||
If you're familiar with deploying Node applications, the built-in Remix app server is production-ready.
|
||||
|
||||
Make sure to deploy the output of `npm run build`
|
||||
|
||||
- `build/server`
|
||||
- `build/client`
|
||||
|
||||
+2
-2
@@ -24,10 +24,10 @@ function createDatabaseSessionStorage({
|
||||
},
|
||||
async updateData(id, data, expires) {
|
||||
if(data && data.id){
|
||||
await db.session.update({data:{data: JSON.stringify({ data }), user:{connect:{id: data.id}}}, where: {id: id}});
|
||||
await db.session.upsert({create:{data: JSON.stringify({ data }), user:{connect:{id: data.id}}}, update:{data: JSON.stringify({ data }), user:{connect:{id: data.id}}}, where: {id: id}});
|
||||
}
|
||||
else {
|
||||
await db.session.update({data:{data: JSON.stringify({ data })}, where: {id: id}});
|
||||
await db.session.upsert({create:{data: JSON.stringify({ data })}, update:{data: JSON.stringify({ data })}, where: {id: id}});
|
||||
}
|
||||
},
|
||||
async deleteData(id) {
|
||||
|
||||
@@ -35,7 +35,12 @@ function DrinkCard(arg:Arguments) {
|
||||
|
||||
var inventory = "";
|
||||
if(isDrinkWithContainers(arg.drink)){
|
||||
inventory = "Inventory: " + arg.drink.containers.length;
|
||||
let inventoryNum = 0;
|
||||
arg.drink.containers.map((container) => {
|
||||
inventoryNum += container.inventory;
|
||||
})
|
||||
|
||||
inventory = "Inventory: " + inventoryNum;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -53,7 +58,6 @@ function DrinkCard(arg:Arguments) {
|
||||
{ isDrinkWithStyle(arg.drink) ? (
|
||||
<ListGroup.Item>Style: {arg.drink.style.name}</ListGroup.Item>
|
||||
) : ""}
|
||||
<ListGroup.Item>Allergy: {arg.drink.gluten ? "G" : ""} {arg.drink.lactose ? "L" : ""} {arg.drink.organic ? "B" : ""}</ListGroup.Item>
|
||||
</ListGroup>
|
||||
<Card.Footer className="text-muted">{inventory}</Card.Footer>
|
||||
</Card>
|
||||
|
||||
@@ -1,42 +1,31 @@
|
||||
|
||||
import { Link } from '@remix-run/react';
|
||||
import { Card, Col, Container, Image, ListGroup, Row } from 'react-bootstrap';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import { DrinkComposite, isDrinkWithContainers, isDrinkWithManufacturer, isDrinkWithStyle } from '~/models/types';
|
||||
import { Manufacturer } from '@zenstackhq/runtime/models';
|
||||
import { Col, Container, Image, Row } from 'react-bootstrap';
|
||||
import { ContainerWithSection, DrinkComposite, isDrinkWithContainersAndSection, isDrinkWithManufacturer, isDrinkWithStyle } from '~/models/types';
|
||||
|
||||
interface Arguments {
|
||||
drink: DrinkComposite
|
||||
}
|
||||
|
||||
function DrinkPage(arg:Arguments) {
|
||||
var trimmedDescription = arg.drink.description.length > 120 ? arg.drink.description.substring(0, 120) + "..." : arg.drink.description;
|
||||
var link = "/drink/";
|
||||
|
||||
switch (arg.drink.type) {
|
||||
case 'Beer':
|
||||
link = "/beer/";
|
||||
break;
|
||||
case 'Wine':
|
||||
link = "/wine/";
|
||||
break;
|
||||
case 'Soda':
|
||||
link = "/soda/";
|
||||
break;
|
||||
}
|
||||
|
||||
var borderColor = "#bbb";
|
||||
if(isDrinkWithStyle(arg.drink)){
|
||||
borderColor = "#" + arg.drink.style.color;
|
||||
}
|
||||
|
||||
var manufacturer = "";
|
||||
var manufacturer: Manufacturer | undefined = undefined;
|
||||
if(isDrinkWithManufacturer(arg.drink)){
|
||||
manufacturer = arg.drink.manufacturer.name + " (" + arg.drink.manufacturer.country_id + ")";
|
||||
manufacturer = arg.drink.manufacturer;
|
||||
}
|
||||
|
||||
var inventory = "";
|
||||
if(isDrinkWithContainers(arg.drink)){
|
||||
inventory = "Inventory: " + arg.drink.containers.length;
|
||||
var containers : ContainerWithSection[] = [];
|
||||
var totalinventory = 0;
|
||||
if(isDrinkWithContainersAndSection(arg.drink)){
|
||||
containers = arg.drink.containers;
|
||||
containers.map((container) => {
|
||||
totalinventory += container.inventory;
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -60,18 +49,22 @@ function DrinkPage(arg:Arguments) {
|
||||
<Col md={6}>
|
||||
<div className="h-100 p-5 text-bg-dark rounded-3">
|
||||
<h2>Inventory</h2>
|
||||
{arg.drink.containers.map((container) => (
|
||||
{totalinventory == 0 ? (
|
||||
"No inventory"
|
||||
) : ""}
|
||||
{containers.map((container) => (
|
||||
<p>{container.section.name} | {container.type} | {container.inventory}</p>
|
||||
))}
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{ manufacturer ? (
|
||||
<Col md={6}>
|
||||
<div className="h-100 p-5 bg-body-tertiary border rounded-3">
|
||||
<h2>{arg.drink.manufacturer.name}</h2>
|
||||
<p>{arg.drink.manufacturer.description}</p>
|
||||
<h2>{manufacturer.name}</h2>
|
||||
<p>{manufacturer.description}</p>
|
||||
</div>
|
||||
</Col>
|
||||
) : ""}
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -47,11 +47,33 @@ function DrinkFilter(arg : Arguments) {
|
||||
|
||||
</div>);
|
||||
|
||||
const sortKey = arg.searchParams.get("sort") || "name";
|
||||
|
||||
return (
|
||||
<Form noValidate method="GET" ref={form} onChange={(event) => {submit(event.currentTarget); }}>
|
||||
<Row>
|
||||
<Col xs={6} md={12}>
|
||||
<Form.Group controlId="drink-type">
|
||||
<Form.Group controlId="sort" className='mb-2'>
|
||||
<Form.Label>Sort</Form.Label>
|
||||
{ [
|
||||
["name", "Name"],
|
||||
["abv-asc", "ABV Low -> High"],
|
||||
["abv-desc", "ABV High -> Low"],
|
||||
["inv-asc", "Inventoy Low -> High"],
|
||||
["inv-desc", "Inventory High -> Low"]
|
||||
].map((pair) => (
|
||||
<Form.Check
|
||||
id={"sort-" + pair[0]}
|
||||
type="radio"
|
||||
label={pair[1]}
|
||||
name="sort"
|
||||
value={pair[0]}
|
||||
defaultChecked={sortKey == pair[0]}
|
||||
/>
|
||||
))}
|
||||
|
||||
</Form.Group>
|
||||
<Form.Group controlId="drink-type" className='mb-2'>
|
||||
<Form.Label>Drink type</Form.Label>
|
||||
{ ["Beer", "Wine", "Soda", "Cocktail"].map(key => (
|
||||
<Form.Check
|
||||
@@ -65,7 +87,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
))}
|
||||
|
||||
</Form.Group>
|
||||
<Form.Group controlId="drink-modifiers">
|
||||
<Form.Group controlId="drink-modifiers" className='mb-2'>
|
||||
<Form.Label>Drink</Form.Label>
|
||||
{ ["Gluten-free", "Lactose-free", "Organic"].map(key => (
|
||||
<Form.Check
|
||||
@@ -80,7 +102,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
<Form.Label>ABV</Form.Label>
|
||||
{slider()}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="manufacturer">
|
||||
<Form.Group controlId="manufacturer" className='mb-2'>
|
||||
<Form.Label>Manufacturer</Form.Label>
|
||||
{ arg.manufacturers.map(manufacturer => (
|
||||
<Form.Check
|
||||
@@ -96,7 +118,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
</Col>
|
||||
<Col>
|
||||
{ showBeerFilter ? (
|
||||
<Form.Group controlId="beer-style">
|
||||
<Form.Group controlId="beer-style" className='mb-2'>
|
||||
<Form.Label>Beer style</Form.Label>
|
||||
{ arg.beerStyles.map(style => (
|
||||
<Form.Check
|
||||
@@ -111,7 +133,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
</Form.Group>
|
||||
) : ""}
|
||||
{ showWineFilter ? (
|
||||
<Form.Group controlId="wine-style">
|
||||
<Form.Group controlId="wine-style" className='mb-2'>
|
||||
<Form.Label>Wine style</Form.Label>
|
||||
{ arg.wineStyles.map(style => (
|
||||
<Form.Check
|
||||
@@ -126,7 +148,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
</Form.Group>
|
||||
) : ""}
|
||||
{ showSodaFilter ? (
|
||||
<Form.Group controlId="soda-carbonated">
|
||||
<Form.Group controlId="soda-carbonated" className='mb-2'>
|
||||
<Form.Label>Soda</Form.Label>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
@@ -138,7 +160,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
</Form.Group>
|
||||
) : ""}
|
||||
{ showCocktailFilter ? (
|
||||
<Form.Group controlId="cocktail-mix">
|
||||
<Form.Group controlId="cocktail-mix" className='mb-2'>
|
||||
<Form.Label>Cocktail</Form.Label>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
@@ -149,7 +171,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
/>
|
||||
</Form.Group>
|
||||
) : ""}
|
||||
<Form.Group controlId="cocktail-mix">
|
||||
<Form.Group controlId="inventory" className='mb-2'>
|
||||
<Form.Label>No Inventory</Form.Label>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
|
||||
@@ -2,6 +2,7 @@ import {LinkContainer} from 'react-router-bootstrap'
|
||||
import {Container, Nav, Navbar, NavDropdown} from 'react-bootstrap';
|
||||
import { User } from '@zenstackhq/runtime/models';
|
||||
import { Link } from '@remix-run/react';
|
||||
import { UserType } from '@prisma/client';
|
||||
|
||||
export type HeaderData = {user?: (User)} & {loggedin: boolean};
|
||||
|
||||
@@ -11,8 +12,13 @@ function Header(data: Arguments) {
|
||||
var loggedin = data.data.loggedin;
|
||||
|
||||
var username = "";
|
||||
if(loggedin){
|
||||
username = data.data.user!.username;
|
||||
var showScanButtons = false;
|
||||
var showAdminButtons = false;
|
||||
|
||||
if(loggedin && data.data.user){
|
||||
username = data.data.user.username;
|
||||
showScanButtons = (data.data.user.type == "Scanner");
|
||||
showAdminButtons = (data.data.user.type == "Admin");
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -24,17 +30,18 @@ function Header(data: Arguments) {
|
||||
<Navbar.Toggle />
|
||||
<Navbar.Collapse>
|
||||
<Nav className="me-auto">
|
||||
<LinkContainer to="/beers">
|
||||
<Nav.Link>Beers</Nav.Link>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/search">
|
||||
<Nav.Link>Search</Nav.Link>
|
||||
</LinkContainer>
|
||||
{ showScanButtons ? (
|
||||
<LinkContainer to="/scan">
|
||||
<Nav.Link>Scan</Nav.Link>
|
||||
</LinkContainer>
|
||||
) : "" }
|
||||
{ showAdminButtons ? (
|
||||
<LinkContainer to="/new/beer">
|
||||
<Nav.Link>Add Beer</Nav.Link>
|
||||
</LinkContainer>
|
||||
) : "" }
|
||||
</Nav>
|
||||
{ data.data.loggedin ? (
|
||||
{ loggedin ? (
|
||||
<Nav>
|
||||
<LinkContainer to="/logout">
|
||||
<Nav.Link>Logout ({username})</Nav.Link>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
|
||||
import { enhance } from "@zenstackhq/runtime";
|
||||
import { db } from "~/utils/db.server";
|
||||
import { DrinkComposite } from "./types";
|
||||
import lodash from "lodash"
|
||||
|
||||
export async function findDrinksFromSearch(searchParams: URLSearchParams) : Promise<DrinkComposite[]> {
|
||||
const dbe = enhance(db);
|
||||
|
||||
var result : DrinkComposite[] = [];
|
||||
var noDrinkSelected = searchParams.getAll("drink").length == 0;
|
||||
|
||||
let whereBase = {delegate_aux_drink: {AND: <any>[]}};
|
||||
|
||||
if(searchParams.has("gluten-free")){
|
||||
whereBase.delegate_aux_drink.AND.push({NOT: {gluten: true}});
|
||||
}
|
||||
if(searchParams.has("lactose-free")){
|
||||
whereBase.delegate_aux_drink.AND.push({NOT: {lactose: true}});
|
||||
}
|
||||
if(searchParams.has("organic")){
|
||||
whereBase.delegate_aux_drink.AND.push({organic: true});
|
||||
}
|
||||
|
||||
if(searchParams.has("min-abv")){
|
||||
let minabv = Number(searchParams.get("min-abv"));
|
||||
whereBase.delegate_aux_drink.AND.push({abv: {gte: minabv}});
|
||||
}
|
||||
if(searchParams.has("max-abv")){
|
||||
let maxabv = Number(searchParams.get("max-abv"));
|
||||
whereBase.delegate_aux_drink.AND.push({abv: {lte: maxabv}});
|
||||
}
|
||||
|
||||
if(searchParams.has("manufacturer")){
|
||||
let whereManufacturer = {manufacturer: {id: {in: searchParams.getAll("manufacturer").map(Number)}}};
|
||||
whereBase = lodash.merge(whereBase, whereManufacturer);
|
||||
}
|
||||
|
||||
if(!searchParams.has("inventory")){
|
||||
let whereInventory = {containers: {some: {inventory: {gt: 0}}}};
|
||||
whereBase = lodash.merge(whereBase, whereInventory);
|
||||
}
|
||||
|
||||
// Beers
|
||||
if(searchParams.has("drink", "Beer") || noDrinkSelected){
|
||||
|
||||
let where = Object.assign({}, whereBase, {style: {}});
|
||||
|
||||
// Beer styles
|
||||
if(searchParams.has("beerstyle")){
|
||||
where.style = {id: {in: searchParams.getAll("beerstyle").map(Number)}};
|
||||
}
|
||||
|
||||
result = result.concat(await dbe.beer.findMany({
|
||||
include: {style: true, manufacturer: true, containers: {include: {section: true}}},
|
||||
where: where
|
||||
}));
|
||||
}
|
||||
|
||||
if(searchParams.has("drink", "Wine") || noDrinkSelected){
|
||||
|
||||
let where = Object.assign({}, whereBase, {style: {}});
|
||||
|
||||
// Beer styles
|
||||
if(searchParams.has("winestyle")){
|
||||
where.style = {id: {in: searchParams.getAll("winestyle").map(Number)}};
|
||||
}
|
||||
|
||||
result = result.concat(await dbe.wine.findMany({
|
||||
include: {style: true, manufacturer: true, containers: {include: {section: true}}},
|
||||
where: where
|
||||
}));
|
||||
}
|
||||
|
||||
if(searchParams.has("drink", "Soda") || noDrinkSelected){
|
||||
|
||||
let where = whereBase;
|
||||
|
||||
// Carbonated
|
||||
if(searchParams.has("carbonated")){
|
||||
where = Object.assign({}, whereBase, {carbonated: true});
|
||||
}
|
||||
|
||||
result = result.concat(await dbe.soda.findMany({
|
||||
include: {manufacturer: true, containers: {include: {section: true}}},
|
||||
where: where
|
||||
}));
|
||||
}
|
||||
|
||||
if(searchParams.has("drink", "Cocktail") || noDrinkSelected){
|
||||
let where = whereBase;
|
||||
|
||||
// Carbonated
|
||||
if(searchParams.has("mix")){
|
||||
where = Object.assign({}, whereBase, {mix: true});
|
||||
}
|
||||
|
||||
result = result.concat(await dbe.cocktail.findMany({
|
||||
include: {manufacturer: true, containers: {include: {section: true}}},
|
||||
where: where
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
|
||||
import { enhance } from "@zenstackhq/runtime";
|
||||
import { db } from "~/utils/db.server";
|
||||
import { DrinkComposite } from "./types";
|
||||
import lodash from "lodash"
|
||||
import { DrinkType, Manufacturer } from "@prisma/client";
|
||||
|
||||
export async function findIdFromSlug(slug:string | undefined) : Promise<number | undefined> {
|
||||
@@ -27,104 +25,5 @@ export async function findManufacturersFromSearch(searchParams: URLSearchParams)
|
||||
result = result.concat(await dbe.manufacturer.findMany());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
export async function findDrinksFromSearch(searchParams: URLSearchParams) : Promise<DrinkComposite[]> {
|
||||
const dbe = enhance(db);
|
||||
|
||||
var result : DrinkComposite[] = [];
|
||||
var noDrinkSelected = searchParams.getAll("drink").length == 0;
|
||||
|
||||
let whereBase = {delegate_aux_drink: {AND: <any>[]}};
|
||||
|
||||
if(searchParams.has("gluten-free")){
|
||||
whereBase.delegate_aux_drink.AND.push({NOT: {gluten: true}});
|
||||
}
|
||||
if(searchParams.has("lactose-free")){
|
||||
whereBase.delegate_aux_drink.AND.push({NOT: {lactose: true}});
|
||||
}
|
||||
if(searchParams.has("organic")){
|
||||
whereBase.delegate_aux_drink.AND.push({organic: true});
|
||||
}
|
||||
|
||||
if(searchParams.has("min-abv")){
|
||||
let minabv = Number(searchParams.get("min-abv"));
|
||||
whereBase.delegate_aux_drink.AND.push({abv: {gte: minabv}});
|
||||
}
|
||||
if(searchParams.has("max-abv")){
|
||||
let maxabv = Number(searchParams.get("max-abv"));
|
||||
whereBase.delegate_aux_drink.AND.push({abv: {lte: maxabv}});
|
||||
}
|
||||
|
||||
if(searchParams.has("manufacturer")){
|
||||
let whereManufacturer = {manufacturer: {id: {in: searchParams.getAll("manufacturer").map(Number)}}};
|
||||
whereBase = lodash.merge(whereBase, whereManufacturer);
|
||||
}
|
||||
|
||||
if(!searchParams.has("inventory")){
|
||||
let whereInventory = {containers: {some: {inventory: {gt: 0}}}};
|
||||
whereBase = lodash.merge(whereBase, whereInventory);
|
||||
}
|
||||
|
||||
// Beers
|
||||
if(searchParams.has("drink", "Beer") || noDrinkSelected){
|
||||
|
||||
let where = Object.assign({}, whereBase, {style: {}});
|
||||
|
||||
// Beer styles
|
||||
if(searchParams.has("beerstyle")){
|
||||
where.style = {id: {in: searchParams.getAll("beerstyle").map(Number)}};
|
||||
}
|
||||
|
||||
result = result.concat(await dbe.beer.findMany({
|
||||
include: {style: true, manufacturer: true, containers: {include: {section: true}}},
|
||||
where: where
|
||||
}));
|
||||
}
|
||||
|
||||
if(searchParams.has("drink", "Wine") || noDrinkSelected){
|
||||
|
||||
let where = Object.assign({}, whereBase, {style: {}});
|
||||
|
||||
// Beer styles
|
||||
if(searchParams.has("winestyle")){
|
||||
where.style = {id: {in: searchParams.getAll("winestyle").map(Number)}};
|
||||
}
|
||||
|
||||
result = result.concat(await dbe.wine.findMany({
|
||||
include: {style: true, manufacturer: true, containers: {include: {section: true}}},
|
||||
where: where
|
||||
}));
|
||||
}
|
||||
|
||||
if(searchParams.has("drink", "Soda") || noDrinkSelected){
|
||||
|
||||
let where = whereBase;
|
||||
|
||||
// Carbonated
|
||||
if(searchParams.has("carbonated")){
|
||||
where = Object.assign({}, whereBase, {carbonated: true});
|
||||
}
|
||||
|
||||
result = result.concat(await dbe.soda.findMany({
|
||||
include: {manufacturer: true, containers: {include: {section: true}}},
|
||||
where: where
|
||||
}));
|
||||
}
|
||||
|
||||
if(searchParams.has("drink", "Cocktail") || noDrinkSelected){
|
||||
let where = whereBase;
|
||||
|
||||
// Carbonated
|
||||
if(searchParams.has("mix")){
|
||||
where = Object.assign({}, whereBase, {mix: true});
|
||||
}
|
||||
|
||||
result = result.concat(await dbe.cocktail.findMany({
|
||||
include: {manufacturer: true, containers: {include: {section: true}}},
|
||||
where: where
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { DrinkComposite, isDrinkWithContainers } from "./types";
|
||||
|
||||
function sortName(n1:DrinkComposite, n2:DrinkComposite) : number{
|
||||
if (n1.name > n2.name) {
|
||||
return 1;
|
||||
}
|
||||
if (n1.name < n2.name) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortAbvLowHigh(n1:DrinkComposite, n2:DrinkComposite) : number{
|
||||
return n1.abv - n2.abv;
|
||||
}
|
||||
|
||||
function sortAbvHighLow(n1:DrinkComposite, n2:DrinkComposite) : number{
|
||||
return n2.abv - n1.abv;
|
||||
}
|
||||
|
||||
function totalInventory(n:DrinkComposite) : number{
|
||||
|
||||
if(isDrinkWithContainers(n)){
|
||||
let total = 0;
|
||||
n.containers.map((container) =>{
|
||||
total += container.inventory;
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortInventoryLowHigh(n1:DrinkComposite, n2:DrinkComposite) : number{
|
||||
return totalInventory(n1) - totalInventory(n2);
|
||||
}
|
||||
|
||||
function sortInventoryHighLow(n1:DrinkComposite, n2:DrinkComposite) : number{
|
||||
return totalInventory(n2) - totalInventory(n1);
|
||||
}
|
||||
|
||||
export function sortDrinksFromSearch(searchParams: URLSearchParams, searchResult: DrinkComposite[]) : DrinkComposite[] {
|
||||
|
||||
var callback = sortName;
|
||||
|
||||
if(searchParams.has("sort", "name")){
|
||||
callback = sortName;
|
||||
}
|
||||
|
||||
if(searchParams.has("sort", "abv-asc")){
|
||||
callback = sortAbvLowHigh;
|
||||
}
|
||||
|
||||
if(searchParams.has("sort", "abv-desc")){
|
||||
callback = sortAbvHighLow;
|
||||
}
|
||||
|
||||
if(searchParams.has("sort", "inv-asc")){
|
||||
callback = sortInventoryLowHigh;
|
||||
}
|
||||
|
||||
if(searchParams.has("sort", "inv-desc")){
|
||||
callback = sortInventoryHighLow;
|
||||
}
|
||||
|
||||
var sortedArray: DrinkComposite[] = searchResult.sort(callback);
|
||||
|
||||
return sortedArray;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Drink, Beer, BeerStyle, Wine, WineStyle, Soda, Manufacturer, Container, Cocktail, Section } from '@zenstackhq/runtime/models';
|
||||
|
||||
export type ContainerWithSection = Container & { section: Section}
|
||||
export type DrinkWithContainersAndSection = (Drink & { containers: ContainerWithSection[]})
|
||||
export type DrinkWithContainers = (Drink & { containers: ContainerWithSection[]}) | (Drink & { containers: Container[]})
|
||||
export type DrinkWithManufacturer = Drink & { manufacturer: Manufacturer}
|
||||
|
||||
@@ -13,6 +14,10 @@ export function isDrinkWithContainers(drink:DrinkComposite) : drink is DrinkWith
|
||||
return ((drink as { containers: Container[]}).containers != undefined) || ((drink as { containers: ContainerWithSection[]}).containers != undefined);
|
||||
}
|
||||
|
||||
export function isDrinkWithContainersAndSection(drink:DrinkComposite) : drink is DrinkWithContainersAndSection{
|
||||
return ((drink as { containers: ContainerWithSection[]}).containers != undefined);
|
||||
}
|
||||
|
||||
export function isDrinkWithManufacturer(drink:DrinkComposite) : drink is DrinkWithManufacturer{
|
||||
return (drink as { manufacturer: Manufacturer}).manufacturer != undefined;
|
||||
}
|
||||
|
||||
+36
-10
@@ -1,10 +1,12 @@
|
||||
import { LoaderFunctionArgs, json } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import { enhance } from "@zenstackhq/runtime";
|
||||
import { Col, Container, Row } from "react-bootstrap";
|
||||
import { Accordion, Button, Col, Container, Row, useAccordionButton } from "react-bootstrap";
|
||||
import DrinkCard from "~/components/cards/drink.card";
|
||||
import DrinkFilter from "~/components/filters/drink.filter";
|
||||
import { findDrinksFromSearch, findManufacturersFromSearch } from "~/models/drinks.server";
|
||||
import { findDrinksFromSearch } from "~/models/drinks.filter.server";
|
||||
import { findManufacturersFromSearch } from "~/models/drinks.server";
|
||||
import { sortDrinksFromSearch } from "~/models/drinks.sort.server";
|
||||
|
||||
import { db } from "~/utils/db.server";
|
||||
|
||||
@@ -17,7 +19,8 @@ export const loader = async ({request} : LoaderFunctionArgs) => {
|
||||
const wineStyles = await dbe.wineStyle.findMany();
|
||||
const manufacturers = await findManufacturersFromSearch(url.searchParams);
|
||||
|
||||
const drinkResults = await findDrinksFromSearch(url.searchParams);
|
||||
const drinkResultsUnsorted = await findDrinksFromSearch(url.searchParams);
|
||||
const drinkResults = sortDrinksFromSearch(url.searchParams, drinkResultsUnsorted);
|
||||
|
||||
return json({beerStyles, wineStyles, manufacturers, drinkResults});
|
||||
};
|
||||
@@ -27,16 +30,39 @@ export default function BeersRoute() {
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
function CustomToggle({ children, eventKey }) {
|
||||
const decoratedOnClick = useAccordionButton(eventKey, () =>
|
||||
console.log('totally custom!'),
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={decoratedOnClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row className="mt-3">
|
||||
<Col xs={12} md={4} xl={3}>
|
||||
<DrinkFilter
|
||||
beerStyles={loadData.beerStyles}
|
||||
wineStyles={loadData.wineStyles}
|
||||
manufacturers={loadData.manufacturers}
|
||||
searchParams={searchParams} >
|
||||
</DrinkFilter>
|
||||
<Col xs={12} md={4} xl={3} className="mb-3">
|
||||
<Accordion>
|
||||
<CustomToggle eventKey="0">Sorting and Filtering</CustomToggle>
|
||||
|
||||
<Accordion.Collapse eventKey="0">
|
||||
<div className="h-100 p-3 mt-3 text-bg-dark rounded-3">
|
||||
<DrinkFilter
|
||||
beerStyles={loadData.beerStyles}
|
||||
wineStyles={loadData.wineStyles}
|
||||
manufacturers={loadData.manufacturers}
|
||||
searchParams={searchParams} >
|
||||
</DrinkFilter>
|
||||
</div>
|
||||
</Accordion.Collapse>
|
||||
</Accordion>
|
||||
</Col>
|
||||
<Col>
|
||||
<Row xs={2} md={3} lg={4} className="g-4">
|
||||
|
||||
+91
-41
@@ -1,65 +1,115 @@
|
||||
import type { ActionFunctionArgs } from "@remix-run/node";
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { useActionData } from "@remix-run/react";
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { useActionData, useLoaderData } from "@remix-run/react";
|
||||
import { enhance } from "@zenstackhq/runtime";
|
||||
import { Col, Container, Form, Row } from "react-bootstrap";
|
||||
import { Col, Container, Form, Row, Table } from "react-bootstrap";
|
||||
import { getSession } from "~/auth/session";
|
||||
import timeAgo from "~/utils/datetime";
|
||||
|
||||
import { db } from "~/utils/db.server";
|
||||
import { badRequest } from "~/utils/request.server";
|
||||
|
||||
export async function loader({
|
||||
request,
|
||||
}: LoaderFunctionArgs) {
|
||||
const session = await getSession(
|
||||
request.headers.get("Cookie")
|
||||
);
|
||||
|
||||
if(!session.has("user_id")){
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
const dbe = session.has("userid") ? enhance(db) : enhance(db, {user: session.get("user")});
|
||||
|
||||
const history = await dbe.history.findMany({select: {checkoutAt: true ,container: {select: {drink: true}}}, take: 10, orderBy: {checkoutAt: "desc"}});
|
||||
return json({history});
|
||||
}
|
||||
|
||||
export const action = async ({
|
||||
request,
|
||||
}: ActionFunctionArgs) => {
|
||||
const dbe = enhance(db);
|
||||
const session = await getSession(
|
||||
request.headers.get("Cookie")
|
||||
);
|
||||
|
||||
const dbe = session.has("userid") ? enhance(db) : enhance(db, {user: session.get("user")});
|
||||
|
||||
const form = await request.formData();
|
||||
const barcode = form.get("barcode");
|
||||
// we do this type check to be extra sure and to make TypeScript happy
|
||||
// we'll explore validation next!
|
||||
if (
|
||||
typeof barcode !== "string"
|
||||
) {
|
||||
return badRequest({
|
||||
fieldErrors: null,
|
||||
fields: null,
|
||||
formError: "Form not submitted correctly.",
|
||||
});
|
||||
const barcode = form.get("barcode")?.toString() || "";
|
||||
|
||||
const container = await dbe.container.findUnique({
|
||||
select: {id: true, inventory: true},
|
||||
where: {barcode: barcode}
|
||||
});
|
||||
|
||||
// No drink found for barcode, do nothing
|
||||
if(!container){
|
||||
return json({error: "Drink not found!"});
|
||||
}
|
||||
// No inventory, do nothing
|
||||
if(container.inventory <= 0){
|
||||
return json({error: "No inventory!"});
|
||||
}
|
||||
|
||||
const drink = await dbe.drink.findFirst({
|
||||
select: {slug: true},
|
||||
where: {containers: {some: {barcode: barcode}}}
|
||||
});
|
||||
if(drink) return redirect(`/beer/${drink.slug}`);
|
||||
// Log entry
|
||||
await dbe.history.create({data: {container: {connect: {id: container.id}}}});
|
||||
|
||||
// Update inventory
|
||||
const newInventory = Math.max(container.inventory - 1, 0);
|
||||
await dbe.container.update({data: {inventory: newInventory}, where: {id: container.id}});
|
||||
|
||||
return json({error: ""});
|
||||
};
|
||||
|
||||
export default function ScanRoute() {
|
||||
const aData = useActionData<typeof action>();
|
||||
const lData = useLoaderData<typeof loader>();
|
||||
|
||||
const hasError = aData?.error ? aData.error.length > 0 : false;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row className="mt-3">
|
||||
<Col xs={4}>
|
||||
<Form method="post">
|
||||
<div>
|
||||
<label>
|
||||
Scan barcode:
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
name="barcode"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Form method="post" noValidate>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
Scan barcode:
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="barcode"
|
||||
isInvalid={hasError}
|
||||
autoFocus
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{aData?.error}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mt-3">
|
||||
<Col xs={12}>
|
||||
<h3>History</h3>
|
||||
<Table striped>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Beer</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lData.history.map((entry) => (
|
||||
<tr key={entry.container.drink.slug}>
|
||||
<td>{entry.container.drink.name}</td>
|
||||
<td>{timeAgo(new Date(entry.checkoutAt))}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="error-container">
|
||||
Something unexpected went wrong. Sorry about that.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export default function timeAgo(previous: Date) {
|
||||
|
||||
var msPerMinute = 60 * 1000;
|
||||
var msPerHour = msPerMinute * 60;
|
||||
var msPerDay = msPerHour * 24;
|
||||
var msPerMonth = msPerDay * 30;
|
||||
var msPerYear = msPerDay * 365;
|
||||
|
||||
const now = new Date();
|
||||
const elapsed : number = (now.getTime() - previous.getTime());
|
||||
|
||||
if (elapsed < msPerMinute) {
|
||||
return Math.round(elapsed/1000) + ' seconds ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed/msPerMinute) + ' minutes ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerDay ) {
|
||||
return Math.round(elapsed/msPerHour ) + ' hours ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerMonth) {
|
||||
return 'approximately ' + Math.round(elapsed/msPerDay) + ' days ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerYear) {
|
||||
return 'approximately ' + Math.round(elapsed/msPerMonth) + ' months ago';
|
||||
}
|
||||
|
||||
else {
|
||||
return 'approximately ' + Math.round(elapsed/msPerYear ) + ' years ago';
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,7 @@ model Container {
|
||||
portions Int?
|
||||
price Float @default(0.0)
|
||||
inventory Int @default(0)
|
||||
checkouts History[]
|
||||
}
|
||||
|
||||
/// @@allow('read', true)
|
||||
@@ -169,6 +170,16 @@ model Country {
|
||||
manufacturers Manufacturer[]
|
||||
}
|
||||
|
||||
/// @@allow('read', true)
|
||||
/// @@allow('create', auth().type == Scanner)
|
||||
/// @@allow('all', auth().type == Admin)
|
||||
model History {
|
||||
id Int @id() @default(autoincrement())
|
||||
container_id Int
|
||||
container Container @relation(fields: [container_id], references: [id])
|
||||
checkoutAt DateTime @default(now())
|
||||
}
|
||||
|
||||
/// @@allow('all', auth() == this)
|
||||
/// @@allow('all', auth().type == Admin)
|
||||
model User {
|
||||
|
||||
+5
-4
@@ -11,10 +11,11 @@ async function seed() {
|
||||
const user = await pr.user.findUnique({where: {username: "kenneth"}}) || undefined;
|
||||
|
||||
// Delete all sessions
|
||||
pr.session.deleteMany({});
|
||||
await pr.session.deleteMany({});
|
||||
|
||||
const db = enhance(pr, {user});
|
||||
|
||||
await db.history.deleteMany({});
|
||||
await db.container.deleteMany({});
|
||||
await db.beer.deleteMany({});
|
||||
await db.wine.deleteMany({});
|
||||
@@ -843,7 +844,7 @@ function getContainers() : ContainerQuery[] {
|
||||
portions: 1,
|
||||
type: ContainerType.BeerBottle,
|
||||
volume: 330,
|
||||
inventory: 1
|
||||
inventory: 3
|
||||
},
|
||||
{
|
||||
barcode: "5411858100067",
|
||||
@@ -852,7 +853,7 @@ function getContainers() : ContainerQuery[] {
|
||||
portions: 1,
|
||||
type: ContainerType.BeerBottle,
|
||||
volume: 330,
|
||||
inventory: 1
|
||||
inventory: 2
|
||||
},
|
||||
{
|
||||
barcode: "5412186000098",
|
||||
@@ -864,4 +865,4 @@ function getContainers() : ContainerQuery[] {
|
||||
inventory: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,8 @@ model Container {
|
||||
|
||||
inventory Int @default(0)
|
||||
|
||||
checkouts History[]
|
||||
|
||||
@@allow('read', true)
|
||||
@@allow('update', auth().type == Scanner)
|
||||
@@allow('all', auth().type == Admin)
|
||||
@@ -179,6 +181,19 @@ model Country {
|
||||
@@allow('all', auth().type == Admin)
|
||||
}
|
||||
|
||||
model History {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
container_id Int
|
||||
container Container @relation(fields: [container_id], references: [id])
|
||||
|
||||
checkoutAt DateTime @default(now())
|
||||
|
||||
@@allow('read', true)
|
||||
@@allow('create', auth().type == Scanner)
|
||||
@@allow('all', auth().type == Admin)
|
||||
}
|
||||
|
||||
// Authentication
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
Reference in New Issue
Block a user