Skip to content

Commit

Permalink
IITM-36 - total amount should accept decimals (#8)
Browse files Browse the repository at this point in the history
* fix(groups): group expense amount accepts decimals

* fix(transactions): using input with currency component

Fixes IITM-37.

* chore(naming): renamed variables

* chore: removed unused imports, added timezone adjustment

---------

Co-authored-by: Ricard Mallafre <[email protected]>
  • Loading branch information
github-actions[bot] and nikensss authored Mar 31, 2024
1 parent 9b30c54 commit 00ce278
Show file tree
Hide file tree
Showing 11 changed files with 84 additions and 47 deletions.
1 change: 1 addition & 0 deletions src/app/dashboard/(transactions)/transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default async function TransactionsOverview({ type, searchParams }: Trans
{transactions.map((transaction) => {
return (
<UpdateTransaction
currency={user.currency}
key={transaction.id}
transaction={transaction}
weekStartsOn={weekStartsOn}
Expand Down
16 changes: 12 additions & 4 deletions src/app/dashboard/(transactions)/update-transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { zodResolver } from '@hookform/resolvers/zod';
import { CalendarIcon } from '@radix-ui/react-icons';
import currencySymbolMap from 'currency-symbol-map/map';
import { format } from 'date-fns';
import { formatInTimeZone, zonedTimeToUtc } from 'date-fns-tz';
import { Loader2, Save, Trash2 } from 'lucide-react';
Expand All @@ -22,7 +23,7 @@ import {
DialogTrigger,
} from '~/components/ui/dialog';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '~/components/ui/form';
import { Input } from '~/components/ui/input';
import { Input, InputWithCurrency } from '~/components/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover';
import { TableCell, TableRow } from '~/components/ui/table';
import { type Tag, TagInput } from '~/components/ui/tag-input/tag-input';
Expand All @@ -31,13 +32,20 @@ import { api } from '~/trpc/react';
import type { RouterOutputs } from '~/trpc/shared';

export type UpdateTransactionProps = {
currency: string | null;
timezone: string;
weekStartsOn: number;
transaction: RouterOutputs['transactions']['personal']['period'][number];
tags: RouterOutputs['tags']['all'];
};

export default function UpdateTransaction({ timezone, weekStartsOn, transaction, tags }: UpdateTransactionProps) {
export default function UpdateTransaction({
currency,
timezone,
weekStartsOn,
transaction,
tags,
}: UpdateTransactionProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
Expand Down Expand Up @@ -136,8 +144,8 @@ export default function UpdateTransaction({ timezone, weekStartsOn, transaction,
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<Input
type="number"
<InputWithCurrency
currency={currencySymbolMap[currency ?? 'EUR'] ?? '€'}
onFocus={(e) => e.target.select()}
step={0.01}
min={0.01}
Expand Down
6 changes: 3 additions & 3 deletions src/app/dashboard/charts/charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ export default async function Charts({ month, year }: { month: string; year: str
const user = await api.users.get.query();
const timezone = user?.timezone ?? 'Europe/Amsterdam';

const time = parse(`${month}, ${year}`, 'LLLL, yyyy', new Date());
const now = parse(`${month}, ${year}`, 'LLLL, yyyy', new Date());
const preferredTimezoneOffset = getTimezoneOffset(timezone);
const localeTimezoneOffset = new Date().getTimezoneOffset() * 60 * 1000;
const from = new Date(startOfMonth(time).getTime() - preferredTimezoneOffset - localeTimezoneOffset);
const to = new Date(endOfMonth(time).getTime() - preferredTimezoneOffset - localeTimezoneOffset);
const from = new Date(startOfMonth(now).getTime() - preferredTimezoneOffset - localeTimezoneOffset);
const to = new Date(endOfMonth(now).getTime() - preferredTimezoneOffset - localeTimezoneOffset);

const [expenses, incomes] = await Promise.all([
api.transactions.personal.period.query({ type: TransactionType.EXPENSE, from, to }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ export default async function DashboardRegisterPersonalExpense() {
api.tags.all.query({ type: TransactionType.EXPENSE }),
api.users.get.query(),
]);
const weekStartsOn = user?.weekStartsOn ?? 1;
const timezone = user?.timezone ?? 'Europe/Amsterdam';
const weekStartsOn = user.weekStartsOn ?? 1;
const timezone = user.timezone ?? 'Europe/Amsterdam';

return (
<RegisterTransaction
currency={user.currency}
timezone={timezone}
weekStartsOn={weekStartsOn}
tags={tags}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ export default async function DashboardRegisterPersonalIncome() {
}),
api.users.get.query(),
]);
const weekStartsOn = user?.weekStartsOn ?? 1;
const timezone = user?.timezone ?? 'Europe/Amsterdam';
const weekStartsOn = user.weekStartsOn ?? 1;
const timezone = user.timezone ?? 'Europe/Amsterdam';

return (
<RegisterTransaction
currency={user.currency}
timezone={timezone}
weekStartsOn={weekStartsOn}
tags={tags.map((t) => ({ ...t, text: t.name }))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { zodResolver } from '@hookform/resolvers/zod';
import type { TransactionType } from '@prisma/client';
import { CalendarIcon } from '@radix-ui/react-icons';
import currencySymbolMap from 'currency-symbol-map/map';
import { format } from 'date-fns';
import { zonedTimeToUtc } from 'date-fns-tz';
import { Loader2 } from 'lucide-react';
Expand All @@ -14,14 +15,15 @@ import { Button } from '~/components/ui/button';
import { Calendar } from '~/components/ui/calendar';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '~/components/ui/dialog';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '~/components/ui/form';
import { Input } from '~/components/ui/input';
import { Input, InputWithCurrency } from '~/components/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover';
import { type Tag, TagInput } from '~/components/ui/tag-input/tag-input';
import { getRandomElement } from '~/lib/utils';
import { api } from '~/trpc/react';
import type { RouterOutputs } from '~/trpc/shared';

export type RegisterTransactionProps = {
currency: string | null;
timezone: string;
weekStartsOn: number;
descriptions: string[];
Expand All @@ -30,6 +32,7 @@ export type RegisterTransactionProps = {
};

export default function RegisterTransaction({
currency,
timezone,
weekStartsOn,
descriptions,
Expand Down Expand Up @@ -125,8 +128,8 @@ export default function RegisterTransaction({
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<Input
type="number"
<InputWithCurrency
currency={currencySymbolMap[currency ?? 'EUR'] ?? '€'}
onFocus={(e) => e.target.select()}
step={0.01}
min={0.01}
Expand Down
47 changes: 22 additions & 25 deletions src/app/groups/[groupId]/expenses/new/group-expense.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { AvatarIcon } from '@radix-ui/react-icons';
import currencySymbolMap from 'currency-symbol-map/map';
import { format } from 'date-fns';
import { zonedTimeToUtc } from 'date-fns-tz';
import { CalendarIcon, ChevronRight, Loader2, Save, Trash2 } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
Expand All @@ -15,7 +16,7 @@ import { Button } from '~/components/ui/button';
import { Calendar } from '~/components/ui/calendar';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/collapsible';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '~/components/ui/form';
import { Input } from '~/components/ui/input';
import { Input, InputWithCurrency } from '~/components/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover';
import { cn } from '~/lib/utils';
import { api } from '~/trpc/react';
Expand Down Expand Up @@ -100,6 +101,12 @@ export default function GroupExpenseForm({ group, user, expense }: GroupExpenseF
});
}

if (user.timezone) {
// little hack to make sure the date used is timezoned to the user's preference
// the calendar component cannot be timezoned
data.date = zonedTimeToUtc(format(data.date, 'yyyy-MM-dd'), user.timezone ?? 'Europe/Amsterdam');
}

upsert.mutate(data);
}

Expand Down Expand Up @@ -128,17 +135,13 @@ export default function GroupExpenseForm({ group, user, expense }: GroupExpenseF
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<div className="flex items-center justify-end">
<Input
className="peer rounded-r-none pr-1 text-right [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
{...field}
min={0.01}
onChange={(e) => form.setValue('amount', parseFloat(e.target.value) || 0)}
/>
<div className="border-primary-200 bg-primary-200 peer-focus-visible:ring-primary-950 flex items-center justify-center self-stretch rounded-r-md border px-2 peer-focus-visible:ring-1 ">
<p>{currencySymbolMap[user.currency ?? 'EUR']}</p>
</div>
</div>
<InputWithCurrency
currency={currencySymbolMap[user.currency ?? 'EUR'] ?? '€'}
{...field}
step={0.01}
min={0.01}
onChange={(e) => form.setValue('amount', parseFloat(e.target.value) || 0)}
/>
</FormControl>
<FormDescription>How much was it?</FormDescription>
<FormMessage />
Expand Down Expand Up @@ -321,19 +324,13 @@ function SplitInput({ value, form, open, onOpenChange, title, group, user, onInp
</div>
<FormItem>
<FormControl>
<div className="flex items-center justify-end">
<Input
className="peer rounded-r-none pr-1 text-right [appearance:textfield] max-md:max-w-24 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
type="number"
value={value.find((s) => s.userId === u.id)?.value ?? 0}
step={0.01}
min={0}
onChange={(e) => onInputChange(parseFloat(e.target.value), u)}
/>
<div className="border-primary-200 bg-primary-200 peer-focus-visible:ring-primary-950 flex items-center justify-center self-stretch rounded-r-md border px-2 peer-focus-visible:ring-1 ">
<p>{currencySymbolMap[user.currency ?? 'EUR']}</p>
</div>
</div>
<InputWithCurrency
currency={currencySymbolMap[user.currency ?? 'EUR'] ?? '€'}
value={value.find((s) => s.userId === u.id)?.value ?? 0}
step={0.01}
min={0}
onChange={(e) => onInputChange(parseFloat(e.target.value), u)}
/>
</FormControl>
</FormItem>
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/app/groups/[groupId]/group-charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ export default async function GroupCharts({
const timezone = user.timezone ?? 'Europe/Amsterdam';
const users = group.UserGroup.map((e) => e.user);

const time = new Date();
const now = new Date();
const preferredTimezoneOffset = getTimezoneOffset(timezone);
const localeTimezoneOffset = new Date().getTimezoneOffset() * 60 * 1000;
const from = new Date(startOfMonth(time).getTime() - preferredTimezoneOffset - localeTimezoneOffset);
const to = new Date(endOfMonth(time).getTime() - preferredTimezoneOffset - localeTimezoneOffset);
const from = new Date(startOfMonth(now).getTime() - preferredTimezoneOffset - localeTimezoneOffset);
const to = new Date(endOfMonth(now).getTime() - preferredTimezoneOffset - localeTimezoneOffset);

const [expenses, settlements] = await Promise.all([
api.groups.expenses.period.query({ groupId: group.id, from, to }),
Expand Down
7 changes: 4 additions & 3 deletions src/app/groups/[groupId]/register-settlement.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { zodResolver } from '@hookform/resolvers/zod';
import { AvatarIcon, CalendarIcon, CheckIcon } from '@radix-ui/react-icons';
import currencySymbolMap from 'currency-symbol-map/map';
import { format } from 'date-fns';
import { zonedTimeToUtc } from 'date-fns-tz';
import { Loader2, MoveDown, Trash2 } from 'lucide-react';
Expand All @@ -16,7 +17,7 @@ import { Calendar } from '~/components/ui/calendar';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '~/components/ui/command';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '~/components/ui/dialog';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '~/components/ui/form';
import { Input } from '~/components/ui/input';
import { InputWithCurrency } from '~/components/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover';
import { cn } from '~/lib/utils';
import { api } from '~/trpc/react';
Expand Down Expand Up @@ -154,8 +155,8 @@ export default function RegisterSettlement({ group, user, children, settlement }
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<Input
type="number"
<InputWithCurrency
currency={currencySymbolMap[user.currency ?? 'EUR'] ?? '€'}
onFocus={(e) => e.target.select()}
step={0.01}
min={0.01}
Expand Down
28 changes: 27 additions & 1 deletion src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,30 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
});
Input.displayName = 'Input';

export { Input };
export interface InputWithCurrencyProps extends InputProps {
currency: string;
}

const InputWithCurrency = React.forwardRef<HTMLInputElement, InputWithCurrencyProps>(
({ className, currency, ...props }, ref) => {
return (
<div className="flex items-center justify-end">
<Input
className={cn(
'peer rounded-r-none pr-1 text-right [appearance:textfield] max-md:max-w-24 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',
className,
)}
type="number"
ref={ref}
{...props}
/>
<div className="border-primary-200 bg-primary-200 peer-focus-visible:ring-primary-950 flex items-center justify-center self-stretch rounded-r-md border px-2 peer-focus-visible:ring-1 ">
<p>{currency}</p>
</div>
</div>
);
},
);
InputWithCurrency.displayName = 'InputWithCurrency';

export { Input, InputWithCurrency };
1 change: 0 additions & 1 deletion src/server/api/routers/groups/groups.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { settings } from '.eslintrc.cjs';
import { TRPCError } from '@trpc/server';
import { log } from 'next-axiom';
import { z } from 'zod';
Expand Down

0 comments on commit 00ce278

Please sign in to comment.