diff --git a/package-lock.json b/package-lock.json index 1509789f..fec7a796 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "dayjs": "^1.11.10", "embla-carousel-react": "^8.0.0-rc21", "lucide-react": "^0.290.0", + "math-expression-evaluator": "^2.0.5", "nanoid": "^5.0.4", "next": "^14.2.3", "next-s3-upload": "^0.3.4", @@ -901,10 +902,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", - "license": "MIT", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -6455,6 +6455,11 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/math-expression-evaluator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-2.0.5.tgz", + "integrity": "sha512-C8Ifr3BpZ1E8ncWiHltndUkimvkzBwEiXPRyDJVxspo/G7zSN8tLHwFVfrGotvWzu2wuRkjnxaEayFblepmN5Q==" + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", diff --git a/package.json b/package.json index 17e66865..78eed291 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "dayjs": "^1.11.10", "embla-carousel-react": "^8.0.0-rc21", "lucide-react": "^0.290.0", + "math-expression-evaluator": "^2.0.5", "nanoid": "^5.0.4", "next": "^14.2.3", "next-s3-upload": "^0.3.4", diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx index 758e93d1..63b13b97 100644 --- a/src/components/expense-form.tsx +++ b/src/components/expense-form.tsx @@ -52,6 +52,9 @@ import { match } from 'ts-pattern' import { DeletePopup } from './delete-popup' import { extractCategoryFromTitle } from './expense-form-actions' import { Textarea } from './ui/textarea' +import Mexp from 'math-expression-evaluator' + +const mexp = new Mexp() export type Props = { group: NonNullable>> @@ -245,6 +248,7 @@ export function ExpenseForm({ } const [isIncome, setIsIncome] = useState(Number(form.getValues().amount) < 0) + const [evaluatedAmount, setEvaluatedAmount] = useState('0') const sExpense = isIncome ? 'income' : 'expense' const sPaid = isIncome ? 'received' : 'paid' @@ -322,47 +326,57 @@ export function ExpenseForm({ {group.currency} { - const v = enforceCurrencyPattern(event.target.value) - const income = Number(v) < 0 - setIsIncome(income) - if (income) form.setValue('isReimbursement', false) + let v = (event.target.value) + if (v === ''){ + setEvaluatedAmount('0') + } + else{ + try{ + const evaluatedValue = Number(mexp.eval(v)).toFixed(2).replace(/\.?0+$/, '') // replace trailing zeros + setEvaluatedAmount(evaluatedValue) + const income = Number(evaluatedValue) < 0 + setIsIncome(income) + if (income) form.setValue('isReimbursement', false) + } + catch{ + setEvaluatedAmount('Invalid Expression') + } + } onChange(v) }} - onFocus={(e) => { - // we're adding a small delay to get around safaris issue with onMouseUp deselecting things again - const target = e.currentTarget - setTimeout(() => target.select(), 1) - }} {...field} /> +
+
+ {' = ' + evaluatedAmount} +
+ {!isIncome && ( + ( + + + + + Reimbursement + - {!isIncome && ( - ( - - - - -
- This is a reimbursement -
-
- )} - /> - )} + )} + /> + )} +
)} /> diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 9578b025..c58b65f4 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -1,5 +1,8 @@ import { SplitMode } from '@prisma/client' import * as z from 'zod' +import Mexp from 'math-expression-evaluator' + +const mexp = new Mexp() export const groupFormSchema = z .object({ @@ -51,7 +54,7 @@ export const expenseFormSchema = z [ z.number(), z.string().transform((value, ctx) => { - const valueAsNumber = Number(value) + const valueAsNumber = Number(mexp.eval(value).toFixed(2).replace(/\.?0+$/, '')) if (Number.isNaN(valueAsNumber)) ctx.addIssue({ code: z.ZodIssueCode.custom,