Files
beer-inventory/app/routes/admin/route.tsx
T
kennyboy55 734c52c898
Build dev docker image / release (push) Successful in 1s
Build dev docker image / build (push) Successful in 36s
Bugfix with usertype
2026-08-01 12:04:21 +02:00

254 lines
11 KiB
TypeScript

import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData, useLoaderData } from "@remix-run/react";
import { Alert, Button, Card, Col, Form as BootstrapForm, ListGroup, Row } from "react-bootstrap";
import { hashPassword } from "~/auth/validate";
import { enhance } from "~/utils/db.server";
export async function loader({ request }: LoaderFunctionArgs) {
const { dbe } = await enhance(request);
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
const [containersWithoutBarcode, checkoutsLastHour, latestCheckout, users] = await Promise.all([
dbe.container.count({ where: { barcode: null } }),
dbe.history.count({ where: { checkoutAt: { gte: oneHourAgo } } }),
dbe.history.findFirst({
orderBy: { checkoutAt: "desc" },
select: {
id: true,
checkoutAt: true,
container: {
select: {
drink: {
select: {
name: true,
},
},
},
},
},
}),
dbe.user.findMany({
select: { id: true, username: true, type: true },
orderBy: { username: "asc" },
}),
]);
return json({
containersWithoutBarcode,
checkoutsLastHour,
latestCheckout,
users,
});
}
export async function action({ request }: ActionFunctionArgs) {
const { dbe } = await enhance(request);
const form = await request.formData();
const intent = form.get("intent")?.toString();
if (intent === "undo-last-checkout") {
const checkoutId = Number(form.get("checkoutId"));
if (!checkoutId) {
return json({ error: "No checkout selected." });
}
const checkout = await dbe.history.findUnique({
where: { id: checkoutId },
select: { container_id: true },
});
if (checkout) {
await dbe.container.update({
where: { id: checkout.container_id },
data: { inventory: { increment: 1 } },
});
await dbe.history.delete({ where: { id: checkoutId } });
}
return redirect("/admin");
}
if (intent === "add-user") {
const username = form.get("username")?.toString().trim();
const password = form.get("password")?.toString() ?? "";
const type = form.get("type")?.toString();
if (!username || !password) {
return json({ error: "Username and password are required." });
}
const normalizedType = (type === "Admin" || type === "Scanner") ? type : "User";
try {
await dbe.user.create({
data: {
username,
password: hashPassword(password),
type: normalizedType,
},
});
return json({ success: "User added" });
} catch {
return json({ error: "That username is already in use." });
}
}
if (intent === "set-password") {
const userId = Number(form.get("userId"));
const password = form.get("password")?.toString() ?? "";
if (!userId || !password) {
return json({ error: "A user and a password are required." });
}
const existingUser = await dbe.user.findUnique({ where: { id: userId }, select: { id: true } });
if (!existingUser) {
return json({ error: "User not found." });
}
await dbe.user.update({
where: { id: userId },
data: { password: hashPassword(password) },
});
return json({ success: "Password set" });
}
return json({ error: "Unknown action." });
}
export default function AdminRoute() {
const loaderData = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
return (
<div>
<h1>Admin</h1>
<h2 className="mt-4">Dashboard</h2>
<Row className="g-3">
<Col md={4}>
<Card className="h-100">
<Card.Body>
<Card.Title>Containers without barcode</Card.Title>
<Card.Text className="display-6">{loaderData.containersWithoutBarcode}</Card.Text>
</Card.Body>
</Card>
</Col>
<Col md={4}>
<Card className="h-100">
<Card.Body>
<Card.Title>Checkouts in the last hour</Card.Title>
<Card.Text className="display-6">{loaderData.checkoutsLastHour}</Card.Text>
</Card.Body>
</Card>
</Col>
<Col md={4}>
<Card className="h-100">
<Card.Body>
<Card.Title>Last checked out</Card.Title>
<Card.Text>
{loaderData.latestCheckout?.container.drink.name ?? "No recent checkouts"}
</Card.Text>
{loaderData.latestCheckout ? (
<Form method="post">
<input type="hidden" name="intent" value="undo-last-checkout" />
<input type="hidden" name="checkoutId" value={loaderData.latestCheckout.id} />
<Button type="submit" variant="outline-danger">Undo</Button>
</Form>
) : null}
</Card.Body>
</Card>
</Col>
</Row>
<h2 className="mt-4">Quick actions</h2>
<ListGroup>
<ListGroup.Item><Link to="/admin/new">Add new items</Link></ListGroup.Item>
<ListGroup.Item><Link to="/admin/edit">Edit or remove items</Link></ListGroup.Item>
<ListGroup.Item><Link to="/admin/manage/inventory">Change inventory numbers</Link></ListGroup.Item>
<ListGroup.Item><Link to="/admin/manage/undo-checkout">Undo checkout</Link></ListGroup.Item>
</ListGroup>
<h2 className="mt-4">Reports</h2>
<ListGroup>
<ListGroup.Item><Link to="/admin/reports">Reports</Link></ListGroup.Item>
</ListGroup>
<h2 className="mt-4">User management</h2>
{actionData?.error ? <Alert variant="danger" className="mb-3">{actionData.error}</Alert> : null}
{actionData?.success ? <Alert variant="success" className="mb-3">{actionData.success}</Alert> : null}
<Row className="g-3 mb-4">
<Col lg={6}>
<Card>
<Card.Body>
<Card.Title>Add user</Card.Title>
<Form method="post">
<input type="hidden" name="intent" value="add-user" />
<BootstrapForm.Group className="mb-3">
<BootstrapForm.Label>Username</BootstrapForm.Label>
<BootstrapForm.Control name="username" required />
</BootstrapForm.Group>
<BootstrapForm.Group className="mb-3">
<BootstrapForm.Label>Password</BootstrapForm.Label>
<BootstrapForm.Control type="password" name="password" required />
</BootstrapForm.Group>
<BootstrapForm.Group className="mb-3">
<BootstrapForm.Label>Role</BootstrapForm.Label>
<BootstrapForm.Select name="type" defaultValue="User">
<option value="User">User</option>
<option value="Scanner">Scanner</option>
<option value="Admin">Admin</option>
</BootstrapForm.Select>
</BootstrapForm.Group>
<Button type="submit">Create user</Button>
</Form>
</Card.Body>
</Card>
</Col>
<Col lg={6}>
<Card>
<Card.Body>
<Card.Title>Set password</Card.Title>
<Form method="post">
<input type="hidden" name="intent" value="set-password" />
<BootstrapForm.Group className="mb-3">
<BootstrapForm.Label>User</BootstrapForm.Label>
<BootstrapForm.Select name="userId" defaultValue={loaderData.users[0]?.id ?? ""}>
{loaderData.users.map((user) => (
<option key={user.id} value={user.id}>{user.username} ({user.type})</option>
))}
</BootstrapForm.Select>
</BootstrapForm.Group>
<BootstrapForm.Group className="mb-3">
<BootstrapForm.Label>New password</BootstrapForm.Label>
<BootstrapForm.Control type="password" name="password" required />
</BootstrapForm.Group>
<Button type="submit" variant="outline-primary">Update password</Button>
</Form>
</Card.Body>
</Card>
</Col>
</Row>
<Card className="mb-4">
<Card.Body>
<Card.Title>Existing users</Card.Title>
<ListGroup variant="flush">
{loaderData.users.map((user) => (
<ListGroup.Item key={user.id} className="d-flex justify-content-between align-items-center">
<span>{user.username}</span>
<span className="text-muted">{user.type}</span>
</ListGroup.Item>
))}
</ListGroup>
</Card.Body>
</Card>
</div>
);
}