3 Commits
Author SHA1 Message Date
kennyboy55 c08a035416 Bugfix: radio buttons, new badges, sort
Build dev docker image / build (push) Successful in 23s
Build dev docker image / build (push) Successful in 35s
Build dev docker image / release (push) Successful in 1s
2026-08-11 12:17:33 +02:00
kennyboy55 0f90972bcb Fix bug with new/restock labels
Build dev docker image / build (push) Successful in 40s
Build dev docker image / build (push) Successful in 36s
Build dev docker image / release (push) Successful in 2s
2026-08-11 11:56:17 +02:00
kennyboy55 36881293ed AI Changes: scan page fix, filters improvements
Build dev docker image / build (push) Successful in 23s
Build dev docker image / build (push) Successful in 36s
Build dev docker image / release (push) Successful in 1s
2026-08-07 11:53:20 +02:00
16 changed files with 137 additions and 378 deletions
-1
View File
@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- feature/*
workflow_dispatch:
jobs:
+5
View File
@@ -1,5 +1,10 @@
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
+1 -2
View File
@@ -13,10 +13,9 @@ 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", "./migrate.sh && npm start"]
CMD ["/bin/sh", "-c", "npx prisma db push && npm start"]
+3 -7
View File
@@ -17,7 +17,6 @@ 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) {
@@ -61,10 +60,7 @@ function DrinkCard(arg:Arguments) {
}
const drinkAddedAt = toDate((arg.drink as { addedAt?: Date | string | null }).addedAt ?? null);
const latestTimestamp = drinkAddedAt && (!latestActivity || drinkAddedAt.getTime() > latestActivity.getTime())
? drinkAddedAt
: latestActivity;
const isNewlyAdded = latestTimestamp ? (Date.now() - latestTimestamp.getTime()) <= 30 * 24 * 60 * 60 * 1000 : false;
const isNewlyAdded = drinkAddedAt ? (Date.now() - drinkAddedAt.getTime()) <= 30 * 24 * 60 * 60 * 1000 : false;
const isRecentlyRestocked = latestActivity ? (Date.now() - latestActivity.getTime()) <= 14 * 24 * 60 * 60 * 1000 : false;
return (
@@ -72,11 +68,11 @@ function DrinkCard(arg:Arguments) {
<div style={{ position: 'relative', width: '100%', aspectRatio: '1 / 1', overflow: 'hidden' }}>
{ (isNewlyAdded || isRecentlyRestocked) ? (
<Badge
bg={isRecentlyRestocked ? "success" : "primary"}
bg={isNewlyAdded ? "primary" : "success"}
className='position-absolute top-0 start-0 m-2 text-wrap'
style={{ zIndex: 2 }}
>
{isNewlyAdded && isRecentlyRestocked ? "New · Restocked" : isNewlyAdded ? "New" : "Recently added"}
{isNewlyAdded ? "New" : (isRecentlyRestocked ? "Restocked" : "")}
</Badge>
) : null }
<Card.Img
+102 -40
View File
@@ -2,7 +2,7 @@
import { useSubmit } from '@remix-run/react';
import { BeerStyle, Manufacturer, WineStyle } from '@zenstackhq/runtime/models';
import { useRef, useState } from 'react';
import { Form, Col, Row } from 'react-bootstrap';
import { Button, Form, Col, Row } from 'react-bootstrap';
interface Arguments {
beerStyles: BeerStyle[];
@@ -20,11 +20,28 @@ function DrinkFilter(arg : Arguments) {
const showSodaFilter = arg.searchParams.has("drink", "Soda");
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(){
setTimeout(() => submit(form.current), 10);
}
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 handleFormChange = (event: React.ChangeEvent<HTMLFormElement>) => {
const formData = new FormData(event.currentTarget);
@@ -37,6 +54,8 @@ 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" },
@@ -57,7 +76,7 @@ function DrinkFilter(arg : Arguments) {
/>
));
const sortKey = arg.searchParams.get("sort") || "name";
const sortKey = arg.searchParams.get("sort") || "new";
return (
<Form noValidate method="GET" ref={form} className='pe-2' onChange={handleFormChange}>
@@ -66,8 +85,8 @@ function DrinkFilter(arg : Arguments) {
<Form.Group controlId="sort" className='bg-body-secondary rounded p-3'>
<Form.Label className='fw-bold'>Sort</Form.Label>
{ [
["name", "Name"],
["new", "New"],
["name", "Name"],
["popular", "Popularity"],
["abv-asc", "Alcohol (Low -> High)"],
["abv-desc", "Alcohol (High -> Low)"],
@@ -87,17 +106,24 @@ 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>
{ ["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}
/>
<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} />
))}
</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>
@@ -119,34 +145,67 @@ function DrinkFilter(arg : Arguments) {
{ showBeerFilter ? (
<>
<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>
{ 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}
/>
))}
<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.Group>
</>
) : ""}
{ showWineFilter ? (
<Form.Group controlId="wine-style" className='bg-body-secondary rounded p-3 mt-3'>
<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>
<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>
) : ""}
{ showSodaFilter ? (
<Form.Group controlId="soda-carbonated" className='bg-body-secondary rounded p-3 mt-3'>
@@ -154,6 +213,7 @@ function DrinkFilter(arg : Arguments) {
<Form.Check
type="radio"
label="Both"
id="carbonated-both"
name="carbonated"
value={0}
defaultChecked={(arg.searchParams.get("carbonated") == "0" || !arg.searchParams.has("carbonated")) ? true : false}
@@ -161,6 +221,7 @@ function DrinkFilter(arg : Arguments) {
<Form.Check
type="radio"
label="Sparkling"
id="carbonated-sparkling"
name="carbonated"
value={1}
defaultChecked={arg.searchParams.get("carbonated") == "1" ? true : false}
@@ -168,6 +229,7 @@ function DrinkFilter(arg : Arguments) {
<Form.Check
type="radio"
label="Non-sparkling"
id="carbonated-flat"
name="carbonated"
value={2}
defaultChecked={arg.searchParams.get("carbonated") == "2" ? true : false}
+5 -5
View File
@@ -97,7 +97,11 @@ function sortNewest(n1:DrinkComposite, n2:DrinkComposite) : number{
export function sortDrinksFromSearch(searchParams: URLSearchParams, searchResult: DrinkComposite[]) : DrinkComposite[] {
var callback = sortName;
var callback = sortNewest;
if(searchParams.has("sort", "new")){
callback = sortNewest;
}
if(searchParams.has("sort", "name")){
callback = sortName;
@@ -123,10 +127,6 @@ export function sortDrinksFromSearch(searchParams: URLSearchParams, searchResult
callback = sortPopularity;
}
if(searchParams.has("sort", "new")){
callback = sortNewest;
}
var sortedArray: DrinkComposite[] = searchResult.sort(callback);
return sortedArray;
+3 -3
View File
@@ -83,7 +83,7 @@ export function ErrorBoundary() {
const hourly = window.setInterval(() => {
tryRefresh();
}, 60 * 60 * 1000);
}, 15 * 60 * 1000); // Every 15 minutes
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 hour.</div>
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every 15 minutes.</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 hour.</div>
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every 15 minutes.</div>
</div>
</div>
</Col>
+1 -5
View File
@@ -78,11 +78,7 @@ export default function BeersRoute() {
return;
}
if (key === "sort" && value === "name") {
return;
}
if ((key === "min-abv" && value === "0") || (key === "max-abv" && value === "50") || (key === "min-ibu" && value === "0") || (key === "max-ibu" && value === "150")) {
if (key === "sort" && value === "new") {
return;
}
+1 -1
View File
@@ -41,7 +41,7 @@ export default function InventoryLayout() {
setCheckouts(newCheckouts);
}
}, 5000);
}, 30000);
return () => clearTimeout(timer);
});
+16 -11
View File
@@ -1,8 +1,8 @@
import type { ActionFunctionArgs, LoaderFunctionArgs } 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 { 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 { 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: 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});
}
@@ -116,6 +116,10 @@ export default function ScanRoute() {
return;
}
if (inputRef.current) {
inputRef.current.value = "";
}
setLastScanned(aData.lastScanned);
setShowLastScanned(true);
@@ -137,7 +141,7 @@ export default function ScanRoute() {
<tr>
<th>Drink</th>
<th>When</th>
<th>Amount left</th>
<th># Left</th>
</tr>
</thead>
<tbody>
@@ -158,14 +162,15 @@ 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"
/>
<Form method="post" action="/scan?index" noValidate>
<Form.Group>
<Form.Control
<RemixForm method="post" action="/scan?index" noValidate>
<BootstrapForm.Group>
<BootstrapForm.Control
ref={inputRef}
type="text"
name="barcode"
@@ -175,11 +180,11 @@ export default function ScanRoute() {
inputMode="text"
style={{ height: '3rem', fontSize: '1.8rem', textAlign: 'center' }}
/>
<Form.Control.Feedback type="invalid">
<BootstrapForm.Control.Feedback type="invalid">
{aData?.error}
</Form.Control.Feedback>
</Form.Group>
</Form>
</BootstrapForm.Control.Feedback>
</BootstrapForm.Group>
</RemixForm>
</Col>
</Row>
<Row>
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
npx prisma migrate resolve --applied 0_init
npx prisma migrate deploy
-245
View File
@@ -1,245 +0,0 @@
-- 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;
@@ -1,28 +0,0 @@
/*
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;
-3
View File
@@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
-9
View File
@@ -40,8 +40,6 @@ 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
@@ -139,13 +137,6 @@ 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
-15
View File
@@ -40,9 +40,6 @@ 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
@@ -180,18 +177,6 @@ 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