diff --git a/frontend/src/components/AccountTable/AccountTable.js b/frontend/src/components/AccountTable/AccountTable.js index fb5b55c..620a69a 100644 --- a/frontend/src/components/AccountTable/AccountTable.js +++ b/frontend/src/components/AccountTable/AccountTable.js @@ -20,6 +20,7 @@ import { import { createPayee, fetchPayees, selectPayeesMap } from '../../redux/slices/Payees' import { TableIcons } from '../../utils/Table' import { formatMonthFromDateString } from '../../utils/Date' +import { ExportCsv } from '../../utils/Export' import { refreshBudget, fetchAvailableMonths } from '../../redux/slices/Budgets' import { fetchBudgetMonth, fetchCategoryMonths } from '../../redux/slices/BudgetMonths' import TextField from '@mui/material/TextField' @@ -39,7 +40,6 @@ import { useTheme } from '@mui/styles' import { payeesSelectors } from '../../redux/slices/Payees' import { createSelector } from '@reduxjs/toolkit' import { categoriesSelectors } from '../../redux/slices/Categories' -import { ExportCsv } from '@material-table/exporters' import SaveAltIcon from '@mui/icons-material/SaveAlt' import Divider from '@mui/material/Divider' import Stack from '@mui/material/Stack' @@ -323,40 +323,64 @@ export default function Account(props) { const payeesMap = useSelector(selectPayeesMap) + const [addingTransaction, setAddingTransaction] = useState(false) + const selectTransactions = createSelector( [ (state, accountId) => state.accounts.entities[accountId] ? state.accounts.entities[accountId].transactions.entities : [], (state, accountId, reconciled) => reconciled, + (state, addingTransaction) => addingTransaction, ], (transactions, reconciled) => { - if (!showReconciled) { - return Object.values(transactions) - .filter(transaction => transaction.status !== 2) - .map(trx => { - return { - ...trx, - ...(trx.categoryId === null && { categoryId: '0' }), - filterField: `${categoriesMap[trx.categoryId]} ${payeesMap[trx.payeeId]}`, - } - }) + const retval = [] + if (addingTransaction) { + const now = new Date().toISOString() + retval.push({ + id: 0, + accountId: props.accountId, + amount: 0, + categoryId: '', + created: now, + date: now, + filterField: '', + memo: '', + payeeId: '', + status: 0, + updated: now, + }) } - return Object.values(transactions).map(trx => { - return { - ...trx, - ...(trx.categoryId === null && { categoryId: '0' }), - filterField: `${categoriesMap[trx.categoryId]} ${payeesMap[trx.payeeId]}`, - } - }) + if (!showReconciled) { + return retval.concat( + Object.values(transactions) + .filter(transaction => transaction.status !== 2) + .map(trx => { + return { + ...trx, + ...(trx.categoryId === null && { categoryId: '0' }), + filterField: `${categoriesMap[trx.categoryId]} ${payeesMap[trx.payeeId]}`, + } + }), + ) + } + + return retval.concat( + Object.values(transactions).map(trx => { + return { + ...trx, + ...(trx.categoryId === null && { categoryId: '0' }), + filterField: `${categoriesMap[trx.categoryId]} ${payeesMap[trx.payeeId]}`, + } + }), + ) }, ) const transactions = useSelector(state => selectTransactions(state, props.accountId, showReconciled)) - const [transactionData, setTransactionData] = useState(transactions) - const data = useMemo(() => transactionData, [transactionData, account]) + const data = useMemo(() => transactions, [transactions, account]) const cancelAddTransaction = () => { - setTransactionData(transactionData.filter(trx => trx.id !== 0)) + setAddingTransaction(false) } const filter = createFilterOptions() @@ -367,6 +391,7 @@ export default function Account(props) { accessor: 'filterField', }, { + title: 'Date', accessor: 'date', Header: 'DATE', width: 135, @@ -375,8 +400,10 @@ export default function Account(props) { return {new Date(props.cell.value).toLocaleDateString()} }, Editing: props => , + exportTransformer: value => new Date(value).toLocaleDateString(), }, { + title: 'Payee', accessor: 'payeeId', Header: 'PAYEE', width: 200, @@ -459,8 +486,10 @@ export default function Account(props) { }} /> ), + exportTransformer: value => payeesMap[value], }, { + title: 'Evenlope', accessor: 'categoryId', Header: 'ENVELOPE', style: { @@ -523,8 +552,10 @@ export default function Account(props) { /> ) }, + exportTransformer: value => categoriesMap[value], }, { + title: 'Memo', accessor: 'memo', Header: 'MEMO', style: { @@ -574,6 +605,7 @@ export default function Account(props) { }, }, { + title: 'Amount', accessor: 'amount', Header: 'AMOUNT', numeric: true, @@ -598,8 +630,10 @@ export default function Account(props) { console.log(props) return }, + exportTransformer: value => intlFormat(valueToDinero(value)), }, { + title: 'Status', accessor: 'status', field: 'status', width: 50, @@ -656,6 +690,16 @@ export default function Account(props) { ), + exportTransformer: value => { + switch (value) { + case 0: + return 'Pending' + case 1: + return 'Cleared' + case 2: + return 'Reconciled' + } + }, }, ].map(col => { col.cellStyle = { @@ -1001,8 +1045,7 @@ export default function Account(props) { } const exportData = () => { - const [cols, data] = getTableData() - ExportCsv(cols, data, `${account.name} Transactions`) + ExportCsv(columns, rows, `${account.name} Transactions`) } const onSelectionChange = data => { @@ -1010,23 +1053,7 @@ export default function Account(props) { } const addTransactionClick = () => { - const now = new Date().toISOString() - setTransactionData([ - { - id: 0, - accountId: props.accountId, - amount: 0, - categoryId: '', - created: now, - date: now, - filterField: '', - memo: '', - payeeId: '', - status: 0, - updated: now, - }, - ...transactionData, - ]) + return setAddingTransaction(true) } return ( diff --git a/frontend/src/components/ImportCSV.js b/frontend/src/components/ImportCSV.js index ff2f9a2..62f0d77 100644 --- a/frontend/src/components/ImportCSV.js +++ b/frontend/src/components/ImportCSV.js @@ -9,7 +9,7 @@ import Box from '@mui/material/Box' import Backdrop from '@mui/material/Backdrop' import { useSelector, useDispatch } from 'react-redux' import Button from '@mui/material/Button' -import { parse } from 'csv-parse/dist/esm/sync.js' +import { parse } from 'csv-parse/browser/esm/sync' import { dinero, multiply, toUnit } from 'dinero.js' import { USD } from '@dinero.js/currencies' import FormGroup from '@mui/material/FormGroup' diff --git a/frontend/src/utils/Export.js b/frontend/src/utils/Export.js new file mode 100644 index 0000000..7199415 --- /dev/null +++ b/frontend/src/utils/Export.js @@ -0,0 +1,27 @@ +import { CsvBuilder } from 'filefy' + +export const ExportCsv = (columns, data = [], filename = 'data', delimiter = ',') => { + try { + let finalData = data + // Grab first item for data array, make sure it is also an array. + // If it is an object, 'flatten' it into an array of strings. + if (data.length && !Array.isArray(data[0])) { + if (typeof data[0] === 'object') { + // Turn data into an array of string arrays, without the `tableData` prop + finalData = data.map(row => + columns.map(col => + col.exportTransformer ? col.exportTransformer(row.original[col.accessor]) : row.original[col.accessor], + ), + ) + } + } + const builder = new CsvBuilder(filename + '.csv') + builder + .setDelimeter(delimiter) + .setColumns(columns.map(col => col.title)) + .addRows(Array.from(finalData)) + .exportFile() + } catch (err) { + console.error(`error in ExportCsv : ${err}`) + } +}