Skip to content

Commit

Permalink
feat(TransactionsImport): match contribution
Browse files Browse the repository at this point in the history
  • Loading branch information
Betree committed Jun 21, 2024
1 parent 0cea006 commit 77a6a38
Show file tree
Hide file tree
Showing 3 changed files with 427 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,378 @@
import React from 'react';
import { query } from 'express';

Check failure on line 2 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'query' is defined but never used
import { ceil, floor, isEmpty, startCase } from 'lodash';
import type { LucideIcon } from 'lucide-react';
import {
ALargeSmall,
Ban,
Calendar,
Coins,
ExternalLink,
Filter,
ListPlus,

Check failure on line 12 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'ListPlus' is defined but never used
RefreshCcw,

Check failure on line 13 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'RefreshCcw' is defined but never used
Save,
ScanSearch,

Check failure on line 15 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'ScanSearch' is defined but never used
Search,

Check failure on line 16 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'Search' is defined but never used
} from 'lucide-react';
import { FormattedMessage } from 'react-intl';
import { z } from 'zod';

import { centsAmountToFloat, floatAmountToCents } from '../../../../lib/currency-utils';

Check failure on line 21 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'centsAmountToFloat' is defined but never used

Check failure on line 21 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'floatAmountToCents' is defined but never used
import type { FilterComponentConfigs, FiltersToVariables } from '../../../../lib/filters/filter-types';
import { integer } from '../../../../lib/filters/schemas';
import type {
Account,
Amount,
TransactionsImport,
TransactionsImportRow,
} from '../../../../lib/graphql/types/v2/graphql';
import useQueryFilter from '../../../../lib/hooks/useQueryFilter';
import type { CSVConfig } from './lib/types';

import Avatar from '../../../Avatar';

Check failure on line 33 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'Avatar' is defined but never used
import DateTime from '../../../DateTime';
import FormattedMoneyAmount from '../../../FormattedMoneyAmount';
import Link from '../../../Link';

Check failure on line 36 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'Link' is defined but never used
import LinkCollective from '../../../LinkCollective';
import StyledLink from '../../../StyledLink';

Check failure on line 38 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / lint

'StyledLink' is defined but never used
import { DataTable } from '../../../table/DataTable';
import { Button } from '../../../ui/Button';
import { Checkbox } from '../../../ui/Checkbox';
import { Dialog, DialogContent, DialogHeader } from '../../../ui/Dialog';
import { Input } from '../../../ui/Input';
import { RadioGroup, RadioGroupItem } from '../../../ui/RadioGroup';
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../ui/Tooltip';
import { amountFilter } from '../../filters/AmountFilter';
import { AmountFilterType } from '../../filters/AmountFilter/schema';
import { dateFilter, expectedDateFilter } from '../../filters/DateFilter';
import { DateFilterType } from '../../filters/DateFilter/schema';
import { expectedFundsFilter } from '../../filters/ExpectedFundsFilter';
import { Filterbar } from '../../filters/Filterbar';
import { orderByFilter } from '../../filters/OrderFilter';
import { SearchFilter, searchFilter } from '../../filters/SearchFilter';

Check failure on line 53 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / typescript

'"../../filters/SearchFilter"' has no exported member named 'SearchFilter'. Did you mean 'searchFilter'?
import type { FilterMeta } from '../contributions/filters';
import { OrderTypeFilter } from '../contributions/filters';
import type { FilterValues } from '../transactions/HostTransactions';

const MOCK_MATCHES = [
{
id: 45624,
description: 'Contribution from John Doe to Webpack',
date: 'June 1, 2023',
account: {
id: 12345,
slug: 'webpack',
name: 'Webpack',
},
amount: {
valueInCents: 1000,
currency: 'USD',
},
},
{
id: 78945,
description: 'Contribution from Toto to Babel',
date: 'June 1, 2023',
account: {
id: 67890,
slug: 'babel',
name: 'Babel',
},
amount: {
valueInCents: 500,
currency: 'USD',
},
},
{
id: 12345,
description: 'Contribution from Mike to Webpack',
date: 'June 1, 2023',
account: {
id: 12345,
slug: 'webpack',
name: 'Webpack',
},
amount: {
valueInCents: 1000,
currency: 'USD',
},
},
];

const NB_CONTRIBUTIONS_DISPLAYED = 5;

export const filtersSchema = z.object({
limit: integer.default(NB_CONTRIBUTIONS_DISPLAYED),
offset: integer.default(0),
orderBy: orderByFilter.schema,
searchTerm: searchFilter.schema,
date: dateFilter.schema,
expectedDate: expectedDateFilter.schema,
expectedFundsFilter: expectedFundsFilter.schema,
amount: amountFilter.schema,
// type: z.nativeEnum(OrderTypeFilter).optional(),
// paymentMethod: z.string().optional(),
});

// TODO change any to query variables
const toVariables: FiltersToVariables<z.infer<typeof filtersSchema>, any, FilterMeta> = {
orderBy: orderByFilter.toVariables,
date: dateFilter.toVariables,
expectedDate: expectedDateFilter.toVariables,
amount: amountFilter.toVariables,
// type: (value: OrderTypeFilter) => {
// switch (value) {
// case OrderTypeFilter.RECURRING:
// return {
// onlySubscriptions: true,
// };
// case OrderTypeFilter.ONETIME:
// return {
// frequency: ContributionFrequency.ONETIME,
// };
// }
// },
// paymentMethod: (value: string) => {
// if (value) {
// return { paymentMethod: { id: value } };
// }

// return null;
// },
};

const filters: FilterComponentConfigs<z.infer<typeof filtersSchema>, FilterMeta> = {
searchTerm: searchFilter.filter,
date: dateFilter.filter,
expectedDate: expectedDateFilter.filter,
expectedFundsFilter: expectedFundsFilter.filter,
amount: amountFilter.filter,
};

function FilterWithRawValueButton({
SecondaryIcon = ALargeSmall,
message,
onClick,
}: {
SecondaryIcon?: LucideIcon;
message?: React.ReactNode | string;
onClick: () => void;
}) {
return (
<Tooltip delayDuration={0}>
<TooltipTrigger>
<Button
variant="ghost"
size="icon-xs"
className="relative inline-block h-4 w-4 p-0 text-neutral-500 hover:bg-white hover:text-neutral-700"
onClick={onClick}
>
<Filter size={14} className="inline" />
{SecondaryIcon && (
<SecondaryIcon size={12} className="radius-50 absolute -bottom-1 -right-1 bg-white bg-opacity-50" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>{message || <FormattedMessage defaultMessage="Search term" id="UD/BhG" />}</TooltipContent>
</Tooltip>
);
}

const filterRawValueEntries = ([key, value]: [string, string], csvConfig: CSVConfig): boolean => {
// Ignore empty values
if (isEmpty(value)) {
return false;
}

// Ignore amount and date, since they're already parsed and displayed above
if (csvConfig) {
const { columns } = csvConfig;
if ([columns.credit.target, columns.debit.target, columns.date.target].includes(key)) {
return false;
}
}

return true;
};

export const MatchContributionFromImportRowDialog = ({
host,
row,
onClose,
transactionsImport,
...props
}: {
host: Account;
row: TransactionsImportRow;
transactionsImport: TransactionsImport;
onClose: () => void;
} & React.ComponentProps<typeof Dialog>) => {
const [selectedRow, setSelectedRow] = React.useState(null);
const [loading, setLoading] = React.useState(false);

const queryFilter = useQueryFilter({
schema: filtersSchema,
toVariables,
filters,
meta: { currency: row.amount.currency },
skipRouter: true,
});

React.useEffect(() => {
setLoading(true);
setTimeout(() => setLoading(false), 1000);
}, [queryFilter.values]);

return (
<Dialog open onOpenChange={onClose} {...props}>
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
<h2 className="text-xl font-bold">
<FormattedMessage defaultMessage="Match contribution" id="c7INEq" />
</h2>
</DialogHeader>
<div className="text-sm">
<Filterbar hideSeparator {...queryFilter} />
<RadioGroup value={selectedRow?.id}>
<DataTable
className="mt-3"
loading={loading}
nbPlaceholders={3}
onClickRow={({ original }) => setSelectedRow(original)}
getRowClassName={({ original }) =>
selectedRow?.id === original.id
? 'bg-blue-50 font-semibold shadow-inner shadow-blue-100 !border-l-2 border-l-blue-500'
: ''
}
columns={[
{
id: 'select',
cell: ({ row }) => <RadioGroupItem value={row.original.id} />,

Check failure on line 251 in components/dashboard/sections/transactions-imports/MatchContributionDialog.tsx

View workflow job for this annotation

GitHub Actions / typescript

Type 'number' is not assignable to type 'string'.
meta: { className: 'w-[20px]' },
},
{
header: 'Date',
accessorKey: 'date',
},
{
header: 'Amount',
accessorKey: 'amount',
cell: ({ cell }) => {
const value = cell.getValue() as Amount;
return (
<FormattedMoneyAmount
amount={value.valueInCents}
currency={value.currency}
showCurrencyCode={false}
/>
);
},
},
{
header: 'Description',
accessorKey: 'description',
cell: ({ cell }) => <div className="flex items-center gap-1">{cell.getValue() as string}</div>,
},
]}
data={MOCK_MATCHES}
/>
</RadioGroup>
</div>
<div className="mt-2 grid grid-cols-2 gap-4 text-sm">
<div className="rounded-lg border border-gray-200 p-4">
<strong className="text-base text-gray-700">
<FormattedMessage defaultMessage="Imported data" id="tmfin0" />
</strong>

<ul className="mt-2 list-inside list-disc">
<li>
<strong>Source</strong>: {transactionsImport.source}{' '}
<FilterWithRawValueButton
onClick={() => queryFilter.setFilter('searchTerm', transactionsImport.source)}
/>
</li>
<li>
<strong>Amount</strong>:{' '}
<FormattedMoneyAmount
amount={row.amount.valueInCents}
currency={row.amount.currency}
amountStyles={null}
/>{' '}
<FilterWithRawValueButton
message={<FormattedMessage defaultMessage="Search amount" id="o9K20a" />}
SecondaryIcon={Coins}
onClick={() => {
const valueInCents = row.amount.valueInCents;
queryFilter.setFilter('amount', {
type: AmountFilterType.IS_BETWEEN,
gte: floor(valueInCents / 20, -2),
lte: ceil(valueInCents / 20, -2),
});
}}
/>
</li>
<li>
<strong>Date</strong>: <DateTime value={row.date} />
<FilterWithRawValueButton
message={<FormattedMessage defaultMessage="Search date" id="o9K20a" />}
SecondaryIcon={Calendar}
onClick={() => queryFilter.setFilter('date', { type: DateFilterType.BEFORE_OR_ON, lte: row.date })}
/>
</li>
{Object.entries(row.rawValue as Record<string, string>)
.filter(entry => filterRawValueEntries(entry, transactionsImport.csvConfig))
.map(([key, value]) => (
<li key={key}>
<strong>{startCase(key)}</strong>: {value.toString()}{' '}
<FilterWithRawValueButton onClick={() => queryFilter.setFilter('searchTerm', value.toString())} />
</li>
))}
</ul>
</div>
{selectedRow && (
<div className="rounded-lg border border-gray-200 p-4">
<div className="flex items-center gap-1 text-base font-semibold text-neutral-700">
<FormattedMessage defaultMessage="Selected match" id="bQndkF" />:
<span className="flex cursor-pointer items-center gap-1 underline hover:text-neutral-900">
contribution #{selectedRow.id}
<ExternalLink size={14} />
</span>
</div>

<ul className="mt-2 list-inside list-disc">
<li>
<strong>Description</strong>: {selectedRow.description}
</li>
<li>
<strong>Date</strong>: {selectedRow.date}
</li>
<li>
<strong>Amount</strong>:{' '}
<FormattedMoneyAmount
amount={selectedRow.amount.valueInCents}
amountStyles={null}
currency={selectedRow.amount.currency}
/>
</li>
<li>
<strong>Account</strong>: <LinkCollective collective={selectedRow.account} />
</li>
</ul>
</div>
)}
</div>
<div className="mt-5 flex justify-end gap-2">
<Button variant="outline" onClick={onClose}>
<Ban size={16} className="mr-2" />
Cancel
</Button>
<Button disabled={!selectedRow}>
<Save size={16} className="mr-2" />
Save
</Button>{' '}
</div>
</DialogContent>
</Dialog>
);
};
Loading

0 comments on commit 77a6a38

Please sign in to comment.