Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export transactions as CSV #197

Merged
merged 1 commit into from
Jan 14, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
selectCurrentUserPermissions,
selectGroupById,
selectSortedTransactions,
selectAccountIdToAccountMap,
} from "@abrechnung/redux";
import { Add, Clear } from "@mui/icons-material";
import SearchIcon from "@mui/icons-material/Search";
Expand All @@ -29,13 +30,14 @@
useMediaQuery,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { SaveAlt } from "@mui/icons-material";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { TagSelector } from "@/components/TagSelector";
import { PurchaseIcon, TransferIcon } from "@/components/style/AbrechnungIcons";
import { MobilePaper } from "@/components/style/mobile";
import { useTitle } from "@/core/utils";
import { selectGroupSlice, useAppDispatch, useAppSelector } from "@/store";
import { selectGroupSlice, useAppDispatch, useAppSelector, selectAccountSlice } from "@/store";
import { TransactionListItem } from "./TransactionListItem";
import { useTranslation } from "react-i18next";

Expand All @@ -46,6 +48,79 @@
const emptyList = [];
const MAX_ITEMS_PER_PAGE = 40;

function exportCsv(groupId) {
const accounts = useAppSelector((state) =>

Check failure on line 52 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

React Hook "useAppSelector" is called in function "exportCsv" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
selectAccountIdToAccountMap({ state: selectAccountSlice(state), groupId })
);

let transactionsSorted = useAppSelector((state) =>

Check failure on line 56 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

React Hook "useAppSelector" is called in function "exportCsv" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
selectSortedTransactions({ state, groupId, searchTerm: "", sortMode: "billed_at", tags: [] })
);
transactionsSorted = [...transactionsSorted].reverse();

const accountIds = Object.keys(accounts).filter(id => !accounts[id].deleted);

Check failure on line 61 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

Replace `id` with `(id)`
const accountNames = accountIds.map(id => accounts[id].name);

Check failure on line 62 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

Replace `id` with `(id)`
const accountIndexById = Object.fromEntries(accountIds.map((id, index) => [id, index]));

let exportedCsv = "ID,Date,Payer,Name,Tags,Value," + accountNames.join(",") + ",Description\n";
for (const transaction of transactionsSorted) {
if (transaction.is_wip) continue;

const creditorId = Object.entries(transaction.creditor_shares)[0][0];
const creditorName = accounts[creditorId].name;
let tags = "";
if (transaction.tags.length == 1) {
tags = transaction.tags[0];
} else if (transaction.tags.length > 1) {
tags = JSON.stringify(transaction.tags.join(","));
}

let value = transaction.value;
let total = accountIds.map(() => 0);

Check failure on line 79 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

'total' is never reassigned. Use 'const' instead

if (transaction.type == "transfer") {
total[accountIndexById[creditorId]] = transaction.value;
const debitorId = Object.entries(transaction.debitor_shares)[0][0];
total[accountIndexById[debitorId]] = -transaction.value;
value = 0

Check failure on line 85 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

Insert `;`
} else {
let extraFromPositions = 0;
let totalPositions = 0;
for (let position of Object.values(transaction.positions)) {

Check failure on line 89 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

'position' is never reassigned. Use 'const' instead
const totalShares = Object.values(position.usages).reduce((a, b) => a + b, 0) + position.communist_shares;

Check failure on line 90 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

Insert `⏎···················`
for (let [accountId, shares] of Object.entries(position.usages)) {

Check failure on line 91 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

'accountId' is never reassigned. Use 'const' instead

Check failure on line 91 in frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx

View workflow job for this annotation

GitHub Actions / build_and_test_frontend / lint

'shares' is never reassigned. Use 'const' instead
let value = position.price*shares/totalShares;
total[accountIndexById[accountId]] += value;
totalPositions += value;
}
extraFromPositions += position.price*position.communist_shares/totalShares;
}
totalPositions += extraFromPositions;
const valueMinusPositions = transaction.value - totalPositions;
const totalShares = Object.values(transaction.debitor_shares).reduce((a, b) => a + b, 0);
const numberOfDebitors = Object.values(transaction.debitor_shares).length;
for (let [accountId, debitorShares] of Object.entries(transaction.debitor_shares)) {
total[accountIndexById[accountId]] += valueMinusPositions*debitorShares/totalShares + extraFromPositions/numberOfDebitors;
}
}
exportedCsv += `${transaction.id},${transaction.billed_at},${creditorName},${JSON.stringify(transaction.name)},${tags},${value.toFixed(2)},`;
exportedCsv += total.map((value) => value.toFixed(2)).join(",");
exportedCsv += "," + JSON.stringify(transaction.description) + "\n";
}
return exportedCsv;
}

function downloadCsv(str, filename) {
let blob = new Blob([str], {type: "text/csv;charset=utf-8"});
let url = URL.createObjectURL(blob);
let link = document.createElement("a");
link.download = filename;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}

export const TransactionList: React.FC<Props> = ({ groupId }) => {
const { t } = useTranslation();
const theme: Theme = useTheme();
Expand Down Expand Up @@ -98,6 +173,8 @@

const handleChangeTagFilter = (newTags: string[]) => setTagFilter(newTags);

const exportedCsv = exportCsv(groupId);

return (
<>
<MobilePaper>
Expand Down Expand Up @@ -162,6 +239,13 @@
/>
</FormControl>
</Box>
<Box sx={{ display: "flex-item" }}>
<div style={{ padding: "8px" }}>
<Tooltip title="Export CSV">
<IconButton size="small" color="primary" onClick={() => {downloadCsv(exportedCsv, "transactions.csv");}}><SaveAlt /></IconButton>
</Tooltip>
</div>
</Box>
{!isSmallScreen && permissions.canWrite && (
<Box sx={{ display: "flex-item" }}>
<div style={{ padding: "8px" }}>
Expand Down
Loading