AI Changes: scan page fix, filters improvements
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
Beer-inventory project for home use.
|
Beer-inventory project for home use.
|
||||||
|
|
||||||
|
The project has three aspects.
|
||||||
|
- The website, which people can visit to see the current inventory of drinks. They can see in which section a drink is stored.
|
||||||
|
- The Admin website, which is part of the main website but only to logged in admin users. Which allows updating drinks, inventory, and adding or editing data.
|
||||||
|
- The scanner, which is a raspberry pi with touchscreen display and a USB barcode scanner. This is next to the fridge, and people use it to scan a drink to remove it from the inventory.
|
||||||
|
|
||||||
Used technology:
|
Used technology:
|
||||||
- remix.run (now known as react router)
|
- remix.run (now known as react router)
|
||||||
- prisma
|
- prisma
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useSubmit } from '@remix-run/react';
|
import { useSubmit } from '@remix-run/react';
|
||||||
import { BeerStyle, Manufacturer, WineStyle } from '@zenstackhq/runtime/models';
|
import { BeerStyle, Manufacturer, WineStyle } from '@zenstackhq/runtime/models';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { Form, Col, Row } from 'react-bootstrap';
|
import { Button, Form, Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
interface Arguments {
|
interface Arguments {
|
||||||
beerStyles: BeerStyle[];
|
beerStyles: BeerStyle[];
|
||||||
@@ -20,11 +20,28 @@ function DrinkFilter(arg : Arguments) {
|
|||||||
const showSodaFilter = arg.searchParams.has("drink", "Soda");
|
const showSodaFilter = arg.searchParams.has("drink", "Soda");
|
||||||
const showCocktailFilter = arg.searchParams.has("drink", "Cocktail");
|
const showCocktailFilter = arg.searchParams.has("drink", "Cocktail");
|
||||||
|
|
||||||
const form = useRef(null);
|
const form = useRef<HTMLFormElement | null>(null);
|
||||||
|
const drinkTypes = ["Beer", "Wine", "Soda", "Cocktail"];
|
||||||
|
const [beerStyleExpanded, setBeerStyleExpanded] = useState(false);
|
||||||
|
const [wineStyleExpanded, setWineStyleExpanded] = useState(false);
|
||||||
|
|
||||||
const submitAfterRender = function(){
|
const toggleDrinkType = (drinkType: string) => {
|
||||||
setTimeout(() => submit(form.current), 10);
|
const params = new URLSearchParams(arg.searchParams.toString());
|
||||||
}
|
const values = params.getAll("drink");
|
||||||
|
|
||||||
|
if (values.includes(drinkType)) {
|
||||||
|
params.delete("drink");
|
||||||
|
values.filter((value) => value !== drinkType).forEach((value) => params.append("drink", value));
|
||||||
|
} else {
|
||||||
|
params.append("drink", drinkType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.get("abv") === "all") {
|
||||||
|
params.delete("abv");
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(params, { method: "get", replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
const handleFormChange = (event: React.ChangeEvent<HTMLFormElement>) => {
|
const handleFormChange = (event: React.ChangeEvent<HTMLFormElement>) => {
|
||||||
const formData = new FormData(event.currentTarget);
|
const formData = new FormData(event.currentTarget);
|
||||||
@@ -37,6 +54,8 @@ function DrinkFilter(arg : Arguments) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const abvSelection = arg.searchParams.get("abv") || "all";
|
const abvSelection = arg.searchParams.get("abv") || "all";
|
||||||
|
const selectedBeerStyleCount = arg.searchParams.getAll("beerstyle").length;
|
||||||
|
const selectedWineStyleCount = arg.searchParams.getAll("winestyle").length;
|
||||||
const abvOptions = [
|
const abvOptions = [
|
||||||
{ value: "all", label: "All" },
|
{ value: "all", label: "All" },
|
||||||
{ value: "alcohol-free", label: "Alcohol free" },
|
{ value: "alcohol-free", label: "Alcohol free" },
|
||||||
@@ -87,17 +106,24 @@ function DrinkFilter(arg : Arguments) {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group controlId="drink-type" className='bg-body-secondary rounded p-3 mt-3'>
|
<Form.Group controlId="drink-type" className='bg-body-secondary rounded p-3 mt-3'>
|
||||||
<Form.Label className='fw-bold'>Drink Type</Form.Label>
|
<Form.Label className='fw-bold'>Drink Type</Form.Label>
|
||||||
{ ["Beer", "Wine", "Soda", "Cocktail"].map(key => (
|
<div className='row row-cols-2 g-2'>
|
||||||
<Form.Check
|
{drinkTypes.map((key) => (
|
||||||
id={"drink-" + key}
|
<div className='col' key={key}>
|
||||||
type="checkbox"
|
<Button
|
||||||
label={key}
|
type="button"
|
||||||
name="drink"
|
variant={arg.searchParams.has("drink", key) ? "primary" : "outline-primary"}
|
||||||
value={key}
|
className='w-100 text-start'
|
||||||
defaultChecked={arg.searchParams.has("drink", key) ? true : false}
|
aria-pressed={arg.searchParams.has("drink", key)}
|
||||||
/>
|
onClick={() => toggleDrinkType(key)}
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{arg.searchParams.getAll("drink").map((drinkValue) => (
|
||||||
|
<input key={drinkValue} type="hidden" name="drink" value={drinkValue} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group controlId="drink-abv" className='bg-body-secondary rounded p-3 mt-3'>
|
<Form.Group controlId="drink-abv" className='bg-body-secondary rounded p-3 mt-3'>
|
||||||
<Form.Label className='fw-bold'>Alcohol Percentage</Form.Label>
|
<Form.Label className='fw-bold'>Alcohol Percentage</Form.Label>
|
||||||
@@ -119,34 +145,67 @@ function DrinkFilter(arg : Arguments) {
|
|||||||
{ showBeerFilter ? (
|
{ showBeerFilter ? (
|
||||||
<>
|
<>
|
||||||
<Form.Group controlId="beer-style" className='bg-body-secondary rounded p-3 mt-3'>
|
<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>
|
<div className='d-flex justify-content-between align-items-center'>
|
||||||
{ arg.beerStyles.map(style => (
|
<Form.Label className='fw-bold mb-0'>Beer style {selectedBeerStyleCount > 0 ? ( <span className="badge text-bg-dark">{selectedBeerStyleCount} selected</span>) : ''}</Form.Label>
|
||||||
<Form.Check
|
<Button
|
||||||
id={"beerstyle-" + style.name}
|
type='button'
|
||||||
type="checkbox"
|
variant='outline-secondary'
|
||||||
label={style.name}
|
size='sm'
|
||||||
name="beerstyle"
|
onClick={() => setBeerStyleExpanded((current) => !current)}
|
||||||
value={style.id}
|
>
|
||||||
defaultChecked={arg.searchParams.has("beerstyle", style.id.toString()) ? true : false}
|
{beerStyleExpanded ? 'Collapse' : 'Expand'}
|
||||||
/>
|
</Button>
|
||||||
))}
|
</div>
|
||||||
|
{beerStyleExpanded ? (
|
||||||
|
<div className='mt-3'>
|
||||||
|
{ arg.beerStyles.map(style => (
|
||||||
|
<Form.Check
|
||||||
|
key={style.id}
|
||||||
|
id={"beerstyle-" + style.name}
|
||||||
|
type="checkbox"
|
||||||
|
label={style.name}
|
||||||
|
name="beerstyle"
|
||||||
|
value={style.id}
|
||||||
|
defaultChecked={arg.searchParams.has("beerstyle", style.id.toString()) ? true : false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='text-muted mt-3'>Tap expand to show beer styles</div>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</>
|
</>
|
||||||
) : ""}
|
) : ""}
|
||||||
{ showWineFilter ? (
|
{ showWineFilter ? (
|
||||||
<Form.Group controlId="wine-style" className='bg-body-secondary rounded p-3 mt-3'>
|
<Form.Group controlId="wine-style" className='bg-body-secondary rounded p-3 mt-3'>
|
||||||
<Form.Label className='fw-bold'>Wine style</Form.Label>
|
<div className='d-flex justify-content-between align-items-center'>
|
||||||
{ arg.wineStyles.map(style => (
|
<Form.Label className='fw-bold mb-0'>Wine style {selectedWineStyleCount > 0 ? ( <span className="badge text-bg-dark">{selectedWineStyleCount} selected</span>) : ''}</Form.Label>
|
||||||
<Form.Check
|
<Button
|
||||||
id={"winestyle-" + style.name}
|
type='button'
|
||||||
type="checkbox"
|
variant='outline-secondary'
|
||||||
label={style.name}
|
size='sm'
|
||||||
name="winestyle"
|
onClick={() => setWineStyleExpanded((current) => !current)}
|
||||||
value={style.id}
|
>
|
||||||
defaultChecked={arg.searchParams.has("winestyle", style.id.toString()) ? true : false}
|
{wineStyleExpanded ? 'Collapse' : 'Expand'}
|
||||||
/>
|
</Button>
|
||||||
))}
|
</div>
|
||||||
</Form.Group>
|
{wineStyleExpanded ? (
|
||||||
|
<div className='mt-3'>
|
||||||
|
{ arg.wineStyles.map(style => (
|
||||||
|
<Form.Check
|
||||||
|
id={"winestyle-" + style.name}
|
||||||
|
type="checkbox"
|
||||||
|
label={style.name}
|
||||||
|
name="winestyle"
|
||||||
|
value={style.id}
|
||||||
|
defaultChecked={arg.searchParams.has("winestyle", style.id.toString()) ? true : false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='text-muted mt-3'>Tap expand to show wine styles</div>
|
||||||
|
)}
|
||||||
|
</Form.Group>
|
||||||
) : ""}
|
) : ""}
|
||||||
{ showSodaFilter ? (
|
{ showSodaFilter ? (
|
||||||
<Form.Group controlId="soda-carbonated" className='bg-body-secondary rounded p-3 mt-3'>
|
<Form.Group controlId="soda-carbonated" className='bg-body-secondary rounded p-3 mt-3'>
|
||||||
|
|||||||
+3
-3
@@ -83,7 +83,7 @@ export function ErrorBoundary() {
|
|||||||
|
|
||||||
const hourly = window.setInterval(() => {
|
const hourly = window.setInterval(() => {
|
||||||
tryRefresh();
|
tryRefresh();
|
||||||
}, 60 * 60 * 1000);
|
}, 15 * 60 * 1000); // Every 15 minutes
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("online", handleOnline);
|
window.removeEventListener("online", handleOnline);
|
||||||
@@ -120,7 +120,7 @@ export function ErrorBoundary() {
|
|||||||
>
|
>
|
||||||
Try refresh now
|
Try refresh now
|
||||||
</button>
|
</button>
|
||||||
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every hour.</div>
|
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every 15 minutes.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -159,7 +159,7 @@ export function ErrorBoundary() {
|
|||||||
>
|
>
|
||||||
Try refresh now
|
Try refresh now
|
||||||
</button>
|
</button>
|
||||||
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every hour.</div>
|
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every 15 minutes.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function InventoryLayout() {
|
|||||||
setCheckouts(newCheckouts);
|
setCheckouts(newCheckouts);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 5000);
|
}, 30000);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
});
|
});
|
||||||
|
|||||||
+16
-11
@@ -1,8 +1,8 @@
|
|||||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||||
import { json, redirect } from "@remix-run/node";
|
import { json, redirect } from "@remix-run/node";
|
||||||
import { useActionData, useLoaderData } from "@remix-run/react";
|
import { Form as RemixForm, useActionData, useLoaderData } from "@remix-run/react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Col, Container, Form, Row, Table } from "react-bootstrap";
|
import { Col, Container, Form as BootstrapForm, Row, Table } from "react-bootstrap";
|
||||||
import timeAgo from "~/utils/datetime";
|
import timeAgo from "~/utils/datetime";
|
||||||
|
|
||||||
import { enhance } from "~/utils/db.server";
|
import { enhance } from "~/utils/db.server";
|
||||||
@@ -16,7 +16,7 @@ export async function loader({
|
|||||||
return redirect("/");
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = await dbe.history.findMany({select: {id: true, checkoutAt: true, inventoryAfter: true, container: {select: {drink: true}}}, take: 10, orderBy: {checkoutAt: "desc"}});
|
const history = await dbe.history.findMany({select: {id: true, checkoutAt: true, inventoryAfter: true, container: {select: {drink: true}}}, take: 5, orderBy: {checkoutAt: "desc"}});
|
||||||
return json({history});
|
return json({history});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +116,10 @@ export default function ScanRoute() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
setLastScanned(aData.lastScanned);
|
setLastScanned(aData.lastScanned);
|
||||||
setShowLastScanned(true);
|
setShowLastScanned(true);
|
||||||
|
|
||||||
@@ -137,7 +141,7 @@ export default function ScanRoute() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Drink</th>
|
<th>Drink</th>
|
||||||
<th>When</th>
|
<th>When</th>
|
||||||
<th>Amount left</th>
|
<th># Left</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -158,14 +162,15 @@ export default function ScanRoute() {
|
|||||||
<Col>
|
<Col>
|
||||||
{/* Hidden fullscreen toggle button - triggered by clicking the h3 headers */}
|
{/* Hidden fullscreen toggle button - triggered by clicking the h3 headers */}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
ref={fullscreenBtnRef}
|
ref={fullscreenBtnRef}
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<Form method="post" action="/scan?index" noValidate>
|
<RemixForm method="post" action="/scan?index" noValidate>
|
||||||
<Form.Group>
|
<BootstrapForm.Group>
|
||||||
<Form.Control
|
<BootstrapForm.Control
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
name="barcode"
|
name="barcode"
|
||||||
@@ -175,11 +180,11 @@ export default function ScanRoute() {
|
|||||||
inputMode="text"
|
inputMode="text"
|
||||||
style={{ height: '3rem', fontSize: '1.8rem', textAlign: 'center' }}
|
style={{ height: '3rem', fontSize: '1.8rem', textAlign: 'center' }}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback type="invalid">
|
<BootstrapForm.Control.Feedback type="invalid">
|
||||||
{aData?.error}
|
{aData?.error}
|
||||||
</Form.Control.Feedback>
|
</BootstrapForm.Control.Feedback>
|
||||||
</Form.Group>
|
</BootstrapForm.Group>
|
||||||
</Form>
|
</RemixForm>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
Reference in New Issue
Block a user