From 2a5be7917c09a283576a96006265c47a7f510601 Mon Sep 17 00:00:00 2001 From: Elias Follin Date: Mon, 20 Oct 2025 19:07:09 +0200 Subject: [PATCH 1/6] first draft --- .../migration.sql | 30 +++ src/database/prisma/schema.prisma | 35 +++ src/database/schema.zmodel | 45 ++++ src/database/seed/.snaplet/dataModel.json | 235 ++++++++++++++++++ src/lib/utils/apiNames.ts | 6 + .../(app)/admin/stocklist/+page.server.ts | 12 + src/routes/(app)/admin/stocklist/+page.svelte | 59 +++++ .../stocklist/addproduct/+page.server.ts | 46 ++++ .../admin/stocklist/addproduct/+page.svelte | 100 ++++++++ .../stocklist/stockchange/+page.server.ts | 93 +++++++ .../admin/stocklist/stockchange/+page.svelte | 81 ++++++ 11 files changed, 742 insertions(+) create mode 100644 src/database/prisma/migrations/20251007172230_sexet_stocklist/migration.sql create mode 100644 src/routes/(app)/admin/stocklist/+page.server.ts create mode 100644 src/routes/(app)/admin/stocklist/+page.svelte create mode 100644 src/routes/(app)/admin/stocklist/addproduct/+page.server.ts create mode 100644 src/routes/(app)/admin/stocklist/addproduct/+page.svelte create mode 100644 src/routes/(app)/admin/stocklist/stockchange/+page.server.ts create mode 100644 src/routes/(app)/admin/stocklist/stockchange/+page.svelte diff --git a/src/database/prisma/migrations/20251007172230_sexet_stocklist/migration.sql b/src/database/prisma/migrations/20251007172230_sexet_stocklist/migration.sql new file mode 100644 index 000000000..39d65efdb --- /dev/null +++ b/src/database/prisma/migrations/20251007172230_sexet_stocklist/migration.sql @@ -0,0 +1,30 @@ +-- CreateEnum +CREATE TYPE "DrinkQuantityType" AS ENUM ('NONE', 'WEIGHT', 'COUNTS'); + +-- CreateEnum +CREATE TYPE "DrinkGroup" AS ENUM ('S1', 'S2', 'S3', 'S4'); + +-- CreateTable +CREATE TABLE "drinkitem" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "quantity_type" "DrinkQuantityType" NOT NULL, + "name" TEXT NOT NULL, + "price" INTEGER NOT NULL, + "group" "DrinkGroup" NOT NULL, + "systembolaget_id" INTEGER NOT NULL, + + CONSTRAINT "drinkitem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "drinkitembatch" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "drink_item_id" UUID NOT NULL, + "best_before_date" TIMESTAMP(3) NOT NULL, + "quantity" INTEGER NOT NULL, + + CONSTRAINT "drinkitembatch_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "drinkitembatch" ADD CONSTRAINT "drinkitembatch_drink_item_id_fkey" FOREIGN KEY ("drink_item_id") REFERENCES "drinkitem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/database/prisma/schema.prisma b/src/database/prisma/schema.prisma index e22664203..3e539e3d2 100644 --- a/src/database/prisma/schema.prisma +++ b/src/database/prisma/schema.prisma @@ -27,6 +27,19 @@ enum recurringType { YEARLY } +enum DrinkQuantityType { + NONE + WEIGHT + COUNTS +} + +enum DrinkGroup { + S1 + S2 + S3 + S4 +} + enum DocumentType { POLICY GUIDELINE @@ -444,6 +457,28 @@ model Markdown { @@map("markdowns") } +model DrinkItem { + id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() + quantityType DrinkQuantityType @map("quantity_type") + name String + price Int + group DrinkGroup + systembolagetID Int @map("systembolaget_id") + stockLists DrinkItemBatch[] + + @@map("drinkitem") +} + +model DrinkItemBatch { + id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() + drinkItemId String @map("drink_item_id") @db.Uuid() + item DrinkItem @relation(fields: [drinkItemId], references: [id]) + bestBeforeDate DateTime @map("best_before_date") + quantity Int + + @@map("drinkitembatch") +} + model Meeting { id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() title String @db.VarChar(255) diff --git a/src/database/schema.zmodel b/src/database/schema.zmodel index 922596d3a..74d28d397 100644 --- a/src/database/schema.zmodel +++ b/src/database/schema.zmodel @@ -539,6 +539,51 @@ model Markdown { @@map("markdowns") } + +model DrinkItem { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + quantityType DrinkQuantityType @map("quantity_type") + name String + price Int + group DrinkGroup + systembolagetID Int @map("systembolaget_id") + + stockLists DrinkItemBatch[] + + @@allow("read", true) + @@allow("create", has(auth().policies, "drinkitem:create")) + @@allow("update", has(auth().policies, "drinkitem:update")) + @@allow("delete", has(auth().policies, "drinkitem:delete")) + @@map("drinkitem") +} + +model DrinkItemBatch { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + drinkItemId String @map("drink_item_id") @db.Uuid + item DrinkItem @relation(fields: [drinkItemId], references: [id]) + bestBeforeDate DateTime @map("best_before_date") + quantity Int + + @@allow("read", true) + @@allow("create", has(auth().policies, "drinkitembatch:create")) + @@allow("update", has(auth().policies, "drinkitembatch:update")) + @@allow("delete", has(auth().policies, "drinkitembatch:delete")) + @@map("drinkitembatch") +} + +enum DrinkQuantityType { + NONE + WEIGHT + COUNTS +} + +enum DrinkGroup { + S1 + S2 + S3 + S4 +} + model Meeting { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid title String @db.VarChar(255) diff --git a/src/database/seed/.snaplet/dataModel.json b/src/database/seed/.snaplet/dataModel.json index b68447411..eb67a1196 100644 --- a/src/database/seed/.snaplet/dataModel.json +++ b/src/database/seed/.snaplet/dataModel.json @@ -3786,6 +3786,210 @@ } ] }, + "drinkitem": { + "id": "public.drinkitem", + "schemaName": "public", + "tableName": "drinkitem", + "fields": [ + { + "id": "public.drinkitem.id", + "name": "id", + "columnName": "id", + "type": "uuid", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": true, + "maxLength": null + }, + { + "id": "public.drinkitem.quantity_type", + "name": "quantity_type", + "columnName": "quantity_type", + "type": "DrinkQuantityType", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.drinkitem.name", + "name": "name", + "columnName": "name", + "type": "text", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.drinkitem.price", + "name": "price", + "columnName": "price", + "type": "int4", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.drinkitem.group", + "name": "group", + "columnName": "group", + "type": "DrinkGroup", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.drinkitem.systembolaget_id", + "name": "systembolaget_id", + "columnName": "systembolaget_id", + "type": "int4", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "name": "drinkitembatch", + "type": "drinkitembatch", + "isRequired": false, + "kind": "object", + "relationName": "drinkitembatchTodrinkitem", + "relationFromFields": [], + "relationToFields": [], + "isList": true, + "isId": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false + } + ], + "uniqueConstraints": [ + { + "name": "drinkitem_pkey", + "fields": [ + "id" + ], + "nullNotDistinct": false + } + ] + }, + "drinkitembatch": { + "id": "public.drinkitembatch", + "schemaName": "public", + "tableName": "drinkitembatch", + "fields": [ + { + "id": "public.drinkitembatch.id", + "name": "id", + "columnName": "id", + "type": "uuid", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": true, + "maxLength": null + }, + { + "id": "public.drinkitembatch.drink_item_id", + "name": "drink_item_id", + "columnName": "drink_item_id", + "type": "uuid", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.drinkitembatch.best_before_date", + "name": "best_before_date", + "columnName": "best_before_date", + "type": "timestamp", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.drinkitembatch.quantity", + "name": "quantity", + "columnName": "quantity", + "type": "int4", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "name": "drinkitem", + "type": "drinkitem", + "isRequired": true, + "kind": "object", + "relationName": "drinkitembatchTodrinkitem", + "relationFromFields": [ + "drink_item_id" + ], + "relationToFields": [ + "id" + ], + "isList": false, + "isId": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false + } + ], + "uniqueConstraints": [ + { + "name": "drinkitembatch_pkey", + "fields": [ + "id" + ], + "nullNotDistinct": false + } + ] + }, "elections": { "id": "public.elections", "schemaName": "public", @@ -9850,6 +10054,37 @@ } ] }, + "DrinkGroup": { + "schemaName": "public", + "values": [ + { + "name": "S1" + }, + { + "name": "S2" + }, + { + "name": "S3" + }, + { + "name": "S4" + } + ] + }, + "DrinkQuantityType": { + "schemaName": "public", + "values": [ + { + "name": "COUNTS" + }, + { + "name": "NONE" + }, + { + "name": "WEIGHT" + } + ] + }, "ShoppableType": { "schemaName": "public", "values": [ diff --git a/src/lib/utils/apiNames.ts b/src/lib/utils/apiNames.ts index 343efd43e..eb20c8f5d 100644 --- a/src/lib/utils/apiNames.ts +++ b/src/lib/utils/apiNames.ts @@ -21,6 +21,12 @@ const apiNames = { TAGS: { ...crud("tags"), }, + DRINKITEM: { + ...crud("drinkitem"), + }, + DRINKITEMBATCH: { + ...crud("drinkitembatch"), + }, EVENT: { ...crud("event"), COMMENT: "event:comment", diff --git a/src/routes/(app)/admin/stocklist/+page.server.ts b/src/routes/(app)/admin/stocklist/+page.server.ts new file mode 100644 index 000000000..803af1d85 --- /dev/null +++ b/src/routes/(app)/admin/stocklist/+page.server.ts @@ -0,0 +1,12 @@ +import apiNames from "$lib/utils/apiNames"; +import { authorize } from "$lib/utils/authorization"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ locals }) => { + const { prisma, user } = locals; + + const items = await prisma.drinkItemBatch.findMany({ + include: { item: true }, + }); + return { items }; +}; diff --git a/src/routes/(app)/admin/stocklist/+page.svelte b/src/routes/(app)/admin/stocklist/+page.svelte new file mode 100644 index 000000000..86297633c --- /dev/null +++ b/src/routes/(app)/admin/stocklist/+page.svelte @@ -0,0 +1,59 @@ + + + + + +
+ +
+
+ + + + + + + + + + + + {#each data.items as item} + + + + + + + + {/each} + +
Id NamnPrisAntalGroup
{item.item.systembolagetID}{item.item.name}{item.item.price / 100}{item.quantity}{item.item.group}
+
diff --git a/src/routes/(app)/admin/stocklist/addproduct/+page.server.ts b/src/routes/(app)/admin/stocklist/addproduct/+page.server.ts new file mode 100644 index 000000000..a6accfebb --- /dev/null +++ b/src/routes/(app)/admin/stocklist/addproduct/+page.server.ts @@ -0,0 +1,46 @@ +import { DrinkQuantityType, DrinkGroup } from "@prisma/client"; +import type { Actions } from "@sveltejs/kit"; +import { superValidate, fail, message } from "sveltekit-superforms"; +import { zod } from "sveltekit-superforms/adapters"; +import { z } from "zod"; +import type { PageServerLoad } from "./$types"; +import apiNames from "$lib/utils/apiNames"; +import { authorize } from "$lib/utils/authorization"; + +const zDrinkGroup = z.nativeEnum(DrinkGroup); +const zDrinkQuantityType = z.nativeEnum(DrinkQuantityType); + +export const load: PageServerLoad = async (event) => { + const { prisma } = event.locals; + const form = await superValidate(event.request, zod(DrinkItemSchema)); + return { form }; +}; + +const DrinkItemSchema = z.object({ + quantityType: zDrinkQuantityType, + name: z.string().min(1), + price: z.number(), + group: zDrinkGroup, + systembolagetID: z.number().int(), +}); + +export const actions: Actions = { + createDrinkItem: async (event) => { + const { user, prisma } = event.locals; + authorize(apiNames.DRINKITEM.CREATE, user); + const form = await superValidate(event.request, zod(DrinkItemSchema)); + if (!form.valid) return fail(400, { form }); + + await prisma.drinkItem.create({ + data: { + quantityType: form.data.quantityType, + name: form.data.name, + price: form.data.price * 100, + group: form.data.group, + systembolagetID: form.data.systembolagetID, + }, + }); + + return message(form, { message: "Produkt tillagd" }); + }, +}; diff --git a/src/routes/(app)/admin/stocklist/addproduct/+page.svelte b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte new file mode 100644 index 000000000..adaa5c980 --- /dev/null +++ b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte @@ -0,0 +1,100 @@ + + +
+ +
+ +
+ +
+{#if $form.quantityType != DrinkQuantityType.NONE} +
+

+ {$form.quantityType == DrinkQuantityType.COUNTS + ? "Öl/Cider/Vin/Annat" + : "Sprit"} +

+
+ + + + + + +
+ +
+ +
+{/if} diff --git a/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts b/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts new file mode 100644 index 000000000..350053483 --- /dev/null +++ b/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts @@ -0,0 +1,93 @@ +import type { Actions } from "@sveltejs/kit"; +import { superValidate, fail, message } from "sveltekit-superforms"; +import { zod } from "sveltekit-superforms/adapters"; +import { z } from "zod"; +import type { PageServerLoad } from "./$types"; +import apiNames from "$lib/utils/apiNames"; +import { authorize } from "$lib/utils/authorization"; + +export const load: PageServerLoad = async (event) => { + const { prisma } = event.locals; + const form = await superValidate(event.request, zod(DrinkItemBatchSchema)); + const drinks = await prisma.drinkItem.findMany(); + return { form, drinks }; +}; + +const DrinkItemBatchSchema = z.object({ + drinkItemId: z.string(), + bestBeforeDate: z.string(), + quantity: z.number(), + inOut: z.string(), +}); + +export const actions: Actions = { + createDrinkItemBatch: async (event) => { + const { user, prisma } = event.locals; + authorize(apiNames.DRINKITEMBATCH.CREATE, user); + const form = await superValidate(event.request, zod(DrinkItemBatchSchema)); + if (!form.valid) return fail(400, { form }); + + if (form.data.inOut == "IN") { + await prisma.drinkItemBatch.create({ + data: { + drinkItemId: form.data.drinkItemId, + quantity: form.data.quantity, + bestBeforeDate: form.data.bestBeforeDate, + }, + }); + return message(form, { message: "Antal inskrivet" }); + } + if (form.data.inOut == "OUT") { + await prisma.$transaction(async (tx) => { + const drinkItemId = form.data.drinkItemId; + const requested = form.data.quantity; + + if (requested <= 0) { + throw new Error("Quantity must be greater than 0."); + } + + // Fetch all batches for this item with stock, oldest first (FIFO) + const batches = await tx.drinkItemBatch.findMany({ + where: { + drinkItemId, + quantity: { gt: 0 }, + }, + orderBy: [{ bestBeforeDate: "asc" }, { id: "asc" }], // oldest first + }); + + const available = batches.reduce((sum, b) => sum + b.quantity, 0); + + if (available < requested) { + throw new Error( + `Not enough stock for drinkItemId=${drinkItemId}. Requested: ${requested}, available: ${available}.`, + ); + } + + // Deduct across batches FIFO + let remaining = requested; + + for (const batch of batches) { + if (remaining === 0) break; + + const take = Math.min(batch.quantity, remaining); + + await tx.drinkItemBatch.update({ + where: { id: batch.id }, + data: { + quantity: { decrement: take }, // atomic + }, + }); + + remaining -= take; + } + }); + return message(form, { message: "Antal utskrivet" }); + } + }, +}; + +// HÄMTA ALLA DRINK ITEMS + +// SKAPA EN FUNCTION FÖR ATT SKRIVA IN + +// SKAPA EN FUNCTION FÖR ATT SKRIVA UT diff --git a/src/routes/(app)/admin/stocklist/stockchange/+page.svelte b/src/routes/(app)/admin/stocklist/stockchange/+page.svelte new file mode 100644 index 000000000..ac8981a87 --- /dev/null +++ b/src/routes/(app)/admin/stocklist/stockchange/+page.svelte @@ -0,0 +1,81 @@ + + +
+ +
+ +
+ +
+ +{#if $form.inOut != ""} +
+

+ {$form.inOut == "IN" ? "Skriv in" : "Skriv ut"} +

+
+ + + + +
+ +
+ +
+{/if} From f2ee779d561628c1e5ded44cd77f5587003b6fb9 Mon Sep 17 00:00:00 2001 From: Elias Follin Date: Mon, 3 Nov 2025 17:22:51 +0100 Subject: [PATCH 2/6] second draft --- .../migration.sql | 8 +++++ src/database/prisma/schema.prisma | 9 +++-- src/database/schema.zmodel | 9 +++-- src/database/seed/.snaplet/dataModel.json | 14 -------- .../(app)/admin/stocklist/+page.server.ts | 20 ++++++++++- src/routes/(app)/admin/stocklist/+page.svelte | 11 ++++-- .../admin/stocklist/addproduct/+page.svelte | 22 ++++-------- .../stocklist/stockchange/+page.server.ts | 9 ++--- .../admin/stocklist/stockchange/+page.svelte | 36 ++++++++----------- 9 files changed, 69 insertions(+), 69 deletions(-) create mode 100644 src/database/prisma/migrations/20251103141206_stocklist_update/migration.sql diff --git a/src/database/prisma/migrations/20251103141206_stocklist_update/migration.sql b/src/database/prisma/migrations/20251103141206_stocklist_update/migration.sql new file mode 100644 index 000000000..f23cccad9 --- /dev/null +++ b/src/database/prisma/migrations/20251103141206_stocklist_update/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `best_before_date` on the `drinkitembatch` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "drinkitembatch" DROP COLUMN "best_before_date"; diff --git a/src/database/prisma/schema.prisma b/src/database/prisma/schema.prisma index 3e539e3d2..47154677d 100644 --- a/src/database/prisma/schema.prisma +++ b/src/database/prisma/schema.prisma @@ -470,11 +470,10 @@ model DrinkItem { } model DrinkItemBatch { - id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() - drinkItemId String @map("drink_item_id") @db.Uuid() - item DrinkItem @relation(fields: [drinkItemId], references: [id]) - bestBeforeDate DateTime @map("best_before_date") - quantity Int + id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() + drinkItemId String @map("drink_item_id") @db.Uuid() + item DrinkItem @relation(fields: [drinkItemId], references: [id]) + quantity Int @@map("drinkitembatch") } diff --git a/src/database/schema.zmodel b/src/database/schema.zmodel index 74d28d397..15717d536 100644 --- a/src/database/schema.zmodel +++ b/src/database/schema.zmodel @@ -558,11 +558,10 @@ model DrinkItem { } model DrinkItemBatch { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - drinkItemId String @map("drink_item_id") @db.Uuid - item DrinkItem @relation(fields: [drinkItemId], references: [id]) - bestBeforeDate DateTime @map("best_before_date") - quantity Int + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + drinkItemId String @map("drink_item_id") @db.Uuid + item DrinkItem @relation(fields: [drinkItemId], references: [id]) + quantity Int @@allow("read", true) @@allow("create", has(auth().policies, "drinkitembatch:create")) diff --git a/src/database/seed/.snaplet/dataModel.json b/src/database/seed/.snaplet/dataModel.json index eb67a1196..4c70806cc 100644 --- a/src/database/seed/.snaplet/dataModel.json +++ b/src/database/seed/.snaplet/dataModel.json @@ -3933,20 +3933,6 @@ "isId": false, "maxLength": null }, - { - "id": "public.drinkitembatch.best_before_date", - "name": "best_before_date", - "columnName": "best_before_date", - "type": "timestamp", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, { "id": "public.drinkitembatch.quantity", "name": "quantity", diff --git a/src/routes/(app)/admin/stocklist/+page.server.ts b/src/routes/(app)/admin/stocklist/+page.server.ts index 803af1d85..6b85958e2 100644 --- a/src/routes/(app)/admin/stocklist/+page.server.ts +++ b/src/routes/(app)/admin/stocklist/+page.server.ts @@ -8,5 +8,23 @@ export const load: PageServerLoad = async ({ locals }) => { const items = await prisma.drinkItemBatch.findMany({ include: { item: true }, }); - return { items }; + + const groupedMap = new Map(); + + for (const batch of items) { + const existing = groupedMap.get(batch.item.id); + if (existing) { + existing.quantity += batch.quantity; + } else { + groupedMap.set(batch.item.id, { ...batch }); + } + } + + const grouped = Array.from(groupedMap.values()); + + const totalInventoryValue = items.reduce( + (sum, i) => sum + i.item.price * i.quantity, + 0, + ); + return { totalInventoryValue, grouped }; }; diff --git a/src/routes/(app)/admin/stocklist/+page.svelte b/src/routes/(app)/admin/stocklist/+page.svelte index 86297633c..f8939aab7 100644 --- a/src/routes/(app)/admin/stocklist/+page.svelte +++ b/src/routes/(app)/admin/stocklist/+page.svelte @@ -20,8 +20,10 @@ }} /> -
-
    +
    + +

    + Totalt lagervärde: {data.totalInventoryValue / 100} kr +

    @@ -45,7 +50,7 @@ - {#each data.items as item} + {#each data.grouped as item} diff --git a/src/routes/(app)/admin/stocklist/addproduct/+page.svelte b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte index adaa5c980..cc4e82bb6 100644 --- a/src/routes/(app)/admin/stocklist/addproduct/+page.svelte +++ b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte @@ -6,11 +6,12 @@ import Labeled from "$lib/components/Labeled.svelte"; const drinkGroup = Object.values(DrinkGroup); - let selected: DrinkQuantityType | "" = ""; export let data: PageData; - const { form, enhance } = superForm(data.form); + const { form, enhance } = superForm(data.form, { + resetForm: true, + });
    @@ -28,19 +29,10 @@
    - + diff --git a/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts b/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts index 350053483..d507e516f 100644 --- a/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts +++ b/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts @@ -15,7 +15,6 @@ export const load: PageServerLoad = async (event) => { const DrinkItemBatchSchema = z.object({ drinkItemId: z.string(), - bestBeforeDate: z.string(), quantity: z.number(), inOut: z.string(), }); @@ -23,18 +22,21 @@ const DrinkItemBatchSchema = z.object({ export const actions: Actions = { createDrinkItemBatch: async (event) => { const { user, prisma } = event.locals; + authorize(apiNames.DRINKITEMBATCH.CREATE, user); const form = await superValidate(event.request, zod(DrinkItemBatchSchema)); if (!form.valid) return fail(400, { form }); - + console.log(form.data.inOut); + console.log(form.data.drinkItemId); + console.log(form.data.quantity); if (form.data.inOut == "IN") { await prisma.drinkItemBatch.create({ data: { drinkItemId: form.data.drinkItemId, quantity: form.data.quantity, - bestBeforeDate: form.data.bestBeforeDate, }, }); + return message(form, { message: "Antal inskrivet" }); } if (form.data.inOut == "OUT") { @@ -52,7 +54,6 @@ export const actions: Actions = { drinkItemId, quantity: { gt: 0 }, }, - orderBy: [{ bestBeforeDate: "asc" }, { id: "asc" }], // oldest first }); const available = batches.reduce((sum, b) => sum + b.quantity, 0); diff --git a/src/routes/(app)/admin/stocklist/stockchange/+page.svelte b/src/routes/(app)/admin/stocklist/stockchange/+page.svelte index ac8981a87..ef458b06c 100644 --- a/src/routes/(app)/admin/stocklist/stockchange/+page.svelte +++ b/src/routes/(app)/admin/stocklist/stockchange/+page.svelte @@ -4,8 +4,8 @@ import { DrinkQuantityType } from "@prisma/client"; import Input from "$lib/components/Input.svelte"; import Labeled from "$lib/components/Labeled.svelte"; - - let selected: string | "" = ""; + import Price from "$lib/components/Price.svelte"; + import { id_ID } from "@faker-js/faker"; export let data: PageData; @@ -27,18 +27,10 @@
    - - - + +
    @@ -55,10 +47,17 @@ use:enhance > - {#each data.drinks as drink} - {/each} + {drink.name} ({drink.price / 100} kr) + {/each} + -
    From 05f0499ae51e6e3ba8840ceb9b93e9779d72a340 Mon Sep 17 00:00:00 2001 From: Elias Follin Date: Mon, 1 Dec 2025 16:40:33 +0100 Subject: [PATCH 3/6] draft 2 --- .../migrations/20251124165427_/migration.sql | 3 + .../migrations/20251124193841_/migration.sql | 8 + .../migrations/20251124204050_/migration.sql | 10 ++ .../migrations/20251124204155_/migration.sql | 10 ++ src/database/prisma/schema.prisma | 29 ++- src/database/schema.zmodel | 33 +++- src/database/seed/.snaplet/dataModel.json | 136 +++++++++++++- src/lib/utils/getTotalInventoryValue.ts | 40 +++++ .../(app)/admin/stocklist/+page.server.ts | 25 +-- src/routes/(app)/admin/stocklist/+page.svelte | 63 ++++--- .../stocklist/addproduct/+page.server.ts | 4 + .../admin/stocklist/addproduct/+page.svelte | 23 ++- .../stocklist/stockchange/+page.server.ts | 105 ++++++----- .../admin/stocklist/stockchange/+page.svelte | 170 +++++++++++++----- .../admin/stocklist/treasury/+page.server.ts | 122 +++++++++++++ .../admin/stocklist/treasury/+page.svelte | 108 +++++++++++ .../(app)/api/admin/stocklist/+server.ts | 13 ++ src/routes/routes.ts | 7 + src/translations/en.json | 3 +- src/translations/sv.json | 3 +- 20 files changed, 756 insertions(+), 159 deletions(-) create mode 100644 src/database/prisma/migrations/20251124165427_/migration.sql create mode 100644 src/database/prisma/migrations/20251124193841_/migration.sql create mode 100644 src/database/prisma/migrations/20251124204050_/migration.sql create mode 100644 src/database/prisma/migrations/20251124204155_/migration.sql create mode 100644 src/lib/utils/getTotalInventoryValue.ts create mode 100644 src/routes/(app)/admin/stocklist/treasury/+page.server.ts create mode 100644 src/routes/(app)/admin/stocklist/treasury/+page.svelte create mode 100644 src/routes/(app)/api/admin/stocklist/+server.ts diff --git a/src/database/prisma/migrations/20251124165427_/migration.sql b/src/database/prisma/migrations/20251124165427_/migration.sql new file mode 100644 index 000000000..821100078 --- /dev/null +++ b/src/database/prisma/migrations/20251124165427_/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "drinkitem" ADD COLUMN "bottle_empty_weight" INTEGER, +ADD COLUMN "bottle_full_weight" INTEGER; diff --git a/src/database/prisma/migrations/20251124193841_/migration.sql b/src/database/prisma/migrations/20251124193841_/migration.sql new file mode 100644 index 000000000..b9ec7873b --- /dev/null +++ b/src/database/prisma/migrations/20251124193841_/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "sexetinventoryvaluelog" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "date" TIMESTAMP(3) NOT NULL, + "value" INTEGER NOT NULL, + + CONSTRAINT "sexetinventoryvaluelog_pkey" PRIMARY KEY ("id") +); diff --git a/src/database/prisma/migrations/20251124204050_/migration.sql b/src/database/prisma/migrations/20251124204050_/migration.sql new file mode 100644 index 000000000..6aba6d7a7 --- /dev/null +++ b/src/database/prisma/migrations/20251124204050_/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - Added the required column `date` to the `drinkitembatch` table without a default value. This is not possible if the table is not empty. + - Added the required column `user` to the `drinkitembatch` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "drinkitembatch" ADD COLUMN "date" TIMESTAMP(3) NOT NULL, +ADD COLUMN "user" TEXT NOT NULL; diff --git a/src/database/prisma/migrations/20251124204155_/migration.sql b/src/database/prisma/migrations/20251124204155_/migration.sql new file mode 100644 index 000000000..702aa9dbd --- /dev/null +++ b/src/database/prisma/migrations/20251124204155_/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `quantity` on the `drinkitembatch` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "drinkitembatch" DROP COLUMN "quantity", +ADD COLUMN "quantityIn" INTEGER, +ADD COLUMN "quantityOut" INTEGER; diff --git a/src/database/prisma/schema.prisma b/src/database/prisma/schema.prisma index 47154677d..171fbbb49 100644 --- a/src/database/prisma/schema.prisma +++ b/src/database/prisma/schema.prisma @@ -458,13 +458,15 @@ model Markdown { } model DrinkItem { - id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() - quantityType DrinkQuantityType @map("quantity_type") - name String - price Int - group DrinkGroup - systembolagetID Int @map("systembolaget_id") - stockLists DrinkItemBatch[] + id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() + quantityType DrinkQuantityType @map("quantity_type") + name String + price Int + group DrinkGroup + systembolagetID Int @map("systembolaget_id") + bottleEmptyWeight Int? @map("bottle_empty_weight") + bottleFullWeight Int? @map("bottle_full_weight") + stockLists DrinkItemBatch[] @@map("drinkitem") } @@ -473,11 +475,22 @@ model DrinkItemBatch { id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() drinkItemId String @map("drink_item_id") @db.Uuid() item DrinkItem @relation(fields: [drinkItemId], references: [id]) - quantity Int + quantityIn Int? + quantityOut Int? + user String + date DateTime @@map("drinkitembatch") } +model SexetInventoryValueLog { + id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() + date DateTime + value Int + + @@map("sexetinventoryvaluelog") +} + model Meeting { id String @id() @default(dbgenerated("gen_random_uuid()")) @db.Uuid() title String @db.VarChar(255) diff --git a/src/database/schema.zmodel b/src/database/schema.zmodel index 15717d536..e5e2b2589 100644 --- a/src/database/schema.zmodel +++ b/src/database/schema.zmodel @@ -541,14 +541,16 @@ model Markdown { model DrinkItem { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - quantityType DrinkQuantityType @map("quantity_type") - name String - price Int - group DrinkGroup - systembolagetID Int @map("systembolaget_id") + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + quantityType DrinkQuantityType @map("quantity_type") + name String + price Int + group DrinkGroup + systembolagetID Int @map("systembolaget_id") + bottleEmptyWeight Int? @map("bottle_empty_weight") + bottleFullWeight Int? @map("bottle_full_weight") - stockLists DrinkItemBatch[] + stockLists DrinkItemBatch[] @@allow("read", true) @@allow("create", has(auth().policies, "drinkitem:create")) @@ -561,7 +563,10 @@ model DrinkItemBatch { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid drinkItemId String @map("drink_item_id") @db.Uuid item DrinkItem @relation(fields: [drinkItemId], references: [id]) - quantity Int + quantityIn Int? + quantityOut Int? + user String + date DateTime @@allow("read", true) @@allow("create", has(auth().policies, "drinkitembatch:create")) @@ -570,6 +575,18 @@ model DrinkItemBatch { @@map("drinkitembatch") } +model SexetInventoryValueLog { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + date DateTime + value Int + + @@allow("read", true) + @@allow("create", has(auth().policies, "drinkitembatch:create")) + @@allow("update", has(auth().policies, "drinkitembatch:update")) + @@allow("delete", has(auth().policies, "drinkitembatch:delete")) + @@map("sexetinventoryvaluelog") +} + enum DrinkQuantityType { NONE WEIGHT diff --git a/src/database/seed/.snaplet/dataModel.json b/src/database/seed/.snaplet/dataModel.json index 4c70806cc..589a7e666 100644 --- a/src/database/seed/.snaplet/dataModel.json +++ b/src/database/seed/.snaplet/dataModel.json @@ -3875,6 +3875,34 @@ "isId": false, "maxLength": null }, + { + "id": "public.drinkitem.bottle_empty_weight", + "name": "bottle_empty_weight", + "columnName": "bottle_empty_weight", + "type": "int4", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.drinkitem.bottle_full_weight", + "name": "bottle_full_weight", + "columnName": "bottle_full_weight", + "type": "int4", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, { "name": "drinkitembatch", "type": "drinkitembatch", @@ -3934,10 +3962,10 @@ "maxLength": null }, { - "id": "public.drinkitembatch.quantity", - "name": "quantity", - "columnName": "quantity", - "type": "int4", + "id": "public.drinkitembatch.date", + "name": "date", + "columnName": "date", + "type": "timestamp", "isRequired": true, "kind": "scalar", "isList": false, @@ -3947,6 +3975,48 @@ "isId": false, "maxLength": null }, + { + "id": "public.drinkitembatch.user", + "name": "user", + "columnName": "user", + "type": "text", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.drinkitembatch.quantityIn", + "name": "quantityIn", + "columnName": "quantityIn", + "type": "int4", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.drinkitembatch.quantityOut", + "name": "quantityOut", + "columnName": "quantityOut", + "type": "int4", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, { "name": "drinkitem", "type": "drinkitem", @@ -8626,6 +8696,64 @@ } ] }, + "sexetinventoryvaluelog": { + "id": "public.sexetinventoryvaluelog", + "schemaName": "public", + "tableName": "sexetinventoryvaluelog", + "fields": [ + { + "id": "public.sexetinventoryvaluelog.id", + "name": "id", + "columnName": "id", + "type": "uuid", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": true, + "maxLength": null + }, + { + "id": "public.sexetinventoryvaluelog.date", + "name": "date", + "columnName": "date", + "type": "timestamp", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public.sexetinventoryvaluelog.value", + "name": "value", + "columnName": "value", + "type": "int4", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + } + ], + "uniqueConstraints": [ + { + "name": "sexetinventoryvaluelog_pkey", + "fields": [ + "id" + ], + "nullNotDistinct": false + } + ] + }, "shoppable": { "id": "public.shoppable", "schemaName": "public", diff --git a/src/lib/utils/getTotalInventoryValue.ts b/src/lib/utils/getTotalInventoryValue.ts new file mode 100644 index 000000000..18093fb71 --- /dev/null +++ b/src/lib/utils/getTotalInventoryValue.ts @@ -0,0 +1,40 @@ +import { DrinkQuantityType } from "@prisma/client"; +import type { PrismaClient } from "@prisma/client"; + +export async function getTotalInventoryValue(prisma: PrismaClient) { + const items = await prisma.drinkItemBatch.findMany({ + include: { item: true }, + }); + + const groupedMap = new Map(); + + for (const batch of items) { + const existing = groupedMap.get(batch.item.id); + if (existing) { + existing.quantityIn = + (existing.quantityIn ?? 0) + (batch.quantityIn ?? 0); + existing.quantityOut = + (existing.quantityOut ?? 0) + (batch.quantityOut ?? 0); + } else { + groupedMap.set(batch.item.id, { ...batch }); + } + } + + const grouped = Array.from(groupedMap.values()); + + const totalInventoryValue = grouped.reduce((sum, i) => { + if (i.item.quantityType === DrinkQuantityType.WEIGHT) { + const realWeight = + (i.quantityIn ?? 0) - (i.quantityOut ?? 0) - i.item.bottleEmptyWeight!; + const fullRealWeight = + i.item.bottleFullWeight! - i.item.bottleEmptyWeight!; + const pricePerWeight = i.item.price / fullRealWeight; + const price = pricePerWeight * realWeight; + return Math.floor(sum + price); + } + + return sum + i.item.price * ((i.quantityIn ?? 0) - (i.quantityOut ?? 0)); + }, 0); + + return { totalInventoryValue, grouped }; +} diff --git a/src/routes/(app)/admin/stocklist/+page.server.ts b/src/routes/(app)/admin/stocklist/+page.server.ts index 6b85958e2..f7dfdcaaf 100644 --- a/src/routes/(app)/admin/stocklist/+page.server.ts +++ b/src/routes/(app)/admin/stocklist/+page.server.ts @@ -1,30 +1,11 @@ -import apiNames from "$lib/utils/apiNames"; -import { authorize } from "$lib/utils/authorization"; +import { DrinkQuantityType } from "@prisma/client"; import type { PageServerLoad } from "./$types"; +import { getTotalInventoryValue } from "$lib/utils/getTotalInventoryValue"; export const load: PageServerLoad = async ({ locals }) => { const { prisma, user } = locals; - const items = await prisma.drinkItemBatch.findMany({ - include: { item: true }, - }); + const { totalInventoryValue, grouped } = await getTotalInventoryValue(prisma); - const groupedMap = new Map(); - - for (const batch of items) { - const existing = groupedMap.get(batch.item.id); - if (existing) { - existing.quantity += batch.quantity; - } else { - groupedMap.set(batch.item.id, { ...batch }); - } - } - - const grouped = Array.from(groupedMap.values()); - - const totalInventoryValue = items.reduce( - (sum, i) => sum + i.item.price * i.quantity, - 0, - ); return { totalInventoryValue, grouped }; }; diff --git a/src/routes/(app)/admin/stocklist/+page.svelte b/src/routes/(app)/admin/stocklist/+page.svelte index f8939aab7..48594f554 100644 --- a/src/routes/(app)/admin/stocklist/+page.svelte +++ b/src/routes/(app)/admin/stocklist/+page.svelte @@ -1,12 +1,8 @@ @@ -23,17 +19,22 @@
    - +

    Totalt lagervärde: {data.totalInventoryValue / 100} kr

    @@ -45,19 +46,33 @@
    - + {#each data.grouped as item} - - - - - - - + {#if item.item.quantityType === DrinkQuantityType.COUNTS} + + + + + + + + {:else} + + + + + + + + {/if} {/each}
    {item.item.systembolagetID} {item.item.name} Id Namn PrisAntalAntal/Vikt Group
    {item.item.systembolagetID}{item.item.name}{item.item.price / 100}{item.quantity}{item.item.group}
    {item.item.systembolagetID}{item.item.name}{item.item.price / 100} kr{(item.quantityIn ?? 0) - (item.quantityOut ?? 0)}{item.item.group}
    {item.item.systembolagetID}{item.item.name}{item.item.price / 100} kr{(item.quantityIn ?? 0) - + (item.quantityOut ?? 0) - + item.item.bottleEmptyWeight!} g{item.item.group}
    diff --git a/src/routes/(app)/admin/stocklist/addproduct/+page.server.ts b/src/routes/(app)/admin/stocklist/addproduct/+page.server.ts index a6accfebb..bb2ae11be 100644 --- a/src/routes/(app)/admin/stocklist/addproduct/+page.server.ts +++ b/src/routes/(app)/admin/stocklist/addproduct/+page.server.ts @@ -22,6 +22,8 @@ const DrinkItemSchema = z.object({ price: z.number(), group: zDrinkGroup, systembolagetID: z.number().int(), + bottleEmptyWeight: z.number().int(), + bottleFullWeight: z.number().int(), }); export const actions: Actions = { @@ -38,6 +40,8 @@ export const actions: Actions = { price: form.data.price * 100, group: form.data.group, systembolagetID: form.data.systembolagetID, + bottleEmptyWeight: form.data.bottleEmptyWeight, + bottleFullWeight: form.data.bottleFullWeight, }, }); diff --git a/src/routes/(app)/admin/stocklist/addproduct/+page.svelte b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte index cc4e82bb6..5ea0c3c81 100644 --- a/src/routes/(app)/admin/stocklist/addproduct/+page.svelte +++ b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte @@ -25,6 +25,9 @@ + + +
@@ -71,9 +74,9 @@ bind:value={$form.systembolagetID} /> - {#each drinkGroup as dg} - + {/each} + {#if $form.quantityType === DrinkQuantityType.WEIGHT} + + + {/if}
diff --git a/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts b/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts index d507e516f..086ccafef 100644 --- a/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts +++ b/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts @@ -10,13 +10,27 @@ export const load: PageServerLoad = async (event) => { const { prisma } = event.locals; const form = await superValidate(event.request, zod(DrinkItemBatchSchema)); const drinks = await prisma.drinkItem.findMany(); - return { form, drinks }; + + const entriesIn = await prisma.drinkItemBatch.findMany({ + where: { + quantityIn: { gt: 0 }, + }, + }); + const entriesOut = await prisma.drinkItemBatch.findMany({ + where: { + quantityOut: { gt: 0 }, + }, + }); + + return { form, drinks, entriesIn, entriesOut }; }; const DrinkItemBatchSchema = z.object({ drinkItemId: z.string(), - quantity: z.number(), + quantityOut: z.number().nonnegative(), inOut: z.string(), + quantityIn: z.number().nonnegative(), + date: z.coerce.date().default(() => new Date()), }); export const actions: Actions = { @@ -26,69 +40,62 @@ export const actions: Actions = { authorize(apiNames.DRINKITEMBATCH.CREATE, user); const form = await superValidate(event.request, zod(DrinkItemBatchSchema)); if (!form.valid) return fail(400, { form }); - console.log(form.data.inOut); - console.log(form.data.drinkItemId); - console.log(form.data.quantity); + if (form.data.inOut == "IN") { + if (form.data.quantityIn === 0) { + return message(form, { message: "Får inte vara 0" }); + } await prisma.drinkItemBatch.create({ data: { drinkItemId: form.data.drinkItemId, - quantity: form.data.quantity, + quantityIn: form.data.quantityIn, + date: form.data.date, + user: user.studentId!, }, }); return message(form, { message: "Antal inskrivet" }); } if (form.data.inOut == "OUT") { - await prisma.$transaction(async (tx) => { - const drinkItemId = form.data.drinkItemId; - const requested = form.data.quantity; + if (form.data.quantityOut === 0) { + return message(form, { message: "Får inte vara 0" }); + } + const requestedId = form.data.drinkItemId; + const requestedAmount = form.data.quantityOut; + + const entriesIn = await prisma.drinkItemBatch.findMany({ + where: { + drinkItemId: requestedId, + quantityIn: { gt: 0 }, + }, + }); + const entriesOut = await prisma.drinkItemBatch.findMany({ + where: { + drinkItemId: requestedId, + quantityOut: { gt: 0 }, + }, + }); + const amountIn = entriesIn.reduce((sum, i) => sum + i.quantityIn!, 0); + const amountOut = entriesOut.reduce((sum, i) => sum + i.quantityOut!, 0); - if (requested <= 0) { - throw new Error("Quantity must be greater than 0."); - } + const availableAmount = amountIn - amountOut; - // Fetch all batches for this item with stock, oldest first (FIFO) - const batches = await tx.drinkItemBatch.findMany({ - where: { - drinkItemId, - quantity: { gt: 0 }, - }, + if (requestedAmount! > availableAmount) { + return message(form, { + message: `Finns inte ${requestedAmount} i lager`, }); + } - const available = batches.reduce((sum, b) => sum + b.quantity, 0); - - if (available < requested) { - throw new Error( - `Not enough stock for drinkItemId=${drinkItemId}. Requested: ${requested}, available: ${available}.`, - ); - } - - // Deduct across batches FIFO - let remaining = requested; - - for (const batch of batches) { - if (remaining === 0) break; - - const take = Math.min(batch.quantity, remaining); - - await tx.drinkItemBatch.update({ - where: { id: batch.id }, - data: { - quantity: { decrement: take }, // atomic - }, - }); - - remaining -= take; - } + await prisma.drinkItemBatch.create({ + data: { + drinkItemId: form.data.drinkItemId, + quantityOut: form.data.quantityOut, + date: form.data.date, + user: user.studentId!, + }, }); + return message(form, { message: "Antal utskrivet" }); } }, }; - -// HÄMTA ALLA DRINK ITEMS - -// SKAPA EN FUNCTION FÖR ATT SKRIVA IN - -// SKAPA EN FUNCTION FÖR ATT SKRIVA UT diff --git a/src/routes/(app)/admin/stocklist/stockchange/+page.svelte b/src/routes/(app)/admin/stocklist/stockchange/+page.svelte index ef458b06c..3c0edc336 100644 --- a/src/routes/(app)/admin/stocklist/stockchange/+page.svelte +++ b/src/routes/(app)/admin/stocklist/stockchange/+page.svelte @@ -1,15 +1,16 @@ @@ -34,40 +38,126 @@ -{#if $form.inOut != ""} -
-

- {$form.inOut == "IN" ? "Skriv in" : "Skriv ut"} -

-
- - + + + + + +
+ +
+ +
+ {:else} +
+

Skriv ut

+
- - -
- -
-
-
+ + + {#if selectedDrinkItem} + {#if selectedDrinkItem!.quantityType === DrinkQuantityType.COUNTS} + i.drinkItemId === selectedDrinkItem!.id).reduce((sum, i) => sum + i.quantityIn!, 0) - data.entriesOut.filter((i) => i.drinkItemId === selectedDrinkItem!.id).reduce((sum, i) => sum + i.quantityOut!, 0)}`} + /> + + + + + + {:else} + i.drinkItemId === selectedDrinkItem!.id).reduce((sum, i) => sum + i.quantityIn!, 0) - data.entriesOut.filter((i) => i.drinkItemId === selectedDrinkItem!.id).reduce((sum, i) => sum + i.quantityOut!, 0)}`} + /> + + + + + + {/if} + {/if} + +
+ +
+ + + {/if} {/if} diff --git a/src/routes/(app)/admin/stocklist/treasury/+page.server.ts b/src/routes/(app)/admin/stocklist/treasury/+page.server.ts new file mode 100644 index 000000000..4454773fa --- /dev/null +++ b/src/routes/(app)/admin/stocklist/treasury/+page.server.ts @@ -0,0 +1,122 @@ +import type { Actions, PageServerLoad } from "../$types"; +import { getTotalInventoryValue } from "$lib/utils/getTotalInventoryValue"; +import { DrinkQuantityType, type Prisma } from "@prisma/client"; +import { z } from "zod"; +import { fail, message, superValidate } from "sveltekit-superforms"; +import { zod } from "sveltekit-superforms/adapters"; +import { redirect } from "@sveltejs/kit"; +import dayjs from "dayjs"; + +type DrinkItemBatchWithItem = Prisma.DrinkItemBatchGetPayload<{ + include: { item: true }; +}>; + +const deleteSchema = z.object({ + id: z.string(), +}); + +const dateSchema = z.object({ + date: z.string().nullable(), +}); + +export const load: PageServerLoad = async (event) => { + const { prisma } = event.locals; + const date = event.url.searchParams.get("date"); + const deleteForm = await superValidate(zod(deleteSchema)); + const dateForm = await superValidate({ date: date ?? null }, zod(dateSchema)); + + let entries; + + if (date) { + entries = await prisma.drinkItemBatch.findMany({ + where: { + date: { lte: new Date(date) }, + }, + include: { item: true }, + orderBy: { date: "desc" }, + }); + } else { + entries = await prisma.drinkItemBatch.findMany({ + include: { item: true }, + orderBy: { date: "desc" }, + }); + } + + type DrinkItemBatchWithItem = Prisma.DrinkItemBatchGetPayload<{ + include: { item: true }; + }>; + + const entriesOnDate = + date == null + ? entries + : entries.filter((i) => dayjs(i.date).format("YYYY-MM-DD") === date!); + + const entriesToDate = entries; + + const totalInventoryValue = + date == null + ? (await getTotalInventoryValue(prisma)).totalInventoryValue / 100 + : totalInventoryValueToDate(entriesToDate); + + function totalInventoryValueToDate(entriesToDate: DrinkItemBatchWithItem[]) { + let localTotalInventoryValue = 0; + for (const row of entriesToDate) { + if (row.item.quantityType === DrinkQuantityType.WEIGHT) { + const fullRealWeight = + row.item.bottleFullWeight! - row.item.bottleEmptyWeight!; + const pricePerWeight = row.item.price / fullRealWeight; + + if (row.quantityIn === null) { + const realWeight = + (row.quantityOut ?? 0) - row.item.bottleEmptyWeight!; + localTotalInventoryValue -= pricePerWeight * realWeight; + } else { + const realWeight = + (row.quantityIn ?? 0) - row.item.bottleEmptyWeight!; + localTotalInventoryValue += pricePerWeight * realWeight; + } + } else { + if (row.quantityIn === null) { + localTotalInventoryValue -= row.quantityOut! * row.item.price; + } else { + localTotalInventoryValue += row.quantityIn! * row.item.price; + } + } + } + return localTotalInventoryValue / 100; + } + + return { + entriesToDate, + entriesOnDate, + totalInventoryValue, + deleteForm, + dateForm, + }; +}; + +export const actions: Actions = { + deleteEntry: async (event) => { + const { prisma, user } = event.locals; + + const form = await superValidate(event.request, zod(deleteSchema)); + if (!form.valid) return fail(400, { form }); + + await prisma.drinkItemBatch.delete({ + where: { id: form.data.id }, + }); + + return message(form, { message: `Batch borttagen` }); + }, + + redirectDate: async (event) => { + const form = await superValidate(event.request, zod(dateSchema)); + + if (!form.valid) return fail(400, { form }); + + const date = dayjs(form.data.date).format("YYYY-MM-DD"); + event.url.searchParams.set("date", date); + event.url.searchParams.delete("/redirectDate"); + redirect(302, event.url); + }, +}; diff --git a/src/routes/(app)/admin/stocklist/treasury/+page.svelte b/src/routes/(app)/admin/stocklist/treasury/+page.svelte new file mode 100644 index 000000000..277a199bb --- /dev/null +++ b/src/routes/(app)/admin/stocklist/treasury/+page.svelte @@ -0,0 +1,108 @@ + + +
+ +
+ dateFormElement.submit()} + /> +
+

+ Totalt lagervärde: {data.totalInventoryValue} kr +

+
+
+ + + + + + + + + + + + + + {#each data.entriesOnDate as entry} + + + + + + + + + + {/each} + +
Datum NamnAnta/Vikt InAntal/Vikt UtAnvändareÄndraTa bort
{entry.date.toDateString()}{entry.item.name}{entry.quantityIn ?? 0}{entry.quantityOut ?? 0}{entry.user} + +
+ +
+
+ +
+ + +
+
+
diff --git a/src/routes/(app)/api/admin/stocklist/+server.ts b/src/routes/(app)/api/admin/stocklist/+server.ts new file mode 100644 index 000000000..200968c9e --- /dev/null +++ b/src/routes/(app)/api/admin/stocklist/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from "@sveltejs/kit"; + +export const DELETE: RequestHandler = async ({ url, locals }) => { + const { prisma } = locals; + + const id = url.searchParams.get("id")!; + + await prisma.drinkItemBatch.delete({ + where: { id: id }, + }); + + return new Response(null, { status: 204 }); +}; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index c6706ae89..bb457ac03 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -213,6 +213,13 @@ export const getRoutes = (): Route[] => accessRequired: apiNames.MARKDOWN.CREATE, appBehaviour: "home-link", }, + { + title: m.stocklist(), + path: "/admin/stocklist", + icon: "i-mdi-text-box-edit", + accessRequired: apiNames.DRINKITEM.READ, + appBehaviour: "home-link", + }, ], }, ] as const; diff --git a/src/translations/en.json b/src/translations/en.json index 0b7bce75c..c9e86a7c8 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -844,5 +844,6 @@ "create_expense": "Create", "pub_example": "Food to pub", "add_receipt": "Add receipt", - "add_row": "Add row" + "add_row": "Add row", + "stocklist": "Stocklist" } diff --git a/src/translations/sv.json b/src/translations/sv.json index 3dcaaca19..3f674d19e 100644 --- a/src/translations/sv.json +++ b/src/translations/sv.json @@ -840,5 +840,6 @@ "create_expense": "Skapa", "pub_example": "Mat till pub", "add_receipt": "Lägg till kvitto", - "add_row": "Lägg till rad" + "add_row": "Lägg till rad", + "stocklist": "Stocklist" } From ca5058cce2b017db439386baf981138d0c7cf00d Mon Sep 17 00:00:00 2001 From: Casper Jensen Date: Mon, 15 Dec 2025 20:38:55 +0100 Subject: [PATCH 4/6] Big changes made --- .../migrations/20251215180520_/migration.sql | 2 + src/database/prisma/schema.prisma | 1 + src/database/schema.zmodel | 1 + src/database/seed/.snaplet/dataModel.json | 14 ++ src/lib/utils/getTotalInventoryValue.ts | 13 +- src/routes/(app)/admin/stocklist/+page.svelte | 11 +- .../admin/stocklist/addproduct/+page.svelte | 5 +- .../stocklist/showproducts/+page.server.ts | 45 ++++ .../admin/stocklist/showproducts/+page.svelte | 97 ++++++++ .../stocklist/stockchange/+page.server.ts | 18 +- .../admin/stocklist/stockchange/+page.svelte | 88 +++++-- .../admin/stocklist/treasury/+page.server.ts | 45 +++- .../admin/stocklist/treasury/+page.svelte | 222 +++++++++++++----- 13 files changed, 478 insertions(+), 84 deletions(-) create mode 100644 src/database/prisma/migrations/20251215180520_/migration.sql create mode 100644 src/routes/(app)/admin/stocklist/showproducts/+page.server.ts create mode 100644 src/routes/(app)/admin/stocklist/showproducts/+page.svelte diff --git a/src/database/prisma/migrations/20251215180520_/migration.sql b/src/database/prisma/migrations/20251215180520_/migration.sql new file mode 100644 index 000000000..5c5c8cef3 --- /dev/null +++ b/src/database/prisma/migrations/20251215180520_/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "drinkitembatch" ADD COLUMN "nrBottles" INTEGER; diff --git a/src/database/prisma/schema.prisma b/src/database/prisma/schema.prisma index 171fbbb49..2af7461af 100644 --- a/src/database/prisma/schema.prisma +++ b/src/database/prisma/schema.prisma @@ -479,6 +479,7 @@ model DrinkItemBatch { quantityOut Int? user String date DateTime + nrBottles Int? @@map("drinkitembatch") } diff --git a/src/database/schema.zmodel b/src/database/schema.zmodel index e5e2b2589..f1707f5ea 100644 --- a/src/database/schema.zmodel +++ b/src/database/schema.zmodel @@ -567,6 +567,7 @@ model DrinkItemBatch { quantityOut Int? user String date DateTime + nrBottles Int? @@allow("read", true) @@allow("create", has(auth().policies, "drinkitembatch:create")) diff --git a/src/database/seed/.snaplet/dataModel.json b/src/database/seed/.snaplet/dataModel.json index 589a7e666..ef6f08bd2 100644 --- a/src/database/seed/.snaplet/dataModel.json +++ b/src/database/seed/.snaplet/dataModel.json @@ -4017,6 +4017,20 @@ "isId": false, "maxLength": null }, + { + "id": "public.drinkitembatch.nrBottles", + "name": "nrBottles", + "columnName": "nrBottles", + "type": "int4", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, { "name": "drinkitem", "type": "drinkitem", diff --git a/src/lib/utils/getTotalInventoryValue.ts b/src/lib/utils/getTotalInventoryValue.ts index 18093fb71..7238a08f9 100644 --- a/src/lib/utils/getTotalInventoryValue.ts +++ b/src/lib/utils/getTotalInventoryValue.ts @@ -15,6 +15,15 @@ export async function getTotalInventoryValue(prisma: PrismaClient) { (existing.quantityIn ?? 0) + (batch.quantityIn ?? 0); existing.quantityOut = (existing.quantityOut ?? 0) + (batch.quantityOut ?? 0); + + if (batch.item.quantityType === DrinkQuantityType.WEIGHT) { + const isBatchIn = batch.quantityIn != null; + if (isBatchIn) { + existing.nrBottles = existing.nrBottles! + batch.nrBottles!; + } else { + existing.nrBottles = existing.nrBottles! - batch.nrBottles!; + } + } } else { groupedMap.set(batch.item.id, { ...batch }); } @@ -25,7 +34,9 @@ export async function getTotalInventoryValue(prisma: PrismaClient) { const totalInventoryValue = grouped.reduce((sum, i) => { if (i.item.quantityType === DrinkQuantityType.WEIGHT) { const realWeight = - (i.quantityIn ?? 0) - (i.quantityOut ?? 0) - i.item.bottleEmptyWeight!; + (i.quantityIn ?? 0) - + (i.quantityOut ?? 0) - + i.item.bottleEmptyWeight! * i.nrBottles!; const fullRealWeight = i.item.bottleFullWeight! - i.item.bottleEmptyWeight!; const pricePerWeight = i.item.price / fullRealWeight; diff --git a/src/routes/(app)/admin/stocklist/+page.svelte b/src/routes/(app)/admin/stocklist/+page.svelte index 48594f554..84de8a7a1 100644 --- a/src/routes/(app)/admin/stocklist/+page.svelte +++ b/src/routes/(app)/admin/stocklist/+page.svelte @@ -31,7 +31,10 @@ - + + + + @@ -65,11 +68,7 @@ {item.item.systembolagetID} {item.item.name} {item.item.price / 100} kr - {(item.quantityIn ?? 0) - - (item.quantityOut ?? 0) - - item.item.bottleEmptyWeight!} g + {(item.quantityIn ?? 0) - (item.quantityOut ?? 0)} g {item.item.group} {/if} diff --git a/src/routes/(app)/admin/stocklist/addproduct/+page.svelte b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte index 5ea0c3c81..ffb9569b8 100644 --- a/src/routes/(app)/admin/stocklist/addproduct/+page.svelte +++ b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte @@ -26,7 +26,10 @@ - + + + + diff --git a/src/routes/(app)/admin/stocklist/showproducts/+page.server.ts b/src/routes/(app)/admin/stocklist/showproducts/+page.server.ts new file mode 100644 index 000000000..45c08fc36 --- /dev/null +++ b/src/routes/(app)/admin/stocklist/showproducts/+page.server.ts @@ -0,0 +1,45 @@ +import { zod } from "sveltekit-superforms/adapters"; +import type { Actions, PageServerLoad } from "../$types"; +import { z } from "zod"; +import { fail, message, superValidate } from "sveltekit-superforms"; +import { Prisma } from "@prisma/client"; + +const deleteSchema = z.object({ + id: z.string(), +}); + +export const load: PageServerLoad = async ({ locals }) => { + const { prisma, user } = locals; + + const drinkItems = await prisma.drinkItem.findMany() + const deleteForm = await superValidate(zod(deleteSchema)); + + return { drinkItems, deleteForm }; +}; + + + +export const actions: Actions = { + deleteEntry: async (event) => { + const { prisma, user } = event.locals; + + const form = await superValidate(event.request, zod(deleteSchema)); + if (!form.valid) return fail(400, { form }); + + try { + await prisma.drinkItem.delete({ + where: { id: form.data.id }, + }); + } catch (error) { + return message(form, {message: `Produkt finns i lager`}) + + } + + + + + + return message(form, { message: `Produkt borttagen` }); + } + +} \ No newline at end of file diff --git a/src/routes/(app)/admin/stocklist/showproducts/+page.svelte b/src/routes/(app)/admin/stocklist/showproducts/+page.svelte new file mode 100644 index 000000000..32d087f9d --- /dev/null +++ b/src/routes/(app)/admin/stocklist/showproducts/+page.svelte @@ -0,0 +1,97 @@ + + + +
+ + + + + + + + + + + + + + {#each data.drinkItems as item} + {#if item.quantityType === DrinkQuantityType.COUNTS} + + + + + + + + + + {:else} + + + + + + + + + + {/if} + {/each} + +
Id NamnPrisGruppVikt TomVikt FullTa bort
{item.systembolagetID}{item.name}{item.price / 100} kr{item.group}-- +
+ + +
+
{item.systembolagetID}{item.name}{item.price / 100} kr{item.group}{item.bottleEmptyWeight}{item.bottleFullWeight} +
+ + +
+
+
diff --git a/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts b/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts index 086ccafef..dd10f91a9 100644 --- a/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts +++ b/src/routes/(app)/admin/stocklist/stockchange/+page.server.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import type { PageServerLoad } from "./$types"; import apiNames from "$lib/utils/apiNames"; import { authorize } from "$lib/utils/authorization"; +import dayjs from "dayjs"; export const load: PageServerLoad = async (event) => { const { prisma } = event.locals; @@ -30,16 +31,21 @@ const DrinkItemBatchSchema = z.object({ quantityOut: z.number().nonnegative(), inOut: z.string(), quantityIn: z.number().nonnegative(), - date: z.coerce.date().default(() => new Date()), + date: z + .string() + .date() + .default(() => new Date().toLocaleDateString("se-SE")), + nrBottles: z.number().nonnegative(), }); export const actions: Actions = { createDrinkItemBatch: async (event) => { const { user, prisma } = event.locals; - authorize(apiNames.DRINKITEMBATCH.CREATE, user); const form = await superValidate(event.request, zod(DrinkItemBatchSchema)); - if (!form.valid) return fail(400, { form }); + if (!form.valid) { + return message(form, { message: "inkorrekt värde" }); + } if (form.data.inOut == "IN") { if (form.data.quantityIn === 0) { @@ -49,7 +55,8 @@ export const actions: Actions = { data: { drinkItemId: form.data.drinkItemId, quantityIn: form.data.quantityIn, - date: form.data.date, + date: dayjs(form.data.date).toDate(), + nrBottles: form.data.nrBottles, user: user.studentId!, }, }); @@ -90,7 +97,8 @@ export const actions: Actions = { data: { drinkItemId: form.data.drinkItemId, quantityOut: form.data.quantityOut, - date: form.data.date, + date: dayjs(form.data.date).toDate(), + nrBottles: form.data.nrBottles, user: user.studentId!, }, }); diff --git a/src/routes/(app)/admin/stocklist/stockchange/+page.svelte b/src/routes/(app)/admin/stocklist/stockchange/+page.svelte index 3c0edc336..6146016eb 100644 --- a/src/routes/(app)/admin/stocklist/stockchange/+page.svelte +++ b/src/routes/(app)/admin/stocklist/stockchange/+page.svelte @@ -4,16 +4,21 @@ import Input from "$lib/components/Input.svelte"; import Labeled from "$lib/components/Labeled.svelte"; import { DrinkQuantityType, type DrinkItem } from "@prisma/client"; + import FormDateInput from "$lib/components/forms/FormDateInput.svelte"; + import dayjs from "dayjs"; const { data } = $props(); const { form, enhance } = superForm(data.form); + console.log($form.date); let selectedDrinkItem: DrinkItem | undefined = $derived( data.drinks.filter((drink) => drink.id === $form.drinkItemId).pop(), ); -
+ @@ -63,20 +71,60 @@ {/each} - - + + {#if selectedDrinkItem} + {#if selectedDrinkItem!.quantityType === DrinkQuantityType.COUNTS} + i.drinkItemId === selectedDrinkItem!.id).reduce((sum, i) => sum + i.quantityIn!, 0) - data.entriesOut.filter((i) => i.drinkItemId === selectedDrinkItem!.id).reduce((sum, i) => sum + i.quantityOut!, 0)}`} + /> + + + + + + {:else} + i.drinkItemId === selectedDrinkItem!.id).reduce((sum, i) => sum + i.quantityIn!, 0) - data.entriesOut.filter((i) => i.drinkItemId === selectedDrinkItem!.id).reduce((sum, i) => sum + i.quantityOut!, 0)}`} + /> + + + + + + + + {/if} + {/if}
+ + { const { prisma } = event.locals; + const drinks = await prisma.drinkItem.findMany(); const date = event.url.searchParams.get("date"); const deleteForm = await superValidate(zod(deleteSchema)); + const updateForm = await superValidate(zod(updateSchema)); const dateForm = await superValidate({ date: date ?? null }, zod(dateSchema)); let entries; @@ -68,11 +78,13 @@ export const load: PageServerLoad = async (event) => { if (row.quantityIn === null) { const realWeight = - (row.quantityOut ?? 0) - row.item.bottleEmptyWeight!; + row.quantityOut! - row.item.bottleEmptyWeight! * row.nrBottles!; + console.log(realWeight); localTotalInventoryValue -= pricePerWeight * realWeight; } else { const realWeight = - (row.quantityIn ?? 0) - row.item.bottleEmptyWeight!; + (row.quantityIn ?? 0) - + row.item.bottleEmptyWeight! * row.nrBottles!; localTotalInventoryValue += pricePerWeight * realWeight; } } else { @@ -91,7 +103,9 @@ export const load: PageServerLoad = async (event) => { entriesOnDate, totalInventoryValue, deleteForm, + updateForm, dateForm, + drinks, }; }; @@ -109,11 +123,38 @@ export const actions: Actions = { return message(form, { message: `Batch borttagen` }); }, + updateEntry: async ({ request, locals }) => { + const { prisma } = locals; + const form = await superValidate(request, zod(updateSchema)); + + if (!form.valid) return fail(400, { form }); + + try { + await prisma.drinkItemBatch.update({ + where: { id: form.data.id }, + data: { + date: new Date(form.data.date), + drinkItemId: form.data.drinkItemId, + quantityIn: form.data.quantityIn, + quantityOut: form.data.quantityOut, + }, + }); + } catch (err) { + return fail(500, { message: "Failed to update" }); + } + + redirect(302, request.url); + }, + redirectDate: async (event) => { const form = await superValidate(event.request, zod(dateSchema)); if (!form.valid) return fail(400, { form }); + if (form.data.date === null) { + redirect(302, "treasury"); + } + const date = dayjs(form.data.date).format("YYYY-MM-DD"); event.url.searchParams.set("date", date); event.url.searchParams.delete("/redirectDate"); diff --git a/src/routes/(app)/admin/stocklist/treasury/+page.svelte b/src/routes/(app)/admin/stocklist/treasury/+page.svelte index 277a199bb..8bc9846f1 100644 --- a/src/routes/(app)/admin/stocklist/treasury/+page.svelte +++ b/src/routes/(app)/admin/stocklist/treasury/+page.svelte @@ -1,17 +1,34 @@
-
- dateFormElement.submit()} - /> -

- Totalt lagervärde: {data.totalInventoryValue} kr + Totalt lagervärde: {( + Math.floor(data.totalInventoryValue * 100) / 100 + ).toFixed(2)} kr

- Datum + Datum Namn Anta/Vikt In Antal/Vikt Ut @@ -67,41 +91,133 @@ Ta bort + {#each data.entriesOnDate as entry} - - {entry.date.toDateString()} - {entry.item.name} - {entry.quantityIn ?? 0} - {entry.quantityOut ?? 0} - {entry.user} - - -
- -
- - - -
- + {@const formId = `form-${entry.id}`} + + {#if editingId === entry.id} + + + { + invalidateAll(); + }, + }} + > + +
+ + + + + + + + + + + + + + + + + {entry.user} + + + - - - + + + + + + + {:else} + + {new Date(entry.date).toLocaleDateString("sv-SE")} + + {entry.item.name} + + {entry.quantityIn ?? 0} + + {entry.quantityOut ?? 0} + + {entry.user} + + + + + + +
+ + +
+ + + {/if} {/each} From 2e829d96f03505cf91306e773dbc4c894afd1a23 Mon Sep 17 00:00:00 2001 From: Casper Jensen Date: Wed, 17 Dec 2025 17:13:48 +0100 Subject: [PATCH 5/6] Fixa buggar kring edit och borttagning av batches --- src/lib/utils/getTotalInventoryValue.ts | 6 +- .../admin/stocklist/treasury/+page.server.ts | 90 ++++++++++++++++++- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/lib/utils/getTotalInventoryValue.ts b/src/lib/utils/getTotalInventoryValue.ts index 7238a08f9..1c9c4621c 100644 --- a/src/lib/utils/getTotalInventoryValue.ts +++ b/src/lib/utils/getTotalInventoryValue.ts @@ -1,7 +1,9 @@ import { DrinkQuantityType } from "@prisma/client"; -import type { PrismaClient } from "@prisma/client"; +import type { Prisma, PrismaClient } from "@prisma/client"; -export async function getTotalInventoryValue(prisma: PrismaClient) { +export async function getTotalInventoryValue( + prisma: PrismaClient | Prisma.TransactionClient, +) { const items = await prisma.drinkItemBatch.findMany({ include: { item: true }, }); diff --git a/src/routes/(app)/admin/stocklist/treasury/+page.server.ts b/src/routes/(app)/admin/stocklist/treasury/+page.server.ts index d846101db..9443a5189 100644 --- a/src/routes/(app)/admin/stocklist/treasury/+page.server.ts +++ b/src/routes/(app)/admin/stocklist/treasury/+page.server.ts @@ -112,13 +112,52 @@ export const load: PageServerLoad = async (event) => { export const actions: Actions = { deleteEntry: async (event) => { const { prisma, user } = event.locals; - const form = await superValidate(event.request, zod(deleteSchema)); + if (!form.valid) return fail(400, { form }); - await prisma.drinkItemBatch.delete({ - where: { id: form.data.id }, - }); + try { + await prisma.$transaction(async (tx) => { + await tx.drinkItemBatch.delete({ + where: { id: form.data.id }, + }); + + const inventoryResult = await getTotalInventoryValue(tx); + + if (inventoryResult.totalInventoryValue < 0) { + throw new Error("INVENTORY_NEGATIVE"); + } + + const productStock = new Map(); + + for (const batch of inventoryResult.grouped) { + const itemId = batch.item.id; + + const quantity = (batch.quantityIn ?? 0) - (batch.quantityOut ?? 0); + + const currentTotal = productStock.get(itemId) || 0; + productStock.set(itemId, currentTotal + quantity); + } + + for (const [itemId, stock] of productStock) { + if (stock < 0) { + throw new Error("PRODUCT_NEGATIVE"); + } + } + }); + } catch (error) { + if (error instanceof Error && error.message === "INVENTORY_NEGATIVE") { + return message(form, { message: `Totalvärde blir negativt` }); + } else if ( + error instanceof Error && + error.message === "PRODUCT_NEGATIVE" + ) { + return message(form, { message: `Mängd av produkt blir negativt` }); + } + + console.error(error); + return fail(500, { form }); + } return message(form, { message: `Batch borttagen` }); }, @@ -129,6 +168,49 @@ export const actions: Actions = { if (!form.valid) return fail(400, { form }); + try { + await prisma.$transaction(async (tx) => { + await tx.drinkItemBatch.delete({ + where: { id: form.data.id }, + }); + + const inventoryResult = await getTotalInventoryValue(tx); + + if (inventoryResult.totalInventoryValue < 0) { + throw new Error("INVENTORY_NEGATIVE"); + } + + const productStock = new Map(); + + for (const batch of inventoryResult.grouped) { + const itemId = batch.item.id; + + const quantity = (batch.quantityIn ?? 0) - (batch.quantityOut ?? 0); + + const currentTotal = productStock.get(itemId) || 0; + productStock.set(itemId, currentTotal + quantity); + } + + for (const [itemId, stock] of productStock) { + if (stock < 0) { + throw new Error("PRODUCT_NEGATIVE"); + } + } + }); + } catch (error) { + if (error instanceof Error && error.message === "INVENTORY_NEGATIVE") { + return message(form, { message: `Totalvärde blir negativt` }); + } else if ( + error instanceof Error && + error.message === "PRODUCT_NEGATIVE" + ) { + return message(form, { message: `Mängd av produkt blir negativt` }); + } + + console.error(error); + return fail(500, { form }); + } + try { await prisma.drinkItemBatch.update({ where: { id: form.data.id }, From 8fdabf9b70ce86ad314a4343780b755e937b8b1d Mon Sep 17 00:00:00 2001 From: "loke.bahr.2643" Date: Wed, 17 Dec 2025 20:39:02 +0100 Subject: [PATCH 6/6] Switched to tailwind from inline style= and added Header for stocklist --- src/routes/(app)/admin/stocklist/+page.svelte | 28 +------ .../(app)/admin/stocklist/StocklistNav.svelte | 46 +++++++++++ .../admin/stocklist/addproduct/+page.svelte | 21 +---- .../admin/stocklist/showproducts/+page.svelte | 29 +------ .../admin/stocklist/stockchange/+page.svelte | 23 +----- .../admin/stocklist/treasury/+page.svelte | 76 ++++++------------- 6 files changed, 81 insertions(+), 142 deletions(-) create mode 100644 src/routes/(app)/admin/stocklist/StocklistNav.svelte diff --git a/src/routes/(app)/admin/stocklist/+page.svelte b/src/routes/(app)/admin/stocklist/+page.svelte index 84de8a7a1..8f567050c 100644 --- a/src/routes/(app)/admin/stocklist/+page.svelte +++ b/src/routes/(app)/admin/stocklist/+page.svelte @@ -3,6 +3,7 @@ import SetPageTitle from "$lib/components/nav/SetPageTitle.svelte"; import SEO from "$lib/seo/SEO.svelte"; import { DrinkQuantityType } from "@prisma/client"; + import StocklistNav from "./StocklistNav.svelte"; export let data: PageData; @@ -16,32 +17,7 @@ }} /> -
- -

- Totalt lagervärde: {data.totalInventoryValue / 100} kr -

-
+
diff --git a/src/routes/(app)/admin/stocklist/StocklistNav.svelte b/src/routes/(app)/admin/stocklist/StocklistNav.svelte new file mode 100644 index 000000000..0fef3bf71 --- /dev/null +++ b/src/routes/(app)/admin/stocklist/StocklistNav.svelte @@ -0,0 +1,46 @@ + + +
+
+ + {#if showDateInput && dateValue !== undefined && onDateChange} + onDateChange?.(e.currentTarget.value)} + /> + {/if} +
+ {#if totalInventoryValue !== undefined} +

+ Totalt lagervärde: {totalInventoryValue / 100} kr +

+ {/if} +
diff --git a/src/routes/(app)/admin/stocklist/addproduct/+page.svelte b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte index ffb9569b8..72b53e892 100644 --- a/src/routes/(app)/admin/stocklist/addproduct/+page.svelte +++ b/src/routes/(app)/admin/stocklist/addproduct/+page.svelte @@ -4,6 +4,7 @@ import { superForm } from "$lib/utils/client/superForms"; import type { PageData } from "./$types"; import Labeled from "$lib/components/Labeled.svelte"; + import StocklistNav from "../StocklistNav.svelte"; const drinkGroup = Object.values(DrinkGroup); @@ -14,25 +15,7 @@ }); - +
@@ -54,7 +33,7 @@ - -
{item.group} - - +
{item.group}
{item.bottleEmptyWeight} {item.bottleFullWeight} + - +
dateFormElement.submit()} - /> - -
-

- Totalt lagervärde: {( - Math.floor(data.totalInventoryValue * 100) / 100 - ).toFixed(2)} kr -

- -
{ + $dateForm.date = date; + dateFormElement.submit(); + }} +/> + + + +
@@ -88,7 +62,7 @@ - + @@ -103,7 +77,7 @@ method="POST" action="?/updateEntry" id={formId} - style="display:none" + class="hidden" use:updateFormEnhance={{ onResult: ({ result }) => { invalidateAll(); @@ -173,7 +147,7 @@ - -
Antal/Vikt Ut Användare ÄndraTa bortTa bort
+ +