Styling changes, suggestions delete as admin

This commit is contained in:
2024-06-03 19:55:00 +02:00
parent c70090e0d5
commit b121abf102
11 changed files with 232 additions and 79 deletions
+2
View File
@@ -5,6 +5,7 @@ import { LinkContainer } from 'react-router-bootstrap';
import { ContainerWithSection, DrinkComposite, containerTypeToString, isBeer, isDrinkWithContainersAndSection, isDrinkWithManufacturer, isDrinkWithStyle } from '~/models/types';
import { volume } from '~/utils/conversions';
import DrinkCard from './drink.card';
import { Link } from '@remix-run/react';
interface Arguments {
drink: DrinkComposite
@@ -172,6 +173,7 @@ function DrinkPage(arg:Arguments) {
<Col md={10}>
<h2>{manufacturer.name} ({manufacturer.country_id})</h2>
<p>{manufacturer.description}</p>
<LinkContainer to={{pathname: "/inventory", search: "?manufacturer=" + manufacturer.id}} ><Button variant='secondary'>All drinks from {manufacturer.name}</Button></LinkContainer>
</Col>
{manufacturer.image ? (
<Col md={2}>
+53 -26
View File
@@ -4,6 +4,7 @@ import { BeerStyle, Manufacturer, WineStyle } from '@zenstackhq/runtime/models';
import { useRef, useState } from 'react';
import { Form, Col, Row } from 'react-bootstrap';
import ReactSlider from 'react-slider'
import { ManufacturerWithDrinks } from '~/models/types';
interface Arguments {
beerStyles: BeerStyle[];
@@ -16,6 +17,7 @@ function DrinkFilter(arg : Arguments) {
const submit = useSubmit();
const [abv, setAbv] = useState({min: Number(arg.searchParams.get("min-abv") || 0), max: Number(arg.searchParams.get("max-abv") || 50)})
const [ibu, setIbu] = useState({min: Number(arg.searchParams.get("min-ibu") || 0), max: Number(arg.searchParams.get("max-ibu") || 150)})
const showBeerFilter = arg.searchParams.has("drink", "Beer");
const showWineFilter = arg.searchParams.has("drink", "Wine");
@@ -28,7 +30,7 @@ function DrinkFilter(arg : Arguments) {
setTimeout(() => submit(form.current), 10);
}
const slider = () =>
const sliderAbv = () =>
(<div>
<ReactSlider
className="form-range"
@@ -47,14 +49,33 @@ function DrinkFilter(arg : Arguments) {
</div>);
const sliderIbu = () =>
(<div>
<ReactSlider
className="form-range"
thumbClassName="bg-primary text-light rounded dummy"
trackClassName="bg-secondary h-100 rounded dummy"
min={0}
max={150}
defaultValue={[ibu.min, ibu.max]}
renderThumb={(props, state) => <div {...props}>{state.valueNow}</div>}
pearling
minDistance={2}
onAfterChange={(number, index) => {setIbu({min: number[0], max: number[1]}); submitAfterRender()}}
/>
<input type="hidden" name="min-ibu" value={ibu.min} />
<input type="hidden" name="max-ibu" value={ibu.max} />
</div>);
const sortKey = arg.searchParams.get("sort") || "name";
return (
<Form noValidate method="GET" ref={form} onChange={(event) => {submit(event.currentTarget); }}>
<Form noValidate method="GET" ref={form} className='pe-2' onChange={(event) => {submit(event.currentTarget); }}>
<Row>
<Col xs={6} md={12}>
<Form.Group controlId="sort" className='mb-2'>
<Form.Label>Sort</Form.Label>
<Col>
<Form.Group controlId="sort" className='bg-body-secondary rounded p-3'>
<Form.Label className='fw-bold'>Sort</Form.Label>
{ [
["name", "Name"],
["popular", "Popularity"],
@@ -74,8 +95,8 @@ function DrinkFilter(arg : Arguments) {
))}
</Form.Group>
<Form.Group controlId="drink-type" className='mb-2'>
<Form.Label>Drink type</Form.Label>
<Form.Group controlId="drink-type" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold'>Drink type</Form.Label>
{ ["Beer", "Wine", "Soda", "Cocktail"].map(key => (
<Form.Check
id={"drink-" + key}
@@ -88,8 +109,8 @@ function DrinkFilter(arg : Arguments) {
))}
</Form.Group>
<Form.Group controlId="drink-modifiers" className='mb-2'>
<Form.Label>Drink</Form.Label>
<Form.Group controlId="drink-modifiers" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold'>Drink</Form.Label>
{ ["Sugar-free", "Gluten-free", "Organic"].map(key => (
<Form.Check
id={"drinkmod-" + key}
@@ -100,11 +121,13 @@ function DrinkFilter(arg : Arguments) {
defaultChecked={arg.searchParams.has(key.toLowerCase()) ? true : false}
/>
))}
<Form.Label>ABV</Form.Label>
{slider()}
</Form.Group>
<Form.Group controlId="manufacturer" className='mb-2'>
<Form.Label>Manufacturer</Form.Label>
<Form.Group controlId="drink-abv" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold'>ABV</Form.Label>
{sliderAbv()}
</Form.Group>
{/* <Form.Group controlId="manufacturer" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold'>Manufacturer</Form.Label>
{ arg.manufacturers.map(manufacturer => (
<Form.Check
id={"manufacturer-" + manufacturer.name}
@@ -115,12 +138,11 @@ function DrinkFilter(arg : Arguments) {
defaultChecked={arg.searchParams.has("manufacturer", manufacturer.id.toString()) ? true : false}
/>
))}
</Form.Group>
</Col>
<Col>
</Form.Group> */}
{ showBeerFilter ? (
<Form.Group controlId="beer-style" className='mb-2'>
<Form.Label>Beer style</Form.Label>
<>
<Form.Group controlId="beer-style" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold mt-3'>Beer style</Form.Label>
{ arg.beerStyles.map(style => (
<Form.Check
id={"beerstyle-" + style.name}
@@ -132,10 +154,15 @@ function DrinkFilter(arg : Arguments) {
/>
))}
</Form.Group>
<Form.Group controlId="beer-ibu" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold'>IBU</Form.Label>
{sliderIbu()}
</Form.Group>
</>
) : ""}
{ showWineFilter ? (
<Form.Group controlId="wine-style" className='mb-2'>
<Form.Label>Wine style</Form.Label>
<Form.Group controlId="wine-style" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold'>Wine style</Form.Label>
{ arg.wineStyles.map(style => (
<Form.Check
id={"winestyle-" + style.name}
@@ -149,8 +176,8 @@ function DrinkFilter(arg : Arguments) {
</Form.Group>
) : ""}
{ showSodaFilter ? (
<Form.Group controlId="soda-carbonated" className='mb-2'>
<Form.Label>Soda</Form.Label>
<Form.Group controlId="soda-carbonated" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold'>Soda</Form.Label>
<Form.Check
type="radio"
label="Both"
@@ -175,8 +202,8 @@ function DrinkFilter(arg : Arguments) {
</Form.Group>
) : ""}
{ showCocktailFilter ? (
<Form.Group controlId="cocktail-mix" className='mb-2'>
<Form.Label>Cocktail</Form.Label>
<Form.Group controlId="cocktail-mix" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold'>Cocktail</Form.Label>
<Form.Check
type="checkbox"
label="Mix"
@@ -186,8 +213,8 @@ function DrinkFilter(arg : Arguments) {
/>
</Form.Group>
) : ""}
<Form.Group controlId="inventory" className='mb-2'>
<Form.Label>No Inventory</Form.Label>
<Form.Group controlId="inventory" className='bg-body-secondary rounded p-3 mt-3'>
<Form.Label className='fw-bold'>No Inventory</Form.Label>
<Form.Check
type="checkbox"
label="Show"
+12 -1
View File
@@ -47,13 +47,24 @@ export async function findDrinksFromSearch(searchParams: URLSearchParams) : Prom
// Beers
if(searchParams.has("drink", "Beer") || noDrinkSelected){
let where = Object.assign({}, whereBase, {style: {}});
let where = Object.assign({}, whereBase, {style: {}, AND: <any>[]});
// Beer styles
if(searchParams.has("beerstyle")){
where.style = {id: {in: searchParams.getAll("beerstyle").map(Number)}};
}
let minibu = 0;
let maxibu = 150;
if(searchParams.has("min-ibu")){
minibu = Number(searchParams.get("min-ibu"));
}
if(searchParams.has("max-ibu")){
maxibu = Number(searchParams.get("max-ibu"));
}
where.AND.push({ibu: {gte: minibu}});
where.AND.push({ibu: {lte: maxibu}});
result = result.concat(await dbe.beer.findMany({
include: {style: true, manufacturer: true, containers: {include: {section: true, checkouts: true}}},
where: where
+2 -2
View File
@@ -23,13 +23,13 @@ export async function findManufacturersFromSearch(searchParams: URLSearchParams)
if(searchParams.has("drink")){
result = result.concat(await dbe.manufacturer.findMany({
orderBy: {name: "asc"},
orderBy: {drinks: {_count: "desc"}},
where: {drinks: { some: { type: { in: searchParams.getAll("drink").map((str) => {return DrinkType[str as keyof typeof DrinkType]})}}}}
}));
}
else {
result = result.concat(await dbe.manufacturer.findMany({
orderBy: {name: "asc"},
orderBy: {drinks: {_count: "desc"}},
where: {NOT: {drinks: {none: {}}}}
}));
}
+2
View File
@@ -11,6 +11,8 @@ export type DrinkWithContainersAndSectionAndHistory = (Drink & { containers: Con
export type DrinkWithContainers = DrinkWithContainersAndSection | DrinkWithContainersAndHistory | DrinkWithContainersAndSectionAndHistory | (Drink & { containers: Container[]})
export type DrinkWithManufacturer = Drink & { manufacturer: Manufacturer}
export type ManufacturerWithDrinks = Manufacturer & {drinks: Drink[]}
export type BeerWithStyle = Beer & { style : BeerStyle }
export type WineWithStyle = Wine & { style : WineStyle }
@@ -0,0 +1,15 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/node";
import { enhance } from "~/utils/db.server";
export const loader = async ({
request,
params
} : LoaderFunctionArgs) => {
const { dbe } = await enhance(request);
const id = Number(params.id);
await dbe.suggestion.delete({where: {id: id}});
return redirect("/inventory/suggestions");
}
+17 -21
View File
@@ -1,7 +1,8 @@
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { enhance } from "@zenstackhq/runtime";
import { Accordion, Button, Col, Container, Row, useAccordionButton } from "react-bootstrap";
import { useState } from "react";
import { Accordion, Button, Col, Container, Offcanvas, Row, useAccordionButton } from "react-bootstrap";
import DrinkCard from "~/components/cards/drink.card";
import DrinkFilter from "~/components/filters/drink.filter";
import { findDrinksFromSearch } from "~/models/drinks.filter.server";
@@ -30,42 +31,37 @@ export default function BeersRoute() {
const [searchParams, setSearchParams] = useSearchParams();
function CustomToggle({ children, eventKey } : any) {
const decoratedOnClick = useAccordionButton(eventKey, () => {});
return (
<Button
variant="primary"
onClick={decoratedOnClick}
>
{children}
</Button>
);
}
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const numResults = loadData.drinkResults.length;
return (
<Container>
<Row className="mt-3">
<Col key="filtering" xs={12} md={4} xl={3} className="mb-3">
<Accordion>
<Col key="filtering" className="mb-3">
<div>
<CustomToggle eventKey="0">Sorting and Filtering</CustomToggle> <span className="float-end">{numResults} Results</span>
<Button variant="primary" onClick={handleShow}>Sorting and Filtering</Button> <span className="float-end">{numResults} Results</span>
</div>
<Accordion.Collapse eventKey="0">
<div className="h-100 p-3 mt-3 text-bg-dark rounded-3">
<Offcanvas show={show} onHide={handleClose}>
<Offcanvas.Header closeButton>
<Offcanvas.Title>Sorting and filtering <br/> <span className="fs-6">{numResults} Results</span></Offcanvas.Title>
</Offcanvas.Header>
<Offcanvas.Body>
<DrinkFilter
beerStyles={loadData.beerStyles}
wineStyles={loadData.wineStyles}
manufacturers={loadData.manufacturers}
searchParams={searchParams} >
</DrinkFilter>
</div>
</Accordion.Collapse>
</Accordion>
</Offcanvas.Body>
</Offcanvas>
</Col>
</Row>
<Row>
<Col key="results">
<Row xs={2} md={3} lg={4} className="g-4">
{loadData.drinkResults.map((drink) => (
+101 -19
View File
@@ -89,29 +89,111 @@ export default function StatsRoute() {
return (
<Container>
<Row className="mt-3">
<Col key="stats-total" className="mb-3">
<Col key="stats-total" className="mb-3" xs={12} sm={6}>
<h2>Total inventory</h2>
Total drinks in inventory: {loadData.total.drinksInventory} <br />
Total beers in inventory: {loadData.total.beersInventory} <br />
Total wines in inventory: {loadData.total.wineInventory} <br />
Total sodas in inventory: {loadData.total.sodaInventory} <br />
Total cocktails in inventory: {loadData.total.cocktailInventory} <br /><br />
Total pure alcohol in inventory: {loadData.total.totalLiterAlcohol} Liter <br />
Total price inventory: {loadData.total.totalPrice} <br />
Average alcohol percentage: {loadData.total.averageAlcoholPercentage}% <br />
<Table className="pe-5">
<thead>
<tr>
<th>Item</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td># Drinks</td>
<td>{loadData.total.drinksInventory}</td>
</tr>
<tr>
<td># Beers</td>
<td>{loadData.total.beersInventory}</td>
</tr>
<tr>
<td># Wines</td>
<td>{loadData.total.wineInventory}</td>
</tr>
<tr>
<td># Sodas</td>
<td>{loadData.total.sodaInventory}</td>
</tr>
<tr>
<td># Cocktails</td>
<td>{loadData.total.cocktailInventory}</td>
</tr>
</tbody>
<thead>
<tr>
<th>Statistic</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>Pure alcohol</td>
<td>{loadData.total.totalLiterAlcohol} Liter</td>
</tr>
<tr>
<td>Price</td>
<td>{loadData.total.totalPrice}</td>
</tr>
<tr>
<td>Average alcohol percentage</td>
<td>{loadData.total.averageAlcoholPercentage}%</td>
</tr>
</tbody>
</Table>
</Col>
<Col key="stats-24h" className="mb-3">
<h2>Last 24H checkouts</h2>
Drinks checked out: {loadData.statsLast24H.drinksInventory} <br />
Beer checked out: {loadData.statsLast24H.beersInventory} <br />
Wines checked out: {loadData.statsLast24H.wineInventory} <br />
Sodas checked out: {loadData.statsLast24H.sodaInventory} <br />
Cocktails checked out: {loadData.statsLast24H.cocktailInventory} <br /><br />
Pure alcohol checked out: {loadData.statsLast24H.totalLiterAlcohol} Liter <br />
Price checked out: {loadData.statsLast24H.totalPrice} <br />
Average alcohol percentage: {loadData.statsLast24H.averageAlcoholPercentage}% <br />
<Table className="pe-5">
<thead>
<tr>
<th>Item</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td># Drinks</td>
<td>{loadData.statsLast24H.drinksInventory}</td>
</tr>
<tr>
<td># Beers</td>
<td>{loadData.statsLast24H.beersInventory}</td>
</tr>
<tr>
<td># Wines</td>
<td>{loadData.statsLast24H.wineInventory}</td>
</tr>
<tr>
<td># Sodas</td>
<td>{loadData.statsLast24H.sodaInventory}</td>
</tr>
<tr>
<td># Cocktails</td>
<td>{loadData.statsLast24H.cocktailInventory}</td>
</tr>
</tbody>
<thead>
<tr>
<th>Statistic</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>Pure alcohol</td>
<td>{loadData.statsLast24H.totalLiterAlcohol} Liter</td>
</tr>
<tr>
<td>Price</td>
<td>{loadData.statsLast24H.totalPrice}</td>
</tr>
<tr>
<td>Average alcohol percentage</td>
<td>{loadData.statsLast24H.averageAlcoholPercentage}%</td>
</tr>
</tbody>
</Table>
</Col>
</Row>
<Row className="mt-3">
+20 -4
View File
@@ -1,6 +1,7 @@
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Button, Col, Container, Form, Row, Table } from "react-bootstrap";
import { LinkContainer } from "react-router-bootstrap";
import timeAgo from "~/utils/datetime";
import { enhance } from "~/utils/db.server";
@@ -33,11 +34,14 @@ export const action = async ({
};
export const loader = async ({request} : LoaderFunctionArgs) => {
const { dbe } = await enhance(request);
const { dbe, session } = await enhance(request);
const suggestions = await dbe.suggestion.findMany({take: 15, orderBy: {createdAt: "desc"}});
return json({suggestions});
let user = session.get("user");
let isAdmin = user?.type == "Admin";
return json({suggestions, isAdmin});
};
export default function SuggestionsRoute() {
@@ -61,7 +65,7 @@ export default function SuggestionsRoute() {
/>
</Form.Group>
<Form.Group>
<Form.Group className="mt-3">
<Form.Label>
Suggestion*:
</Form.Label>
@@ -73,7 +77,7 @@ export default function SuggestionsRoute() {
/>
</Form.Group>
<Button variant="primary" className="mt-1" type="submit">
<Button variant="primary" className="mt-3" type="submit">
Submit
</Button>
</Form>
@@ -89,6 +93,9 @@ export default function SuggestionsRoute() {
<th>Name</th>
<th>Date</th>
<th>Suggestions</th>
{ loadData.isAdmin ? (
<th>Delete</th>
) : ""}
</tr>
</thead>
<tbody>
@@ -97,6 +104,15 @@ export default function SuggestionsRoute() {
<td>{entry.name ? (<>{entry.name}</>) : ( <><i>No name</i></> ) }</td>
<td>{timeAgo(new Date(entry.createdAt))}</td>
<td>{entry.content}</td>
{ loadData.isAdmin ? (
<td>
<LinkContainer to={"/admin/remove/suggestion/" + entry.id}>
<Button variant="danger">
Remove
</Button>
</LinkContainer>
</td>
) : ""}
</tr>
))}
</tbody>
+6 -6
View File
@@ -10,26 +10,26 @@ export default function timeAgo(previous: Date) {
const elapsed : number = (now.getTime() - previous.getTime());
if (elapsed < msPerMinute) {
return Math.round(elapsed/1000) + ' seconds ago';
return Math.round(elapsed/1000) + ' seconds ago';
}
else if (elapsed < msPerHour) {
return Math.round(elapsed/msPerMinute) + ' minutes ago';
return Math.round(elapsed/msPerMinute) + ' minutes ago';
}
else if (elapsed < msPerDay ) {
return Math.round(elapsed/msPerHour ) + ' hours ago';
return Math.round(elapsed/msPerHour ) + ' hours ago';
}
else if (elapsed < msPerMonth) {
return 'approximately ' + Math.round(elapsed/msPerDay) + ' days ago';
return Math.round(elapsed/msPerDay) + ' days ago';
}
else if (elapsed < msPerYear) {
return 'approximately ' + Math.round(elapsed/msPerMonth) + ' months ago';
return Math.round(elapsed/msPerMonth) + ' months ago';
}
else {
return 'approximately ' + Math.round(elapsed/msPerYear ) + ' years ago';
return Math.round(elapsed/msPerYear ) + ' years ago';
}
}
+2
View File
@@ -104,6 +104,8 @@ export default defineConfig({
route("edit/container/:id", "routes/admin/edit/container.$id.tsx");
route("remove/container/:id", "routes/admin/edit/remove/container.$id.tsx");
route("remove/suggestion/:id", "routes/admin/edit/remove/suggestion.$id.tsx");
route("manage/inventory", "routes/admin/manage/inventory.tsx");
route("manage/checkout", "routes/admin/manage/checkout.tsx");
});