Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
062bdbd16e |
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- feature/*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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:
|
||||
- remix.run (now known as react router)
|
||||
- prisma
|
||||
|
||||
+2
-1
@@ -13,9 +13,10 @@ COPY . .
|
||||
ENV NODE_ENV=production
|
||||
ENV DATABASE_URL="REQUIRED"
|
||||
|
||||
RUN chmod +x ./migrate.sh
|
||||
RUN npx zenstack generate
|
||||
|
||||
EXPOSE 3000
|
||||
VOLUME /usr/src/app/public
|
||||
|
||||
CMD ["/bin/sh", "-c", "npx prisma db push && npm start"]
|
||||
CMD ["/bin/sh", "-c", "./migrate.sh && npm start"]
|
||||
@@ -17,6 +17,7 @@ function toDate(value: Date | string | null | undefined): Date | null {
|
||||
}
|
||||
|
||||
function DrinkCard(arg:Arguments) {
|
||||
var trimmedDescription = arg.drink.description.length > 120 ? arg.drink.description.substring(0, 120) + "..." : arg.drink.description;
|
||||
var link = "/inventory/";
|
||||
|
||||
switch (arg.drink.type) {
|
||||
@@ -60,7 +61,10 @@ function DrinkCard(arg:Arguments) {
|
||||
}
|
||||
|
||||
const drinkAddedAt = toDate((arg.drink as { addedAt?: Date | string | null }).addedAt ?? null);
|
||||
const isNewlyAdded = drinkAddedAt ? (Date.now() - drinkAddedAt.getTime()) <= 30 * 24 * 60 * 60 * 1000 : false;
|
||||
const latestTimestamp = drinkAddedAt && (!latestActivity || drinkAddedAt.getTime() > latestActivity.getTime())
|
||||
? drinkAddedAt
|
||||
: latestActivity;
|
||||
const isNewlyAdded = latestTimestamp ? (Date.now() - latestTimestamp.getTime()) <= 30 * 24 * 60 * 60 * 1000 : false;
|
||||
const isRecentlyRestocked = latestActivity ? (Date.now() - latestActivity.getTime()) <= 14 * 24 * 60 * 60 * 1000 : false;
|
||||
|
||||
return (
|
||||
@@ -72,7 +76,7 @@ function DrinkCard(arg:Arguments) {
|
||||
className='position-absolute top-0 start-0 m-2 text-wrap'
|
||||
style={{ zIndex: 2 }}
|
||||
>
|
||||
{isNewlyAdded && isRecentlyRestocked ? "New · Restocked" : isNewlyAdded ? "New" : "Restocked"}
|
||||
{isNewlyAdded && isRecentlyRestocked ? "New · Restocked" : isNewlyAdded ? "New" : "Recently added"}
|
||||
</Badge>
|
||||
) : null }
|
||||
<Card.Img
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useSubmit } from '@remix-run/react';
|
||||
import { BeerStyle, Manufacturer, WineStyle } from '@zenstackhq/runtime/models';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Button, Form, Col, Row } from 'react-bootstrap';
|
||||
import { Form, Col, Row } from 'react-bootstrap';
|
||||
|
||||
interface Arguments {
|
||||
beerStyles: BeerStyle[];
|
||||
@@ -20,28 +20,11 @@ function DrinkFilter(arg : Arguments) {
|
||||
const showSodaFilter = arg.searchParams.has("drink", "Soda");
|
||||
const showCocktailFilter = arg.searchParams.has("drink", "Cocktail");
|
||||
|
||||
const form = useRef<HTMLFormElement | null>(null);
|
||||
const drinkTypes = ["Beer", "Wine", "Soda", "Cocktail"];
|
||||
const [beerStyleExpanded, setBeerStyleExpanded] = useState(false);
|
||||
const [wineStyleExpanded, setWineStyleExpanded] = useState(false);
|
||||
const form = useRef(null);
|
||||
|
||||
const toggleDrinkType = (drinkType: string) => {
|
||||
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 submitAfterRender = function(){
|
||||
setTimeout(() => submit(form.current), 10);
|
||||
}
|
||||
|
||||
const handleFormChange = (event: React.ChangeEvent<HTMLFormElement>) => {
|
||||
const formData = new FormData(event.currentTarget);
|
||||
@@ -54,8 +37,6 @@ function DrinkFilter(arg : Arguments) {
|
||||
}
|
||||
|
||||
const abvSelection = arg.searchParams.get("abv") || "all";
|
||||
const selectedBeerStyleCount = arg.searchParams.getAll("beerstyle").length;
|
||||
const selectedWineStyleCount = arg.searchParams.getAll("winestyle").length;
|
||||
const abvOptions = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "alcohol-free", label: "Alcohol free" },
|
||||
@@ -106,24 +87,17 @@ function DrinkFilter(arg : Arguments) {
|
||||
</Form.Group>
|
||||
<Form.Group controlId="drink-type" className='bg-body-secondary rounded p-3 mt-3'>
|
||||
<Form.Label className='fw-bold'>Drink Type</Form.Label>
|
||||
<div className='row row-cols-2 g-2'>
|
||||
{drinkTypes.map((key) => (
|
||||
<div className='col' key={key}>
|
||||
<Button
|
||||
type="button"
|
||||
variant={arg.searchParams.has("drink", key) ? "primary" : "outline-primary"}
|
||||
className='w-100 text-start'
|
||||
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} />
|
||||
{ ["Beer", "Wine", "Soda", "Cocktail"].map(key => (
|
||||
<Form.Check
|
||||
id={"drink-" + key}
|
||||
type="checkbox"
|
||||
label={key}
|
||||
name="drink"
|
||||
value={key}
|
||||
defaultChecked={arg.searchParams.has("drink", key) ? true : false}
|
||||
/>
|
||||
))}
|
||||
|
||||
</Form.Group>
|
||||
<Form.Group controlId="drink-abv" className='bg-body-secondary rounded p-3 mt-3'>
|
||||
<Form.Label className='fw-bold'>Alcohol Percentage</Form.Label>
|
||||
@@ -145,67 +119,34 @@ function DrinkFilter(arg : Arguments) {
|
||||
{ showBeerFilter ? (
|
||||
<>
|
||||
<Form.Group controlId="beer-style" className='bg-body-secondary rounded p-3 mt-3'>
|
||||
<div className='d-flex justify-content-between align-items-center'>
|
||||
<Form.Label className='fw-bold mb-0'>Beer style {selectedBeerStyleCount > 0 ? ( <span className="badge text-bg-dark">{selectedBeerStyleCount} selected</span>) : ''}</Form.Label>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline-secondary'
|
||||
size='sm'
|
||||
onClick={() => setBeerStyleExpanded((current) => !current)}
|
||||
>
|
||||
{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.Label className='fw-bold mt-3'>Beer style</Form.Label>
|
||||
{ arg.beerStyles.map(style => (
|
||||
<Form.Check
|
||||
id={"beerstyle-" + style.name}
|
||||
type="checkbox"
|
||||
label={style.name}
|
||||
name="beerstyle"
|
||||
value={style.id}
|
||||
defaultChecked={arg.searchParams.has("beerstyle", style.id.toString()) ? true : false}
|
||||
/>
|
||||
))}
|
||||
</Form.Group>
|
||||
</>
|
||||
) : ""}
|
||||
{ showWineFilter ? (
|
||||
<Form.Group controlId="wine-style" className='bg-body-secondary rounded p-3 mt-3'>
|
||||
<div className='d-flex justify-content-between align-items-center'>
|
||||
<Form.Label className='fw-bold mb-0'>Wine style {selectedWineStyleCount > 0 ? ( <span className="badge text-bg-dark">{selectedWineStyleCount} selected</span>) : ''}</Form.Label>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline-secondary'
|
||||
size='sm'
|
||||
onClick={() => setWineStyleExpanded((current) => !current)}
|
||||
>
|
||||
{wineStyleExpanded ? 'Collapse' : 'Expand'}
|
||||
</Button>
|
||||
</div>
|
||||
{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>
|
||||
<Form.Label className='fw-bold'>Wine style</Form.Label>
|
||||
{ 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}
|
||||
/>
|
||||
))}
|
||||
</Form.Group>
|
||||
) : ""}
|
||||
{ showSodaFilter ? (
|
||||
<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(() => {
|
||||
tryRefresh();
|
||||
}, 15 * 60 * 1000); // Every 15 minutes
|
||||
}, 60 * 60 * 1000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("online", handleOnline);
|
||||
@@ -120,7 +120,7 @@ export function ErrorBoundary() {
|
||||
>
|
||||
Try refresh now
|
||||
</button>
|
||||
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every 15 minutes.</div>
|
||||
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every hour.</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -159,7 +159,7 @@ export function ErrorBoundary() {
|
||||
>
|
||||
Try refresh now
|
||||
</button>
|
||||
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every 15 minutes.</div>
|
||||
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every hour.</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function InventoryLayout() {
|
||||
setCheckouts(newCheckouts);
|
||||
}
|
||||
|
||||
}, 30000);
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
|
||||
+11
-16
@@ -1,8 +1,8 @@
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { Form as RemixForm, useActionData, useLoaderData } from "@remix-run/react";
|
||||
import { useActionData, useLoaderData } from "@remix-run/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Col, Container, Form as BootstrapForm, Row, Table } from "react-bootstrap";
|
||||
import { Col, Container, Form, Row, Table } from "react-bootstrap";
|
||||
import timeAgo from "~/utils/datetime";
|
||||
|
||||
import { enhance } from "~/utils/db.server";
|
||||
@@ -16,7 +16,7 @@ export async function loader({
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
const history = await dbe.history.findMany({select: {id: true, checkoutAt: true, inventoryAfter: true, container: {select: {drink: true}}}, take: 5, orderBy: {checkoutAt: "desc"}});
|
||||
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});
|
||||
}
|
||||
|
||||
@@ -116,10 +116,6 @@ export default function ScanRoute() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
|
||||
setLastScanned(aData.lastScanned);
|
||||
setShowLastScanned(true);
|
||||
|
||||
@@ -141,7 +137,7 @@ export default function ScanRoute() {
|
||||
<tr>
|
||||
<th>Drink</th>
|
||||
<th>When</th>
|
||||
<th># Left</th>
|
||||
<th>Amount left</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -162,15 +158,14 @@ export default function ScanRoute() {
|
||||
<Col>
|
||||
{/* Hidden fullscreen toggle button - triggered by clicking the h3 headers */}
|
||||
<button
|
||||
type="button"
|
||||
ref={fullscreenBtnRef}
|
||||
onClick={toggleFullscreen}
|
||||
style={{ display: 'none' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<RemixForm method="post" action="/scan?index" noValidate>
|
||||
<BootstrapForm.Group>
|
||||
<BootstrapForm.Control
|
||||
<Form method="post" action="/scan?index" noValidate>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
name="barcode"
|
||||
@@ -180,11 +175,11 @@ export default function ScanRoute() {
|
||||
inputMode="text"
|
||||
style={{ height: '3rem', fontSize: '1.8rem', textAlign: 'center' }}
|
||||
/>
|
||||
<BootstrapForm.Control.Feedback type="invalid">
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{aData?.error}
|
||||
</BootstrapForm.Control.Feedback>
|
||||
</BootstrapForm.Group>
|
||||
</RemixForm>
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
npx prisma migrate resolve --applied 0_init
|
||||
npx prisma migrate deploy
|
||||
@@ -0,0 +1,245 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ContainerType" AS ENUM ('BeerBottle', 'WineBottle', 'PlasticBottle', 'Can', 'Carton', 'Keg');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DrinkType" AS ENUM ('Drink', 'Beer', 'Wine', 'Soda', 'Cocktail');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserType" AS ENUM ('User', 'Scanner', 'Admin');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Beer" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"style_id" INTEGER NOT NULL,
|
||||
"ibu" DOUBLE PRECISION,
|
||||
"glass" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "Beer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BeerStyle" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"color" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "BeerStyle_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Cocktail" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"mix" BOOLEAN NOT NULL,
|
||||
|
||||
CONSTRAINT "Cocktail_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Container" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"barcode" TEXT,
|
||||
"drink_id" INTEGER NOT NULL,
|
||||
"section_id" INTEGER NOT NULL,
|
||||
"type" "ContainerType" NOT NULL,
|
||||
"volume" INTEGER NOT NULL,
|
||||
"portions" INTEGER,
|
||||
"price" DOUBLE PRECISION NOT NULL DEFAULT 0.0,
|
||||
"inventory" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastAdded" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Container_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Country" (
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Country_pkey" PRIMARY KEY ("code")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Drink" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"manufacturer_id" INTEGER NOT NULL,
|
||||
"type" "DrinkType" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"abv" DOUBLE PRECISION NOT NULL,
|
||||
"image" TEXT,
|
||||
"link" TEXT,
|
||||
"gluten" BOOLEAN NOT NULL,
|
||||
"lactose" BOOLEAN NOT NULL,
|
||||
"organic" BOOLEAN NOT NULL,
|
||||
"sugar" BOOLEAN NOT NULL DEFAULT true,
|
||||
"addedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Drink_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "History" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"container_id" INTEGER NOT NULL,
|
||||
"checkoutAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"inventoryAfter" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "History_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Manufacturer" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"country_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"image" TEXT,
|
||||
|
||||
CONSTRAINT "Manufacturer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Report" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"dateStart" TIMESTAMP(3) NOT NULL,
|
||||
"dateEnd" TIMESTAMP(3) NOT NULL,
|
||||
"file" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Report_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Section" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Section_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"user_id" INTEGER,
|
||||
"data" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Soda" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"carbonated" BOOLEAN NOT NULL,
|
||||
|
||||
CONSTRAINT "Soda_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Suggestion" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"name" TEXT,
|
||||
"content" TEXT NOT NULL,
|
||||
"resolved" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "Suggestion_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"type" "UserType" NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Wine" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"style_id" INTEGER NOT NULL,
|
||||
"heavy_score" INTEGER,
|
||||
"tannine_score" INTEGER,
|
||||
"dry_score" INTEGER,
|
||||
"fresh_score" INTEGER,
|
||||
"notes" TEXT,
|
||||
|
||||
CONSTRAINT "Wine_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WineStyle" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"color" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "WineStyle_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BeerStyle_name_key" ON "BeerStyle"("name" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Container_barcode_key" ON "Container"("barcode" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Drink_name_key" ON "Drink"("name" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Drink_slug_key" ON "Drink"("slug" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Manufacturer_name_key" ON "Manufacturer"("name" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Report_name_key" ON "Report"("name" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Section_name_key" ON "Section"("name" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WineStyle_name_key" ON "WineStyle"("name" ASC);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Beer" ADD CONSTRAINT "Beer_id_fkey" FOREIGN KEY ("id") REFERENCES "Drink"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Beer" ADD CONSTRAINT "Beer_style_id_fkey" FOREIGN KEY ("style_id") REFERENCES "BeerStyle"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Cocktail" ADD CONSTRAINT "Cocktail_id_fkey" FOREIGN KEY ("id") REFERENCES "Drink"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Container" ADD CONSTRAINT "Container_drink_id_fkey" FOREIGN KEY ("drink_id") REFERENCES "Drink"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Container" ADD CONSTRAINT "Container_section_id_fkey" FOREIGN KEY ("section_id") REFERENCES "Section"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Drink" ADD CONSTRAINT "Drink_manufacturer_id_fkey" FOREIGN KEY ("manufacturer_id") REFERENCES "Manufacturer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "History" ADD CONSTRAINT "History_container_id_fkey" FOREIGN KEY ("container_id") REFERENCES "Container"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Manufacturer" ADD CONSTRAINT "Manufacturer_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "Country"("code") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Session" ADD CONSTRAINT "Session_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Soda" ADD CONSTRAINT "Soda_id_fkey" FOREIGN KEY ("id") REFERENCES "Drink"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Wine" ADD CONSTRAINT "Wine_id_fkey" FOREIGN KEY ("id") REFERENCES "Drink"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Wine" ADD CONSTRAINT "Wine_style_id_fkey" FOREIGN KEY ("style_id") REFERENCES "WineStyle"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `brand_id` to the `Drink` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- CreateTable
|
||||
CREATE TABLE "Brand" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"image" TEXT,
|
||||
|
||||
CONSTRAINT "Brand_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Brand_name_key" ON "Brand"("name");
|
||||
|
||||
-- Add default brand
|
||||
INSERT INTO "Brand" ("name") VALUES ('Default');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Drink" ADD COLUMN "brand_id" INTEGER;
|
||||
UPDATE "Drink" SET "brand_id" = (SELECT "id" FROM "Brand" WHERE "name" = 'Default');
|
||||
ALTER TABLE "Drink" ALTER COLUMN "brand_id" SET NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Drink" ADD CONSTRAINT "Drink_brand_id_fkey" FOREIGN KEY ("brand_id") REFERENCES "Brand"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -40,6 +40,8 @@ model Drink {
|
||||
slug String @unique()
|
||||
manufacturer_id Int
|
||||
manufacturer Manufacturer @relation(fields: [manufacturer_id], references: [id])
|
||||
brand_id Int
|
||||
brand Brand @relation(fields: [brand_id], references: [id])
|
||||
type DrinkType
|
||||
name String @unique()
|
||||
description String
|
||||
@@ -137,6 +139,13 @@ model Manufacturer {
|
||||
drinks Drink[]
|
||||
}
|
||||
|
||||
model Brand {
|
||||
id Int @id() @default(autoincrement())
|
||||
name String @unique()
|
||||
image String?
|
||||
drinks Drink[]
|
||||
}
|
||||
|
||||
model Country {
|
||||
code String @id()
|
||||
name String
|
||||
|
||||
@@ -40,6 +40,9 @@ model Drink {
|
||||
manufacturer_id Int
|
||||
manufacturer Manufacturer @relation(fields: [manufacturer_id], references: [id])
|
||||
|
||||
brand_id Int
|
||||
brand Brand @relation(fields: [brand_id], references: [id])
|
||||
|
||||
type DrinkType
|
||||
|
||||
name String @unique
|
||||
@@ -177,6 +180,18 @@ model Manufacturer {
|
||||
@@allow('all', auth().type == Admin)
|
||||
}
|
||||
|
||||
model Brand {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
name String @unique
|
||||
image String?
|
||||
|
||||
drinks Drink[]
|
||||
|
||||
@@allow('read', true)
|
||||
@@allow('all', auth().type == Admin)
|
||||
}
|
||||
|
||||
model Country {
|
||||
code String @id
|
||||
name String
|
||||
|
||||
Reference in New Issue
Block a user