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

csvexport #198

Merged
merged 2 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

[Compare the full difference.](https://github.com/SFTtech/abrechnung/compare/v0.12.1...HEAD)

- Add CSV exports for transactions in web by @ymeiron

## 0.12.1 (2024-01-05)

[Compare the full difference.](https://github.com/SFTtech/abrechnung/compare/v0.12.0...v0.12.1)

### Fixed

- Correctly filter out deleted transactions in balance computations
- Correctly filter out deleted transactions in balance computations
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { TransactionSortMode } from "@abrechnung/core";
import { TransactionSortMode, transactionCsvDump } from "@abrechnung/core";
import {
createTransaction,
selectCurrentUserPermissions,
selectGroupById,
selectSortedTransactions,
selectGroupAccounts,
selectTransactionBalanceEffects,
} from "@abrechnung/redux";
import { Add, Clear } from "@mui/icons-material";
import SearchIcon from "@mui/icons-material/Search";
Expand All @@ -29,15 +31,17 @@ import {
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, selectTransactionSlice } from "@/store";
import { TransactionListItem } from "./TransactionListItem";
import { useTranslation } from "react-i18next";
import { Transaction } from "@abrechnung/types";

interface Props {
groupId: number;
Expand All @@ -46,6 +50,29 @@ interface Props {
const emptyList = [];
const MAX_ITEMS_PER_PAGE = 40;

const downloadFile = (content: string, filename: string, mimetype: string) => {
const blob = new Blob([content], { type: `${mimetype};charset=utf-8` });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = filename;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};

const useDownloadCsv = (groupId: number, transactions: Transaction[]) => {
const accounts = useAppSelector((state) => selectGroupAccounts({ state: selectAccountSlice(state), groupId }));
const balanceEffects = useAppSelector((state) =>
selectTransactionBalanceEffects({ state: selectTransactionSlice(state), groupId })
);

return React.useCallback(() => {
const csv = transactionCsvDump(transactions, balanceEffects, accounts);
downloadFile(csv, "transactions.csv", "text/csv");
}, [accounts, balanceEffects, transactions]);
};

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

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

const downloadCsv = useDownloadCsv(groupId, transactions);

return (
<>
<MobilePaper>
Expand Down Expand Up @@ -164,19 +193,28 @@ export const TransactionList: React.FC<Props> = ({ groupId }) => {
</Box>
{!isSmallScreen && permissions.canWrite && (
<Box sx={{ display: "flex-item" }}>
<div style={{ padding: "8px" }}>
<Add color="primary" />
</div>
<Tooltip title={t("transactions.createPurchase")}>
<IconButton color="primary" onClick={onCreatePurchase}>
<PurchaseIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("transactions.createTransfer")}>
<IconButton color="primary" onClick={onCreateTransfer}>
<TransferIcon />
<Tooltip title={t("common.exportAsCsv")}>
<IconButton size="small" color="primary" onClick={downloadCsv}>
<SaveAlt />
</IconButton>
</Tooltip>
{permissions.canWrite && (
<>
<div style={{ padding: "8px" }}>
<Add color="primary" />
</div>
<Tooltip title={t("transactions.createPurchase")}>
<IconButton color="primary" onClick={onCreatePurchase}>
<PurchaseIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("transactions.createTransfer")}>
<IconButton color="primary" onClick={onCreateTransfer}>
<TransferIcon />
</IconButton>
</Tooltip>
</>
)}
</Box>
)}
</Box>
Expand Down
57 changes: 55 additions & 2 deletions frontend/libs/core/src/lib/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Transaction, TransactionBalanceEffect } from "@abrechnung/types";
import { fromISOString } from "@abrechnung/utils";
import { Account, Transaction, TransactionBalanceEffect } from "@abrechnung/types";
import { buildCsv, fromISOString } from "@abrechnung/utils";

export type TransactionSortMode = "last_changed" | "value" | "name" | "description" | "billed_at";

Expand Down Expand Up @@ -104,3 +104,56 @@ export const computeTransactionBalanceEffect = (transaction: Transaction): Trans

return accountBalances;
};

export const transactionCsvDump = (
transactions: Transaction[],
balanceEffects: { [id: number]: TransactionBalanceEffect },
accounts: Account[]
): string => {
const transactionsSorted = [...transactions]
.filter((t) => !t.is_wip)
.sort((t1, t2) => t1.billed_at.localeCompare(t2.billed_at));

const accountMap = Object.fromEntries(
accounts.filter((acc) => !acc.deleted).map((acc) => [`account-${acc.id}`, acc.name])
);

const csvHeaders = {
id: "ID",
date: "Date",
payer: "Payer",
name: "Name",
description: "Description",
currency_symbol: "Currency",
currency_conversion_rate: "Currency Conversion Rate",
tags: "Tags",
value: "Value",
...accountMap,
};

const data = [];

for (const transaction of transactionsSorted) {
const balanceEffect = balanceEffects[transaction.id];
const creditorId = Object.keys(transaction.creditor_shares)[0];
const creditorName = accountMap[`account-${creditorId}`];
const tags = transaction.tags.join(",");

const rowData = {
id: transaction.id,
date: transaction.billed_at,
payer: creditorName,
name: transaction.name,
description: transaction.description,
currency_symbol: transaction.currency_symbol,
currency_conversion_rate: transaction.currency_conversion_rate,
tags: tags,
value: transaction.value.toFixed(2),
...Object.fromEntries(
accounts.map((acc) => [`account-${acc.id}`, balanceEffect[acc.id]?.total.toFixed(2) ?? ""])
),
};
data.push(rowData);
}
return buildCsv(csvHeaders, data);
};
1 change: 1 addition & 0 deletions frontend/libs/translations/src/lib/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const translations = {
send: "Senden",
currency: "Währung",
addNewTag: "Neuen Tag hinzufügen",
exportAsCsv: "Als CSV Datei exportieren",
},
shareSelect: {
selectedPeople_one: "{{count}} Person",
Expand Down
1 change: 1 addition & 0 deletions frontend/libs/translations/src/lib/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const translations = {
send: "Send",
currency: "Currency",
addNewTag: "Add new Tag",
exportAsCsv: "Export as CSV",
},
shareSelect: {
selectedPeople_one: "{{count}} Person",
Expand Down
1 change: 1 addition & 0 deletions frontend/libs/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./lib/utils";
export * from "./lib/validators";
export * from "./lib/event-emitter";
export * from "./lib/floats";
export * from "./lib/csv";
17 changes: 17 additions & 0 deletions frontend/libs/utils/src/lib/csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type CSVHeaders<T extends object> = {
[K in keyof T]: string;
};

export const buildCsv = <T extends object>(headers: CSVHeaders<T>, data: T[]): string => {
const header = Object.values(headers).join(",") + "\n";

const stringifiedData = data
.map((row) =>
Object.keys(headers)
.map((headerKey) => String(row[headerKey as keyof T] ?? ""))
.join(",")
)
.join("\n");

return header + stringifiedData;
};
Loading