();
+
+ return (
+
+
Update inventory
+
+ Checkouts
+
+ { lData.checkouts.map((chkt) => (
+
+ ))}
+
+
+
+
+
+ );
+ }
\ No newline at end of file
diff --git a/app/routes/admin/edit/inventory.tsx b/app/routes/admin/edit/inventory.tsx
new file mode 100644
index 0000000..dbc3637
--- /dev/null
+++ b/app/routes/admin/edit/inventory.tsx
@@ -0,0 +1,59 @@
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { Button, Form } from "react-bootstrap";
+import { containerTypeToString } from "~/models/types";
+
+import { enhance } from "~/utils/db.server";
+
+export async function loader({
+ params,
+ request
+ }: LoaderFunctionArgs) {
+ const { dbe } = await enhance(request);
+
+ const containers = await dbe.container.findMany({select: {id: true, drink: true, type: true, inventory: true}, orderBy: {drink: {name: "asc"}}});
+
+ return json({ containers });
+ };
+
+export async function action({
+ request,
+}: ActionFunctionArgs) {
+ const { dbe } = await enhance(request);
+
+ const form = await request.formData();
+ const containerid = Number(form.get("id"));
+ let inventory = Number(form.get("inventory"));
+
+ if(inventory <= 0) inventory = 0;
+
+ await dbe.container.update({data: { inventory: inventory}, where: {id: containerid}});
+
+ return null;
+};
+
+export default function EditInventoryRoute() {
+ const lData = useLoaderData();
+
+ return (
+
+
Update inventory
+
+ Drink container
+
+ { lData.containers.map((container) => (
+
+ ))}
+
+
+
+ New inventory amount
+
+
+
+
+
+ );
+ }
\ No newline at end of file
diff --git a/app/routes/admin/layout.tsx b/app/routes/admin/layout.tsx
new file mode 100644
index 0000000..ca32d82
--- /dev/null
+++ b/app/routes/admin/layout.tsx
@@ -0,0 +1,51 @@
+import { UserType } from "@prisma/client";
+import { LoaderFunctionArgs, redirect } from "@remix-run/node";
+import { Outlet, json, useLoaderData } from "@remix-run/react";
+import { Container, Row } from "react-bootstrap";
+import { getSession } from "~/auth/session";
+import Header, { HeaderData } from "~/components/header";
+
+export async function loader({
+ request,
+ }: LoaderFunctionArgs) {
+ const session = await getSession(
+ request.headers.get("Cookie")
+ );
+
+ if(!session.has("user_id")){
+ return redirect("/");
+ }
+
+ let user = session.get("user");
+ if(user.type != UserType.Admin){
+ return redirect("/");
+ }
+
+ let data : HeaderData = {loggedin: false};
+
+ if(session.has("user_id")){
+ data.user = session.get("user");
+ data.loggedin = true;
+ }
+
+ return json({data});
+ }
+
+export default function InventoryLayout() {
+ let data = useLoaderData();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+ }
\ No newline at end of file
diff --git a/app/routes/new/beer.tsx b/app/routes/admin/new/beer.tsx
similarity index 98%
rename from app/routes/new/beer.tsx
rename to app/routes/admin/new/beer.tsx
index 5df3e53..a5f3a4f 100644
--- a/app/routes/new/beer.tsx
+++ b/app/routes/admin/new/beer.tsx
@@ -57,7 +57,7 @@ export const action = async ({
}
const beer = await dbe.beer.create({ data: fields });
- return redirect(`/beer/${beer.id}`);
+ return redirect(`/inventory/beer/${beer.id}`);
};
export default function NewBeerRoute() {
diff --git a/app/routes/admin/route.tsx b/app/routes/admin/route.tsx
new file mode 100644
index 0000000..6fd3ca7
--- /dev/null
+++ b/app/routes/admin/route.tsx
@@ -0,0 +1,11 @@
+import { Link } from "@remix-run/react";
+import { ListGroup } from "react-bootstrap";
+
+export default function AdminRoute() {
+ return (
+
+ Change inventory numbers
+ Undo checkout
+
+ );
+ }
\ No newline at end of file
diff --git a/app/routes/beer.$beerslug.tsx b/app/routes/inventory/beer.$beerslug.tsx
similarity index 96%
rename from app/routes/beer.$beerslug.tsx
rename to app/routes/inventory/beer.$beerslug.tsx
index 0c7c944..afc42d3 100644
--- a/app/routes/beer.$beerslug.tsx
+++ b/app/routes/inventory/beer.$beerslug.tsx
@@ -13,7 +13,6 @@ import DrinkPage from "~/components/cards/drink.page";
import { db } from "~/utils/db.server";
import { enhance } from "@zenstackhq/runtime";
import { findIdFromSlug } from "~/models/drinks.server";
-import { Container, Row, Col } from "react-bootstrap";
export const loader = async ({
params,
diff --git a/app/routes/inventory/layout.tsx b/app/routes/inventory/layout.tsx
new file mode 100644
index 0000000..d07e903
--- /dev/null
+++ b/app/routes/inventory/layout.tsx
@@ -0,0 +1,36 @@
+import { LoaderFunctionArgs } from "@remix-run/node";
+import { Outlet, json, useLoaderData } from "@remix-run/react";
+import { getSession } from "~/auth/session";
+import Header, { HeaderData } from "~/components/header";
+
+export async function loader({
+ request,
+ }: LoaderFunctionArgs) {
+ const session = await getSession(
+ request.headers.get("Cookie")
+ );
+
+ let data : HeaderData = {loggedin: false};
+
+ if(session.has("user_id")){
+ data.user = session.get("user");
+ data.loggedin = true;
+ }
+
+ return json({data});
+ }
+
+export default function InventoryLayout() {
+ let data = useLoaderData();
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+ }
\ No newline at end of file
diff --git a/app/routes/inventory/route.tsx b/app/routes/inventory/route.tsx
new file mode 100644
index 0000000..7ae55a4
--- /dev/null
+++ b/app/routes/inventory/route.tsx
@@ -0,0 +1,81 @@
+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 DrinkCard from "~/components/cards/drink.card";
+import DrinkFilter from "~/components/filters/drink.filter";
+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";
+
+export const loader = async ({request} : LoaderFunctionArgs) => {
+ const dbe = enhance(db);
+
+ const url = new URL(request.url);
+
+ const beerStyles = await dbe.beerStyle.findMany();
+ const wineStyles = await dbe.wineStyle.findMany();
+ const manufacturers = await findManufacturersFromSearch(url.searchParams);
+
+ const drinkResultsUnsorted = await findDrinksFromSearch(url.searchParams);
+ const drinkResults = sortDrinksFromSearch(url.searchParams, drinkResultsUnsorted);
+
+ return json({beerStyles, wineStyles, manufacturers, drinkResults});
+};
+
+export default function BeersRoute() {
+ const loadData = useLoaderData();
+
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ function CustomToggle({ children, eventKey } : any) {
+ const decoratedOnClick = useAccordionButton(eventKey, () =>
+ console.log('totally custom!'),
+ );
+
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+ Sorting and Filtering
+
+
+
+
+
+
+
+
+
+
+
+ {loadData.drinkResults.map((drink) => (
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
+
diff --git a/app/routes/soda.$sodaslug.tsx b/app/routes/inventory/soda.$sodaslug.tsx
similarity index 100%
rename from app/routes/soda.$sodaslug.tsx
rename to app/routes/inventory/soda.$sodaslug.tsx
diff --git a/app/routes/wine.$wineslug.tsx b/app/routes/inventory/wine.$wineslug.tsx
similarity index 100%
rename from app/routes/wine.$wineslug.tsx
rename to app/routes/inventory/wine.$wineslug.tsx
diff --git a/app/routes/login.tsx b/app/routes/login.tsx
index ffda2cf..54579e1 100644
--- a/app/routes/login.tsx
+++ b/app/routes/login.tsx
@@ -3,7 +3,7 @@ import type {
LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
- import { useLoaderData } from "@remix-run/react";
+ import { useActionData, useLoaderData } from "@remix-run/react";
import { Button, Col, Container, Form, InputGroup, Row } from "react-bootstrap";
import { getSession, commitSession } from "~/auth/session";
@@ -16,14 +16,12 @@ import { validateCredentials } from "~/auth/validate";
request.headers.get("Cookie")
);
- if (session.has("id")) {
+ if (session.has("user_id")) {
// Redirect to the home page if they are already signed in.
return redirect("/");
}
- const data = { error: session.get("error") };
-
- return json(data);
+ return null;
}
export async function action({
@@ -42,13 +40,11 @@ import { validateCredentials } from "~/auth/validate";
);
if (id == null) {
- session.flash("error", "Invalid username/password");
-
// Redirect back to the login page with errors.
- return redirect("/login");
+ return json({error: "Invalid credentials"});
}
- session.set("id", id);
+ session.set("user_id", id);
// Login succeeded, send them to the home page.
return redirect("/", {
@@ -59,7 +55,7 @@ import { validateCredentials } from "~/auth/validate";
}
export default function Login() {
- const { error } = useLoaderData();
+ const { error } = useActionData() || {error: undefined};
return (
diff --git a/app/routes/logout.tsx b/app/routes/logout.tsx
index 0cf6b90..671e9ef 100644
--- a/app/routes/logout.tsx
+++ b/app/routes/logout.tsx
@@ -12,10 +12,13 @@ import type {
request.headers.get("Cookie")
);
- if (session.has("id")) {
+ if (session.has("user_id")) {
// Logout
- await destroySession(session);
- return redirect("/login");
+ return redirect("/login", {
+ headers: {
+ "Set-Cookie": await destroySession(session),
+ },
+ });
}
return redirect("/");
diff --git a/app/routes/scan/layout.tsx b/app/routes/scan/layout.tsx
new file mode 100644
index 0000000..a343359
--- /dev/null
+++ b/app/routes/scan/layout.tsx
@@ -0,0 +1,30 @@
+import { LoaderFunctionArgs } from "@remix-run/node";
+import { Outlet, json, redirect, useLoaderData } from "@remix-run/react";
+import { enhance } from "~/utils/db.server";
+
+export async function loader({
+ request,
+}: LoaderFunctionArgs) {
+ const { session } = await enhance(request);
+
+ if(!session.has("user_id")){
+ return redirect("/");
+ }
+
+ return null;
+}
+
+export default function InventoryLayout() {
+ useLoaderData();
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+ }
\ No newline at end of file
diff --git a/app/routes/scan.tsx b/app/routes/scan/route.tsx
similarity index 84%
rename from app/routes/scan.tsx
rename to app/routes/scan/route.tsx
index 6d7a2a6..12125c4 100644
--- a/app/routes/scan.tsx
+++ b/app/routes/scan/route.tsx
@@ -1,38 +1,29 @@
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, Table } from "react-bootstrap";
import { getSession } from "~/auth/session";
import timeAgo from "~/utils/datetime";
-import { db } from "~/utils/db.server";
+import { db, enhance } from "~/utils/db.server";
export async function loader({
request,
}: LoaderFunctionArgs) {
- const session = await getSession(
- request.headers.get("Cookie")
- );
+ const { dbe, session } = await enhance(request);
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 ({
+export async function action({
request,
-}: ActionFunctionArgs) => {
- const session = await getSession(
- request.headers.get("Cookie")
- );
-
- const dbe = session.has("userid") ? enhance(db) : enhance(db, {user: session.get("user")});
+}: ActionFunctionArgs){
+ const { dbe } = await enhance(request);
const form = await request.formData();
const barcode = form.get("barcode")?.toString() || "";
@@ -71,7 +62,7 @@ export default function ScanRoute() {
-
Scan barcode:
diff --git a/app/utils/db.server.ts b/app/utils/db.server.ts
index 8736437..ebdc6e0 100644
--- a/app/utils/db.server.ts
+++ b/app/utils/db.server.ts
@@ -1,6 +1,8 @@
import { PrismaClient } from "@prisma/client";
import { singleton } from "./singleton.server";
+import { getSession } from "~/auth/session";
+import { enhance as znenhance } from "@zenstackhq/runtime";
// Hard-code a unique key, so we can look up the client when this module gets re-imported
export const db = singleton(
@@ -8,3 +10,13 @@ export const db = singleton(
() => new PrismaClient()
);
+export async function enhance(
+ request : Request
+){
+ const session = await getSession(
+ request.headers.get("Cookie")
+ );
+
+ const dbe = session.has("user_id") ? znenhance(db, {user: session.get("user")}) : znenhance(db);
+ return {dbe, session};
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index ffc3d36..72c4cd3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"@remix-run/node": "^2.9.1",
"@remix-run/react": "^2.9.1",
"@remix-run/serve": "^2.9.1",
- "@zenstackhq/runtime": "2.0.1",
+ "@zenstackhq/runtime": "2.0.3",
"bootstrap": "^5.3.3",
"isbot": "^4.1.0",
"lodash": "^4.17.21",
@@ -41,7 +41,7 @@
"typescript": "^5.4.5",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1",
- "zenstack": "2.0.1"
+ "zenstack": "2.0.3"
},
"engines": {
"node": ">=18.0.0"
@@ -2759,18 +2759,18 @@
"integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw=="
},
"node_modules/@zenstackhq/language": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@zenstackhq/language/-/language-2.0.1.tgz",
- "integrity": "sha512-PdnjVKndmXNn/vmdO4aKTwR26MJdyHO+Q8UOgmamIsA3Hp0nBsBy1BhvJPqFnPGCf7Pqwg5AD6pPvCmAg2i3Kw==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@zenstackhq/language/-/language-2.0.3.tgz",
+ "integrity": "sha512-q8cy+QukLeRCTTg2vqTeZZLnN2ghq4ZXxRmy/P6QA5lvJo3sPREQkPBkhFmXR/soVIbEL73HxWTMZrxJvSYYHA==",
"dev": true,
"dependencies": {
"langium": "1.3.1"
}
},
"node_modules/@zenstackhq/runtime": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-2.0.1.tgz",
- "integrity": "sha512-iRIEtYrElqHSvrEN9NF8IHeNUlBSb1yGch1U1unLtaK806I5meLw17k1kBbRE3CwKNhicrClpYkXBCpOZ8xBqg==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-2.0.3.tgz",
+ "integrity": "sha512-ER/DNU2Hhb816D6kZEHqyMy5auwr8s31L+aNfKm4phgj52FYwj3eVFutx5ERIJSsx08pFSrH8E9YP4C/7rT9Mg==",
"dependencies": {
"bcryptjs": "^2.4.3",
"buffer": "^6.0.3",
@@ -2818,15 +2818,15 @@
}
},
"node_modules/@zenstackhq/sdk": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@zenstackhq/sdk/-/sdk-2.0.1.tgz",
- "integrity": "sha512-FoBGNOeYGVP31uqmSUzqWJ3N9O6/rkRrVOJn590OHF5PtShJU3sLFonzSU3IBdrv2xpnMTu2iH/WSgo7+LSLxQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@zenstackhq/sdk/-/sdk-2.0.3.tgz",
+ "integrity": "sha512-YL7mux6Yecd1EMLS3RbmyhcmsWtFklF170ZHI7KMmz8sdiJ+rmjFnjy5XzW1TwnQ7swYbjhgQQfY9OPuKLY/MQ==",
"dev": true,
"dependencies": {
"@prisma/generator-helper": "5.7.0",
"@prisma/internals": "5.7.0",
- "@zenstackhq/language": "2.0.1",
- "@zenstackhq/runtime": "2.0.1",
+ "@zenstackhq/language": "2.0.3",
+ "@zenstackhq/runtime": "2.0.3",
"langium": "1.3.1",
"lower-case-first": "^2.0.2",
"semver": "^7.5.2",
@@ -13329,16 +13329,16 @@
}
},
"node_modules/zenstack": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-2.0.1.tgz",
- "integrity": "sha512-IDTXX+aTYxow//3w+w11buMSDPbxm1zMzp5/EMRurmrzPrJDkCByhow36LSfzoaXlcvUOF1JFTW2G47YOC6ZdA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-2.0.3.tgz",
+ "integrity": "sha512-ManI6LC/b4oEQzsndJCsipLCbYynjZVal+jh7p/M2t1GQDEPA+7S2UDv1IYbzNy/J5DQh5htsEZFqZ3txT9/Ig==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@paralleldrive/cuid2": "^2.2.0",
"@types/node": "^20.12.7",
- "@zenstackhq/language": "2.0.1",
- "@zenstackhq/sdk": "2.0.1",
+ "@zenstackhq/language": "2.0.3",
+ "@zenstackhq/sdk": "2.0.3",
"async-exit-hook": "^2.0.1",
"change-case": "^4.1.2",
"colors": "1.4.0",
diff --git a/package.json b/package.json
index 0ad79e4..ceedfc4 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"@remix-run/node": "^2.9.1",
"@remix-run/react": "^2.9.1",
"@remix-run/serve": "^2.9.1",
- "@zenstackhq/runtime": "2.0.1",
+ "@zenstackhq/runtime": "2.0.3",
"bootstrap": "^5.3.3",
"isbot": "^4.1.0",
"lodash": "^4.17.21",
@@ -46,7 +46,7 @@
"typescript": "^5.4.5",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1",
- "zenstack": "2.0.1"
+ "zenstack": "2.0.3"
},
"engines": {
"node": ">=18.0.0"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 698a954..df8b5a8 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -125,8 +125,8 @@ model Cocktail {
}
/// @@allow('read', true)
-/// @@allow('update', auth().type == Scanner)
-/// @@allow('all', auth().type == Admin)
+/// @@allow('update', auth() != null)
+/// @@allow('create,delete', auth().type == Admin)
model Container {
id Int @id() @default(autoincrement())
barcode String? @unique()
diff --git a/schema.zmodel b/schema.zmodel
index cfc8a82..a08d72e 100644
--- a/schema.zmodel
+++ b/schema.zmodel
@@ -141,7 +141,7 @@ model Container {
checkouts History[]
@@allow('read', true)
- @@allow('update', auth().type == Scanner)
+ @@allow('update', auth() != null)
@@allow('all', auth().type == Admin)
}
diff --git a/vite.config.ts b/vite.config.ts
index 2b6aff9..c48bc63 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,5 +6,33 @@ import tsconfigPaths from "vite-tsconfig-paths";
installGlobals();
export default defineConfig({
- plugins: [remix(), tsconfigPaths()],
+ plugins: [remix({
+ routes(defineRoutes) {
+ return defineRoutes((route) => {
+ // Index
+ route("/", "routes/_index.tsx", { index: true });
+
+ // Inventory
+ route("inventory", "routes/inventory/layout.tsx", () => {
+ route("", "routes/inventory/route.tsx", { index: true });
+ route("beer/:beerslug", "routes/inventory/beer.$beerslug.tsx");
+ route("wine/:wineslug", "routes/inventory/wine.$wineslug.tsx");
+ route("soda/:sodaslug", "routes/inventory/soda.$sodaslug.tsx");
+ });
+
+ // Scanner
+ route("scan", "routes/scan/layout.tsx", () => {
+ route("", "routes/scan/route.tsx", { index: true });
+ });
+
+ // Admin
+ route("admin", "routes/admin/layout.tsx", () => {
+ route("", "routes/admin/route.tsx", { index: true });
+ route("new/beer", "routes/admin/new/beer.tsx");
+ route("edit/inventory", "routes/admin/edit/inventory.tsx");
+ route("edit/checkout", "routes/admin/edit/checkout.tsx");
+ });
+ });
+ },
+ }), tsconfigPaths()],
});