Skip to content

Commit

Permalink
Merge pull request #44 from dsaltares/feat/42
Browse files Browse the repository at this point in the history
feat: 42 - transactions table supports filtering by multiple accounts and categories
  • Loading branch information
dsaltares authored Jan 4, 2025
2 parents 4c644b6 + 2866c1a commit dde3245
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 90 deletions.
2 changes: 1 addition & 1 deletion src/components/AccountLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function AccountLink({ id, name }: Props) {
return (
<LinkWithSearchParams
pathname={Routes.transactions}
searchParam="filterByAccountId"
searchParam="filterByAccounts"
searchValue={id}
>
{name}
Expand Down
2 changes: 1 addition & 1 deletion src/components/CategoryChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function CategoryChip({ id, name }: Props) {
return id && name ? (
<LinkWithSearchParams
pathname={Routes.transactions}
searchParam="filterByCategoryId"
searchParam="filterByCategories"
searchValue={id}
>
<Chip
Expand Down
4 changes: 3 additions & 1 deletion src/components/CreateUpdateTransactionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ export default function CreateUpdateTransactionDialog({
label: category.name,
id: `${category.id}`,
}));
const filteredAccountId = filtersByField.accountId;
const filteredAccounts = filtersByField.accounts?.split(',');
const filteredAccountId =
filteredAccounts?.length === 1 ? filteredAccounts[0] : '';
const account = transaction
? accountOptions.find(
(option) => `${transaction.accountId}` === option.id,
Expand Down
79 changes: 52 additions & 27 deletions src/components/TransactionFilterChips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Chip from '@mui/material/Chip';
import PaymentIcon from '@mui/icons-material/Payment';
import PaidIcon from '@mui/icons-material/Paid';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useTheme } from '@mui/material/styles';
import client from '@lib/client';
import useFiltersFromUrl from '@lib/useFiltersFromUrl';
Expand Down Expand Up @@ -44,9 +44,30 @@ export default function TransactionFilterChips() {
[categories],
);

const handleClearType = () => setFilters({ type: undefined });
const handleClearAccount = () => setFilters({ accountId: undefined });
const handleClearCategory = () => setFilters({ categoryId: undefined });
const handleClearType = useCallback(
() => setFilters({ type: undefined }),
[setFilters],
);
const handleClearAccount = useCallback(
(accountId: string) =>
setFilters({
accounts: filtersByField.accounts
?.split(',')
.filter((id) => id !== accountId)
.join(','),
}),
[setFilters, filtersByField],
);
const handleClearCategory = useCallback(
(categoryId: string) =>
setFilters({
categories: filtersByField.categories
?.split(',')
.filter((id) => id !== categoryId)
.join(','),
}),
[filtersByField, setFilters],
);
const handleClearDescription = () => setFilters({ description: undefined });
const handleClearAmount = () =>
setFilters({ minAmount: undefined, maxAmount: undefined });
Expand Down Expand Up @@ -112,13 +133,16 @@ export default function TransactionFilterChips() {
onDelete={handleClearAmount}
/>
)}
{!!filtersByField.accountId && accountsById[filtersByField.accountId] && (
<Chip
variant="outlined"
label={accountsById[filtersByField.accountId].name}
onDelete={handleClearAccount}
/>
)}
{filtersByField.accounts
?.split(',')
.map((accountId) => (
<Chip
key={`account-${accountId}`}
variant="outlined"
label={accountsById[accountId].name}
onDelete={() => handleClearAccount(accountId)}
/>
))}
{!!filtersByField.type &&
TransactionTypes.includes(filtersByField.type as TransactionType) && (
<Chip
Expand All @@ -136,22 +160,23 @@ export default function TransactionFilterChips() {
onDelete={handleClearType}
/>
)}
{!!filtersByField.categoryId &&
categoriesById[filtersByField.categoryId] && (
<Chip
sx={{
backgroundColor: stringToColor(
categoriesById[filtersByField.categoryId].name,
),
color: theme.palette.getContrastText(
stringToColor(categoriesById[filtersByField.categoryId].name),
),
}}
variant="outlined"
label={categoriesById[filtersByField.categoryId].name}
onDelete={handleClearCategory}
/>
)}
{filtersByField.categories?.split(',').map(
(categoryId) =>
categoriesById[categoryId] && (
<Chip
key={`category-${categoryId}`}
sx={{
backgroundColor: stringToColor(categoriesById[categoryId].name),
color: theme.palette.getContrastText(
stringToColor(categoriesById[categoryId].name),
),
}}
variant="outlined"
label={categoriesById[categoryId].name}
onDelete={() => handleClearCategory(categoryId)}
/>
),
)}
{!!filtersByField.description && (
<Chip
variant="outlined"
Expand Down
72 changes: 28 additions & 44 deletions src/components/TransactionFilterDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import Autocomplete from '@mui/material/Autocomplete';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
Expand All @@ -15,12 +14,13 @@ import { endOfDay } from 'date-fns/endOfDay';
import createUTCDate from '@lib/createUTCDate';
import useFiltersFromUrl from '@lib/useFiltersFromUrl';
import type { Category } from '@server/category/types';
import { isOptionEqualToValue } from '@lib/autoCompleteOptions';
import { isPeriod, type Period } from '@server/types';
import type { Account } from '@server/accounts/types';
import type { TransactionType } from '@server/transactions/types';
import PeriodSelect from './PeriodSelect';
import TransactionTypeSelect from './TransactionTypeSelect';
import AccountAutocomplete from './AccountAutocomplete';
import CategoryAutocomplete from './CategoryAutocomplete';

type Props = {
open: boolean;
Expand All @@ -40,36 +40,19 @@ export default function TransactionFilterDialog({
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
const { filtersByField, setFilters } = useFiltersFromUrl();
const accountOptions = useMemo(
() =>
accounts.map((account) => ({
label: account.name,
id: `${account.id}`,
})),
[accounts],
const [selectedAccounts, setSelectedAccounts] = useState<number[]>(
filtersByField.accounts
? filtersByField.accounts.split(',').map(Number)
: [],
);
const categoryOptions = useMemo(
() =>
categories.map((category) => ({
label: category.name,
id: `${category.id}`,
})),
[categories],
);
const [account, setAccount] = useState(
() =>
accountOptions.find((option) => option.id === filtersByField.accountId) ||
null,
const [selectedCategories, setSelectedCategories] = useState<number[]>(
filtersByField.categories
? filtersByField.categories.split(',').map(Number)
: [],
);
const [type, setType] = useState<TransactionType | ''>(() =>
filtersByField.type ? (filtersByField.type as TransactionType) : '',
);
const [category, setCategory] = useState(
() =>
categoryOptions.find(
(option) => option.id === filtersByField.categoryId,
) || null,
);
const [description, setDescription] = useState(filtersByField.description);
const [period, setPeriod] = useState<Period | ''>(
isPeriod(filtersByField.period) ? filtersByField.period : '',
Expand All @@ -94,8 +77,15 @@ export default function TransactionFilterDialog({
minAmount,
maxAmount,
type: type || undefined,
accountId: account?.id,
categoryId: category?.id,
accounts:
selectedAccounts.length > 0 && selectedAccounts.length < accounts.length
? selectedAccounts.join(',')
: undefined,
categories:
selectedCategories.length > 0 &&
selectedCategories.length < categories.length
? selectedCategories.join(',')
: undefined,
description: description || undefined,
});
onClose();
Expand Down Expand Up @@ -188,22 +178,16 @@ export default function TransactionFilterDialog({
}}
/>
</Stack>
<Autocomplete
id="account-autocomplete"
value={account}
onChange={(_event, newValue) => setAccount(newValue)}
options={accountOptions}
isOptionEqualToValue={isOptionEqualToValue}
renderInput={(params) => <TextField {...params} label="Account" />}
<AccountAutocomplete
accounts={accounts}
selected={selectedAccounts}
onChange={setSelectedAccounts}
/>
<TransactionTypeSelect value={type} onChange={setType} clearable />
<Autocomplete
id="category-autocomplete"
value={category}
onChange={(_event, newValue) => setCategory(newValue)}
options={categoryOptions}
isOptionEqualToValue={isOptionEqualToValue}
renderInput={(params) => <TextField {...params} label="Category" />}
<CategoryAutocomplete
categories={categories}
selected={selectedCategories}
onChange={setSelectedCategories}
/>
<TextField
label="Description"
Expand Down
4 changes: 2 additions & 2 deletions src/lib/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ const Routes = {
transactions: '/transactions',
recentTransactions: '/transactions?filterByPeriod=lastMonth',
transactionsForCategory: (categoryId: number) =>
`/transactions?filterByCategoryId=${categoryId}&filterByPeriod=lastMonth`,
`/transactions?filterByCategories=${categoryId}&filterByPeriod=lastMonth`,
transactionsForAccount: (accountId: number) =>
`/transactions?filterByAccountId=${accountId}&filterByPeriod=lastMonth`,
`/transactions?filterByAccounts=${accountId}&filterByPeriod=lastMonth`,
accounts: '/accounts',
insights: '/insights?filterByPeriod=lastMonth',
budget: '/budget',
Expand Down
8 changes: 2 additions & 6 deletions src/routes/TransactionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,9 @@ export default function TransactionsPage() {
maxAmount: filtersByField.maxAmount
? parseFloat(filtersByField.maxAmount)
: undefined,
accountId: filtersByField.accountId
? parseInt(filtersByField.accountId, 10)
: undefined,
accounts: filtersByField.accounts?.split(',').map(Number),
type: filtersByField.type as TransactionType | undefined,
categoryId: filtersByField.categoryId
? parseInt(filtersByField.categoryId, 10)
: undefined,
categories: filtersByField.categories?.split(',').map(Number),
description: filtersByField.description,
});
const { data, isLoading: isLoadingAccounts } = client.getAccounts.useQuery();
Expand Down
12 changes: 6 additions & 6 deletions src/server/transactions/getTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ const getTransactions: Procedure<
date,
minAmount,
maxAmount,
accountId,
accounts,
type,
categoryId,
categories,
description,
},
}) => {
Expand All @@ -31,8 +31,8 @@ const getTransactions: Procedure<
.where('deletedAt', 'is', null)
.$narrowType<{ numAttachments: NotNull }>();

if (accountId) {
query = query.where('accountId', 'is', accountId);
if (accounts && accounts.length > 0) {
query = query.where('accountId', 'in', accounts);
}

const dateFilter = getDateWhereFromFilter(date);
Expand Down Expand Up @@ -62,8 +62,8 @@ const getTransactions: Procedure<
query = query.where('type', 'is', type);
}

if (categoryId) {
query = query.where('categoryId', 'is', categoryId);
if (categories && categories.length > 0) {
query = query.where('categoryId', 'in', categories);
}

if (description) {
Expand Down
4 changes: 2 additions & 2 deletions src/server/transactions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export const GetTransactionsInput = z.object({
date: DateFilter.optional(),
minAmount: z.number().optional(),
maxAmount: z.number().optional(),
accountId: z.number().optional(),
accounts: z.number().array().optional(),
type: TransactionType.optional(),
categoryId: z.number().optional(),
categories: z.number().array().optional(),
description: z.string().optional(),
});
export const GetTransactionsOutput = z.array(Transaction);
Expand Down

0 comments on commit dde3245

Please sign in to comment.