From b4f7fb5e4d6e2335730bb2f19d5f840c8ea05a22 Mon Sep 17 00:00:00 2001 From: afffe Date: Sat, 17 Oct 2026 21:06:21 +0200 Subject: [PATCH 01/22] Split money requests between several users - Request money from multiple users at once (atomic, one transaction per user) - Split by factors, percentages or absolute amounts; unset shares are calculated automatically and can be reset per person - Optionally include yourself in the split (your share isn't requested) - Equal card heights on the split page Co-Authored-By: Claude Opus 5.5 --- src/components/PageComponents/GetMoney.tsx | 314 +++++++++++++++++---- src/helper/generalFunctions.test.ts | 35 +++ src/helper/generalFunctions.ts | 32 +++ src/pages/split.tsx | 4 +- src/server/api/routers/transactions.ts | 63 +++-- 5 files changed, 370 insertions(+), 78 deletions(-) create mode 100644 src/helper/generalFunctions.test.ts diff --git a/src/components/PageComponents/GetMoney.tsx b/src/components/PageComponents/GetMoney.tsx index 83252a2..5d71f0f 100644 --- a/src/components/PageComponents/GetMoney.tsx +++ b/src/components/PageComponents/GetMoney.tsx @@ -1,10 +1,11 @@ import { useSession } from "next-auth/react" import { useRef, useState } from "react" -import { AlertCircle, ArrowDown } from "lucide-react" +import { AlertCircle, ArrowDown, RotateCcw, X } from "lucide-react" import ActionResponsePopup, { animate, type AnimationHandle, } from "~/components/General/ActionResponsePopup" +import { calculateSplit, type SplitMode } from "~/helper/generalFunctions" import { api } from "~/utils/api" type Props = { @@ -12,6 +13,15 @@ type Props = { sendDescription?: string } +// share === undefined: calculated automatically +type Participant = { userId: string; share?: number } + +const splitModes: { mode: SplitMode; label: string; unit: string }[] = [ + { mode: "factor", label: "Faktoren", unit: "×" }, + { mode: "percent", label: "Prozent", unit: "%" }, + { mode: "absolute", label: "Beträge", unit: "€" }, +] + const GetMoney = (props: Props) => { const allUserRequest = api.user.getAllUsers.useQuery() const animationRef = useRef(null) @@ -19,41 +29,177 @@ const GetMoney = (props: Props) => { const trpcUtils = api.useUtils() const session = useSession() - const [amountSend, setAmountSend] = useState(1) + const [totalAmount, setTotalAmount] = useState(0) + const [splitMode, setSplitMode] = useState("factor") + const [includeSelf, setIncludeSelf] = useState(true) + const [selfShare, setSelfShare] = useState() + const [participants, setParticipants] = useState([]) const [noteSend, setNoteSend] = useState(props.comment || "") - const [selectedUser, setSelectedUser] = useState() - const [errorMessage, setErrorMessage] = useState("") const apiRetractMoney = api.transaction.retractMoney.useMutation() + // self (if included) is always the last share + const enteredShares = [...participants.map((p) => p.share), ...(includeSelf ? [selfShare] : [])] + const { shares, amounts } = calculateSplit(totalAmount, enteredShares, splitMode) + // with only fixed amounts, the total is their sum - otherwise it stays as entered + const allAmountsFixed = + splitMode === "absolute" && enteredShares.length > 0 && enteredShares.every((share) => share !== undefined) + const effectiveTotal = allAmountsFixed ? amounts.reduce((sum, amount) => sum + amount, 0) : totalAmount + const participantAmounts = amounts.slice(0, participants.length) + const requestedAmount = participantAmounts.reduce((sum, amount) => sum + amount, 0) + const percentSum = shares.reduce((sum, share) => sum + share, 0) + const unit = splitModes.find((m) => m.mode === splitMode)?.unit + + // called before every change of the shares: while all amounts are fixed, the total is their sum - + // store it, so the total stays put once an amount becomes automatic again + const keepTotal = () => { + if (allAmountsFixed) setTotalAmount(effectiveTotal) + } + + const getUserName = (userId: string) => allUserRequest.data?.find((u) => u.id === userId)?.name + + const validationError = (() => { + if (participants.length === 0) return "Bitte wähle mindestens einen Nutzer aus" + if (!(effectiveTotal > 0)) return "Bitte gib einen Gesamtbetrag an" + if (splitMode === "percent" && shares.some((share) => share < 0)) + return "Die festen Prozente ergeben mehr als 100%" + if (splitMode === "percent" && Math.abs(percentSum - 100) > 0.001) + return `Die Prozente müssen 100% ergeben (aktuell ${+percentSum.toFixed(2)}%) - setze einen Wert auf automatisch zurück` + if (splitMode === "absolute" && shares.some((share) => share < 0)) + return "Die festen Beträge übersteigen den Gesamtbetrag" + if (participantAmounts.some((amount) => !(amount > 0))) + return "Jeder ausgewählte Nutzer braucht einen Anteil größer 0" + return undefined + })() + + const changeSplitMode = (mode: SplitMode) => { + if (mode === splitMode) return + // keep the split: fixed shares stay fixed (converted to the new mode), automatic ones stay automatic. + // All effective shares are proportional to the amounts, so their normalized weights are the same in every mode. + const shareSum = shares.reduce((sum, share) => sum + share, 0) + const weights = shares.map((share) => share / shareSum) + const autoIndex = enteredShares.findIndex((share) => share === undefined) + const factorUnit = autoIndex >= 0 ? weights[autoIndex] ?? 0 : Math.min(...weights.filter((w) => w > 0)) + const canConvert = + shareSum > 0 && + shares.every((share) => share >= 0) && + (mode !== "absolute" || effectiveTotal > 0) && + (mode !== "factor" || factorUnit > 0) + const newShares = enteredShares.map((share, i) => { + if (!canConvert || share === undefined) return undefined + const weight = weights[i] ?? 0 + return mode === "factor" ? weight / factorUnit : mode === "percent" ? weight * 100 : weight * effectiveTotal + }) + setParticipants(participants.map((p, i) => ({ ...p, share: newShares[i] }))) + setSelfShare(newShares[participants.length]) + setTotalAmount(effectiveTotal) + setSplitMode(mode) + } + + const addParticipant = (userId: string) => { + if (!userId || participants.some((p) => p.userId === userId)) return + keepTotal() + setParticipants([...participants, { userId }]) + } + + const updateShare = (userId: string, share: number | undefined) => { + keepTotal() + setParticipants(participants.map((p) => (p.userId === userId ? { ...p, share } : p))) + } + + const updateSelfShare = (share: number | undefined) => { + keepTotal() + setSelfShare(share) + } + + const removeParticipant = (userId: string) => { + keepTotal() + setParticipants(participants.filter((p) => p.userId !== userId)) + } + + const toggleIncludeSelf = (include: boolean) => { + keepTotal() + setSelfShare(undefined) + setIncludeSelf(include) + } + const getMoneyAction = async () => { - if (selectedUser) { - setErrorMessage("") - await apiRetractMoney.mutateAsync( - { - amount: amountSend, - moneySourceUserId: selectedUser, - note: noteSend, + if (validationError) return + await apiRetractMoney.mutateAsync( + { + requests: participants.map((p, i) => ({ + moneySourceUserId: p.userId, + amount: participantAmounts[i] ?? 0, + })), + note: noteSend, + }, + { + onError: (error) => { + console.error(error) + animate(animationRef, "failure", error.message) + }, + onSuccess: async () => { + animate(animationRef, "success") + setParticipants([]) + setTotalAmount(0) + setSelfShare(undefined) + setNoteSend("") + await trpcUtils.user.invalidate() }, - { - onError: (error) => { - console.error(error) - animate(animationRef, "failure", error.message) - }, - onSuccess: async () => { - animate(animationRef, "success") - setSelectedUser(undefined) - setAmountSend(1) - setNoteSend("") - await trpcUtils.user.invalidate() - }, - } - ) - } else { - setErrorMessage("Bitte wähle einen User aus") - } + } + ) } + // automatic shares show their calculated value (grey) - typing into a field fixes it, ⭮ makes it automatic again + const shareInput = (value: number | undefined, autoValue: number, onChange: (value: number | undefined) => void) => ( + <> +
+ onChange(parseFloat(e.target.value))} + /> + + {unit} + +
+ {value !== undefined ? ( + + ) : ( + + )} + + ) + + const shareRow = (key: string, name: string | null | undefined, index: number, onChange: (value: number | undefined) => void, onRemove?: () => void) => ( + // "contents": the cells are laid out by the surrounding grid, so each column fits its widest entry +
+ {name} + {shareInput(enteredShares[index], shares[index] ?? 0, onChange)} + {/* invisible for absolute amounts, so the input keeps its position */} + + {(amounts[index] ?? 0).toFixed(2)}€ + + {onRemove ? ( + + ) : ( + + )} +
+ ) + return ( <>
@@ -61,39 +207,65 @@ const GetMoney = (props: Props) => { {/* Amount Input */}
setAmountSend(parseFloat(e.target.value))} + disabled={allAmountsFixed} + onChange={(e) => setTotalAmount(parseFloat(e.target.value))} /> €
+ {allAmountsFixed && ( + + )} +
+ + {/* Split Mode */} +
+ +
+ {splitModes.map((m) => ( + + ))} +
{/* User Selection */}
+
+ {/* Shares */} + {(participants.length > 0 || includeSelf) && ( +
+ {participants.map((p, i) => + shareRow( + p.userId, + getUserName(p.userId), + i, + (value) => updateShare(p.userId, value), + () => removeParticipant(p.userId) + ) + )} + {includeSelf && + shareRow("self", `${session.data?.user.name ?? ""} (ich)`, participants.length, updateSelfShare)} +
+ )} + {/* Note Input */}
- {/* Error Message */} - {errorMessage && ( -
- - {errorMessage} -
- )} - {/* Action Button */} + {validationError && ( +
+ + {validationError} +
+ )} {/* Transaction Summary */} - {selectedUser && amountSend > 0 && ( + {!validationError && (
Übersicht:
-
- - Von {allUserRequest.data?.find(u => u.id === selectedUser)?.name} - - - {amountSend.toFixed(2)}€ - -
+ {participants.map((p, i) => ( +
+ Von {getUserName(p.userId)} + {(participantAmounts[i] ?? 0).toFixed(2)}€ +
+ ))} + {participants.length > 1 && ( +
+ Gesamt ({participants.length} Nutzer) + {requestedAmount.toFixed(2)}€ +
+ )}
)}
diff --git a/src/helper/generalFunctions.test.ts b/src/helper/generalFunctions.test.ts new file mode 100644 index 0000000..66b39ec --- /dev/null +++ b/src/helper/generalFunctions.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest" +import { calculateSplit } from "~/helper/generalFunctions" + +describe("calculateSplit", () => { + it("splits by factors (empty = 1)", () => { + expect(calculateSplit(30, [1, 1, 1], "factor").amounts).toEqual([10, 10, 10]) + expect(calculateSplit(30, [2, undefined], "factor")).toEqual({ shares: [2, 1], amounts: [20, 10] }) + }) + + it("splits by percentages, empty ones share the rest of 100%", () => { + expect(calculateSplit(50, [50, 30, 20], "percent").amounts).toEqual([25, 15, 10]) + expect(calculateSplit(50, [50, undefined, undefined], "percent")).toEqual({ + shares: [50, 25, 25], + amounts: [25, 12.5, 12.5], + }) + expect(calculateSplit(50, [undefined, undefined], "percent").shares).toEqual([50, 50]) + }) + + it("uses absolute values, empty ones share the rest of the total", () => { + expect(calculateSplit(100, [4.5, 3], "absolute").amounts).toEqual([4.5, 3]) + expect(calculateSplit(30, [10, undefined, undefined], "absolute").amounts).toEqual([10, 10, 10]) + }) + + it("returns negative automatic shares if fixed shares exceed the target", () => { + expect(calculateSplit(10, [80, 40, undefined], "percent").shares).toEqual([80, 40, -20]) + expect(calculateSplit(10, [15, undefined], "absolute").amounts).toEqual([15, -5]) + }) + + it("treats invalid shares and totals as 0", () => { + expect(calculateSplit(30, [NaN, -1, 1], "factor").amounts).toEqual([0, 0, 30]) + expect(calculateSplit(NaN, [1, 1], "factor").amounts).toEqual([0, 0]) + expect(calculateSplit(30, [0, 0], "factor").amounts).toEqual([0, 0]) + expect(calculateSplit(0, [NaN, 2], "absolute").amounts).toEqual([0, 2]) + }) +}) diff --git a/src/helper/generalFunctions.ts b/src/helper/generalFunctions.ts index 02da02b..d017e88 100644 --- a/src/helper/generalFunctions.ts +++ b/src/helper/generalFunctions.ts @@ -40,3 +40,35 @@ export function toFlatPropertyMap(obj: object, keySeparator = '.') { }; return flattenRecursive(obj); } + +export type SplitMode = "factor" | "percent" | "absolute" + +/** + * Calculates the effective share and the amount of each participant. + * Shares that are `undefined` are calculated automatically: + * factor -> 1, percent/absolute -> equal part of what's left of 100% / the total. + * Invalid (NaN/negative) shares count as 0. + */ +export const calculateSplit = (total: number, shares: (number | undefined)[], mode: SplitMode) => { + const cleanTotal = Number.isFinite(total) && total > 0 ? total : 0 + const fixedShares = shares.map((share) => + share === undefined ? undefined : Number.isFinite(share) && share > 0 ? share : 0 + ) + if (mode === "factor") { + const factors = fixedShares.map((share) => share ?? 1) + const factorSum = factors.reduce((sum, factor) => sum + factor, 0) + return { + shares: factors, + amounts: factors.map((factor) => (factorSum > 0 ? (cleanTotal * factor) / factorSum : 0)), + } + } + const target = mode === "percent" ? 100 : cleanTotal + const fixedSum = fixedShares.reduce((sum, share) => sum + (share ?? 0), 0) + const autoCount = fixedShares.filter((share) => share === undefined).length + const autoShare = autoCount > 0 ? (target - fixedSum) / autoCount : 0 // negative if fixed shares exceed the target + const effectiveShares = fixedShares.map((share) => share ?? autoShare) + return { + shares: effectiveShares, + amounts: mode === "percent" ? effectiveShares.map((share) => (cleanTotal * share) / 100) : effectiveShares, + } +} diff --git a/src/pages/split.tsx b/src/pages/split.tsx index 86e6a64..bb7edf9 100644 --- a/src/pages/split.tsx +++ b/src/pages/split.tsx @@ -39,7 +39,7 @@ const SplitPage: NextPage = () => { {/* Main Actions Grid */}
{/* Send Money Section */} -
+
@@ -55,7 +55,7 @@ const SplitPage: NextPage = () => {
{/* Get Money Section */} -
+
diff --git a/src/server/api/routers/transactions.ts b/src/server/api/routers/transactions.ts index 6358e5d..26380f6 100644 --- a/src/server/api/routers/transactions.ts +++ b/src/server/api/routers/transactions.ts @@ -201,36 +201,61 @@ export const transactionRouter = createTRPCRouter({ }), retractMoney: protectedProcedure - .input(z.object({ moneySourceUserId: id, amount: z.number(), note: z.string().optional() })) - .mutation(async ({ ctx, input }) => { - const moneySourceUser = await prisma.user.findUniqueOrThrow({ - where: { id: input.moneySourceUserId }, + .input( + z.object({ + requests: z.array(z.object({ moneySourceUserId: id, amount: z.number() })).min(1), + note: z.string().optional(), }) - if (input.amount <= 0) { + ) + .mutation(async ({ ctx, input }) => { + const sourceUserIds = input.requests.map((request) => request.moneySourceUserId) + if (new Set(sourceUserIds).size !== sourceUserIds.length) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "You can't request money from the same user twice", + }) + } + if (sourceUserIds.includes(ctx.session.user.id)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "You can't request money from yourself", + }) + } + if (input.requests.some((request) => !(request.amount > 0))) { throw new TRPCError({ code: "BAD_REQUEST", message: "You can't request a negative amount - send money instead", }) } + const moneySourceUsers = await ctx.prisma.user.findMany({ + where: { id: { in: sourceUserIds } }, + select: { id: true }, + }) + if (moneySourceUsers.length !== sourceUserIds.length) { + throw new TRPCError({ code: "NOT_FOUND", message: "User not found" }) + } + const totalAmount = input.requests.reduce((sum, request) => sum + request.amount, 0) // atomic action: await ctx.prisma.$transaction([ - ctx.prisma.transaction.create({ - data: { - user: { connect: { id: ctx.session.user.id } }, - type: 2, - moneyDestination: { connect: { id: moneySourceUser.id } }, - totalAmount: -input.amount, // send negative amount to retract money but still indicate the executing user - note: input.note, - }, - }), + ...input.requests.flatMap((request) => [ + ctx.prisma.transaction.create({ + data: { + user: { connect: { id: ctx.session.user.id } }, + type: 2, + moneyDestination: { connect: { id: request.moneySourceUserId } }, + totalAmount: -request.amount, // send negative amount to retract money but still indicate the executing user + note: input.note, + }, + }), + ctx.prisma.user.update({ + where: { id: request.moneySourceUserId }, + data: { balance: { decrement: request.amount } }, + }), + ]), ctx.prisma.user.update({ where: { id: ctx.session.user.id }, - data: { balance: { increment: input.amount } }, - }), - ctx.prisma.user.update({ - where: { id: moneySourceUser.id }, - data: { balance: { decrement: input.amount } }, + data: { balance: { increment: totalAmount } }, }), ]) }), From df054255ecc0e26a83f56bd62a90b4fb55228c63 Mon Sep 17 00:00:00 2001 From: afffe Date: Sat, 17 Oct 2026 19:44:34 +0000 Subject: [PATCH 02/22] =?UTF-8?q?Aufladen-Knopf=20im=20Header=20an=20die?= =?UTF-8?q?=20Farbe=20der=20anderen=20Nav-Kn=C3=B6pfe=20angleichen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_017yaFTPNEHGx3LenyDccnGs --- src/components/Layout/Header.tsx | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 2d20036..c1eafd6 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -40,12 +40,8 @@ export default function Header() { path: string, baseClasses = "hover:bg-primary hover:text-primary-content transition-colors duration-200 font-medium", ) => { - if (path === "/top-up") { - const activeClasses = "bg-success text-success-content" - return isActive(path) ? `${baseClasses} ${activeClasses}` : baseClasses - } - - const activeClasses = "bg-primary text-primary-content" + const activeClasses = + path === "/admin" ? "bg-warning text-warning-content" : "bg-primary text-primary-content" return isActive(path) ? `${baseClasses} ${activeClasses}` : baseClasses } @@ -70,13 +66,7 @@ export default function Header() {
  • - + Aufladen From 9a34ca5a9d33849d2bd38502e72cf7121af933c8 Mon Sep 17 00:00:00 2001 From: afffe Date: Sat, 17 Oct 2026 20:19:21 +0000 Subject: [PATCH 03/22] =?UTF-8?q?Kreditw=C3=BCrdig-Haken=20im=20User-Detai?= =?UTF-8?q?ls-Popup=20beim=20Userwechsel=20zur=C3=BCcksetzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XWPHvWv8YsGKLcJ2gfSart --- src/components/PageComponents/UserOverview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PageComponents/UserOverview.tsx b/src/components/PageComponents/UserOverview.tsx index 7426020..f814788 100644 --- a/src/components/PageComponents/UserOverview.tsx +++ b/src/components/PageComponents/UserOverview.tsx @@ -169,7 +169,7 @@ const UserOverview = () => { setSelectedUser(undefined)}> - setSelectedUser(undefined)} /> + setSelectedUser(undefined)} /> From 89190da4c64834f02c0918c6c3241e7d46e10ca0 Mon Sep 17 00:00:00 2001 From: afffe Date: Sat, 17 Oct 2026 20:25:11 +0000 Subject: [PATCH 04/22] =?UTF-8?q?Gruppenkauf-Wiederholung:=20"Stunden=20vo?= =?UTF-8?q?rher"=20speichern=20und=20im=20Cron=20ber=C3=BCcksichtigen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neues Feld ordersStartsAt (Standard 72h) an der Vorlage inkl. Migration. Der Cron legt den Gruppenkauf jetzt an, sobald die nächste Deadline höchstens ordersStartsAt Stunden entfernt ist, statt fest 3 Tage vorher. Im Formular wird der Wert als Zahl übergeben und die Wochentage beginnen mit Montag. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QpZB9UHK22rJeuAYwvXVoe --- .../migration.sql | 2 + prisma/schema.prisma | 13 +++--- .../Forms/GrouporderTemplateForm.tsx | 27 ++++++++--- src/pages/api/cron/trigger.ts | 45 ++++++++++--------- src/server/api/routers/groupOrders.ts | 1 + 5 files changed, 55 insertions(+), 33 deletions(-) create mode 100644 prisma/migrations/20261017200000_add_orders_starts_at_to_template/migration.sql diff --git a/prisma/migrations/20261017200000_add_orders_starts_at_to_template/migration.sql b/prisma/migrations/20261017200000_add_orders_starts_at_to_template/migration.sql new file mode 100644 index 0000000..91687c5 --- /dev/null +++ b/prisma/migrations/20261017200000_add_orders_starts_at_to_template/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "GroupOrderTemplate" ADD COLUMN "ordersStartsAt" INTEGER NOT NULL DEFAULT 72; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 784d0eb..eb7506d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -71,12 +71,13 @@ model GroupOrder { } model GroupOrderTemplate { - id String @id @default(cuid()) - name String - weekday Int //0:sun, 1: mon, ..., 6:sat - ordersCloseAt DateTime // only time relevant - date not important - active Boolean @default(true) - GroupOrders GroupOrder[] + id String @id @default(cuid()) + name String + weekday Int //0:sun, 1: mon, ..., 6:sat + ordersCloseAt DateTime // only time relevant - date not important + ordersStartsAt Int @default(72) // hours before ordersCloseAt when the grouporder gets created + active Boolean @default(true) + GroupOrders GroupOrder[] } model Category { diff --git a/src/components/Forms/GrouporderTemplateForm.tsx b/src/components/Forms/GrouporderTemplateForm.tsx index d88c116..63eddb7 100644 --- a/src/components/Forms/GrouporderTemplateForm.tsx +++ b/src/components/Forms/GrouporderTemplateForm.tsx @@ -13,11 +13,13 @@ export const validationSchema = z.object({ weekday: z.number().min(0).max(6), ordersCloseAt_h: z.number().nonnegative(), ordersCloseAt_min: z.number().nonnegative(), + ordersStartsAt: z.number().min(1).max(160), }) export const formSchema = z.object({ name: z.string(), weekday: z.number().min(0).max(6), ordersCloseAt: z.string(), + ordersStartsAt: z.number().min(1).max(160), }) type Props = { @@ -27,8 +29,8 @@ type Props = { const AddGrouporderTemplateForm = (props: Props) => { const trpcUtils = api.useUtils() const createGrouporderTemplate = api.groupOrders.createTemplate.useMutation() - const updateGrouporderTempate = api.groupOrders.updateTemplate.useMutation() - const deleteGrouporderTempate = api.groupOrders.deleteTemplate.useMutation() + const updateGrouporderTemplate = api.groupOrders.updateTemplate.useMutation() + const deleteGrouporderTemplate = api.groupOrders.deleteTemplate.useMutation() const currentGroupOrderTemplate = api.groupOrders.getTemplate.useQuery( { id: props.id! }, @@ -57,7 +59,7 @@ const AddGrouporderTemplateForm = (props: Props) => { ), } : {} - : { name: "", weekday: undefined, ordersCloseAt: undefined }, + : { name: "", weekday: undefined, ordersCloseAt: undefined, ordersStartsAt: 72 }, [currentGroupOrderTemplate.data, props.id], ) @@ -77,7 +79,7 @@ const AddGrouporderTemplateForm = (props: Props) => { ...data, } if (!!props.id) { - await updateGrouporderTempate.mutateAsync({ id: props.id, ...dataToSend}) + await updateGrouporderTemplate.mutateAsync({ id: props.id, ...dataToSend}) } else { await createGrouporderTemplate.mutateAsync(dataToSend) } @@ -87,7 +89,7 @@ const AddGrouporderTemplateForm = (props: Props) => { const onDelete = async () => { if (!!props.id) { - await deleteGrouporderTempate.mutateAsync({ id: props.id }) + await deleteGrouporderTemplate.mutateAsync({ id: props.id }) props.finishAction() await trpcUtils.groupOrders.invalidate() } @@ -117,9 +119,10 @@ const AddGrouporderTemplateForm = (props: Props) => { className="select select-bordered select-primary w-full max-w-xs" {...register("weekday", { required: true, valueAsNumber: true })} > - {weekdays.map((day, index) => ( + {/* show monday first - value stays the JS weekday (0: sunday) */} + {[1, 2, 3, 4, 5, 6, 0].map((index) => ( ))} @@ -134,6 +137,16 @@ const AddGrouporderTemplateForm = (props: Props) => { className="input input-primary w-full max-w-md" />
  • +
    + + +