Files
beer-inventory/app/routes/scan/route.tsx
T
kennyboy55 c816af19a5
Build dev docker image / build (push) Successful in 43s
Build dev docker image / release (push) Successful in 1s
AI Changes: new scan page, add inventory filter
2026-08-03 12:48:09 +02:00

192 lines
6.1 KiB
TypeScript

import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { useActionData, useLoaderData } from "@remix-run/react";
import { useEffect, useRef, useState } from "react";
import { Col, Container, Form, Row, Table } from "react-bootstrap";
import timeAgo from "~/utils/datetime";
import { enhance } from "~/utils/db.server";
export async function loader({
request,
}: LoaderFunctionArgs) {
const { dbe, session } = await enhance(request);
if(!session.has("user_id")){
return redirect("/");
}
const history = await dbe.history.findMany({select: {id: true, checkoutAt: true, inventoryAfter: true, container: {select: {drink: true}}}, take: 10, orderBy: {checkoutAt: "desc"}});
return json({history});
}
export async function action({
request,
}: ActionFunctionArgs){
const { dbe } = await enhance(request);
const form = await request.formData();
const barcode = form.get("barcode")?.toString() || "";
const container = await dbe.container.findUnique({
select: {
id: true,
inventory: true,
drink: { select: { name: true, image: true } },
},
where: { barcode: barcode },
});
// No drink found for barcode, do nothing
if(!container){
return redirect("/scan/link/" + barcode);
}
// No inventory, do nothing
if(container.inventory <= 0){
return json({error: "No inventory!"});
}
// Update inventory
const newInventory = Math.max(container.inventory - 1, 0);
await dbe.container.update({data: {inventory: newInventory}, where: {id: container.id}});
// Log entry
await dbe.history.create({data: {container: {connect: {id: container.id}}, inventoryAfter: newInventory}});
return json({
error: "",
lastScanned: {
name: container.drink.name,
image: container.drink.image ?? null,
},
});
};
export default function ScanRoute() {
const aData = useActionData<typeof action>();
const lData = useLoaderData<typeof loader>();
const [lastScanned, setLastScanned] = useState<{name:string;image:string|null} | null>(null);
const [showLastScanned, setShowLastScanned] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const hasError = aData?.error ? aData.error.length > 0 : false;
const focusInput = () => {
window.setTimeout(() => inputRef.current?.focus(), 0);
};
useEffect(() => {
focusInput();
}, []);
useEffect(() => {
focusInput();
}, [aData]);
useEffect(() => {
const handleBlur = () => {
focusInput();
};
const input = inputRef.current;
input?.addEventListener("blur", handleBlur);
return () => {
input?.removeEventListener("blur", handleBlur);
};
}, []);
useEffect(() => {
if (!aData?.lastScanned) {
return;
}
setLastScanned(aData.lastScanned);
setShowLastScanned(true);
const timer = window.setTimeout(() => {
setShowLastScanned(false);
}, 5000);
return () => window.clearTimeout(timer);
}, [aData?.lastScanned]);
return (
<Container fluid className="mt-3">
<Row>
<Col xs={12} md={6} className="mb-3">
<h3>History</h3>
<div style={{ maxHeight: '70vh', overflowY: 'auto' }}>
<Table striped size="sm">
<thead>
<tr>
<th>Drink</th>
<th>When</th>
<th>Amount left</th>
</tr>
</thead>
<tbody>
{lData.history.map((entry) => (
<tr key={entry.id}>
<td>{entry.container.drink.name}</td>
<td>{timeAgo(new Date(entry.checkoutAt))}</td>
<td>{entry.inventoryAfter}</td>
</tr>
))}
</tbody>
</Table>
</div>
</Col>
<Col xs={12} md={6}>
<h3>Scan Barcode</h3>
<Row className="mb-3">
<Col>
<Form method="post" action="/scan?index" noValidate>
<Form.Group>
<Form.Control
ref={inputRef}
type="text"
name="barcode"
isInvalid={hasError}
autoComplete="off"
spellCheck={false}
inputMode="text"
style={{ height: '3rem', fontSize: '1.8rem', textAlign: 'center' }}
/>
<Form.Control.Feedback type="invalid">
{aData?.error}
</Form.Control.Feedback>
</Form.Group>
</Form>
</Col>
</Row>
<Row>
<Col>
<div className="p-4 border rounded" style={{ minHeight: '50vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}>
{showLastScanned && lastScanned ? (
<>
<h1 style={{ fontSize: '2rem', marginBottom: '1rem' }}>{lastScanned.name}</h1>
{lastScanned.image ? (
<img
src={lastScanned.image}
alt={lastScanned.name}
style={{ width: '100%', maxHeight: '40vh', objectFit: 'contain' }}
/>
) : (
<div className="text-muted" style={{ fontSize: '1.5rem' }}>No image available</div>
)}
</>
) : (
<div className="text-muted" style={{ fontSize: '2rem', fontWeight: 600 }}>
<h1>Please scan your drink</h1>
</div>
)}
</div>
</Col>
</Row>
</Col>
</Row>
</Container>
);
}