Skip to content
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
1 change: 0 additions & 1 deletion backend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions frontend/api/purchase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FilteredPurchases,
PurchaseLineItemType,
PurchasesWithCount,
PurchaseWithLineItems,
} from "../types/purchase";
import { authHeader, authWrapper, getClient } from "./client";

Expand Down Expand Up @@ -141,3 +142,35 @@ export const fetchAllCategories = async (): Promise<string[]> => {
};
return authWrapper<string[]>()(req);
};

export const getAllPurchasesForExport = async (
filters: FilteredPurchases,
total: number
): Promise<PurchaseWithLineItems[]> => {
const req = async (token: string): Promise<PurchaseWithLineItems[]> => {
const client = getClient();
const { data, error, response } = await client.GET("/purchase", {
headers: authHeader(token),
params: {
query: {
categories: filters.categories,
dateFrom: filters.dateFrom,
dateTo: filters.dateTo,
search: filters.search,
sortBy: filters.sortBy,
sortOrder: filters.sortOrder,
type: filters.type,
resultsPerPage: total,
},
},
});

if (response.ok) {
return data?.purchases || [];
} else {
throw Error(error?.error);
}
};

return authWrapper<PurchaseWithLineItems[]>()(req);
};
22 changes: 16 additions & 6 deletions frontend/app/expense-tracker/expense-table/expense-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Card, CardAction, CardContent, CardFooter, CardHeader, CardTitle } from
import { PurchaseSelections } from "@/types/claim";
import { FilteredPurchases, PurchaseLineItemType, PurchaseWithLineItems } from "@/types/purchase";
import { useQuery } from "@tanstack/react-query";
import { FileUp, Printer } from "lucide-react";
import { FileUp, Loader2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { FaExclamation } from "react-icons/fa6";
import { IoFilterOutline } from "react-icons/io5";
Expand All @@ -17,6 +17,7 @@ import ResultsPerPageSelect from "./ResultsPerPageSelect";
import ExpenseSideView from "./side-view";
import TableContent from "./table-content";
import { fetchAllCategories, fetchPurchases } from "@/api/purchase";
import { handleExportClick } from "./export";

interface ExpenseTableConfig {
title: string;
Expand All @@ -41,6 +42,7 @@ export default function ExpenseTable({
}: ExpenseTableConfig) {
const [filters, setFilters] = useState<FilteredPurchases>({ pageNumber: 0, resultsPerPage: 5 });
const [showFilters, setShowFilters] = useState(true);
const [isExporting, setIsExporting] = useState(false);

useEffect(() => {
if (filterPending) {
Expand Down Expand Up @@ -105,11 +107,19 @@ export default function ExpenseTable({
<IoFilterOutline className="h-4 w-4 text-black" />
Filters
</Button>
<Button size="icon" className="h-[34px] w-8 bg-slate rounded-full border-0">
<Printer className="h-4 w-4" />
</Button>
<Button size="icon" className="h-[34px] w-8 bg-slate rounded-full border-0">
<FileUp className="h-4 w-4" />
<Button
size="icon"
className="h-[2.125rem] w-8 bg-slate rounded-full border-0"
onClick={() =>
handleExportClick(setIsExporting, filters, purchases.data?.numPurchases)
}
disabled={isExporting || purchases.data?.numPurchases === 0}
>
{isExporting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<FileUp className="h-4 w-4" />
)}
</Button>
</div>
</CardAction>
Expand Down
54 changes: 54 additions & 0 deletions frontend/app/expense-tracker/expense-table/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { FilteredPurchases, PurchaseWithLineItems } from "@/types/purchase";
import Papa from "papaparse";
import { DISASTER_TYPE_LABELS } from "@/types/disaster";
import { getAllPurchasesForExport } from "@/api/purchase";

const handleCSVCreation = (purchases: PurchaseWithLineItems[]) => {
const data = purchases.flatMap((p) => {
if (p.lineItems.length === 0) {
return [
{
Merchant: p.vendor || "Unknown Vendor",
"Purchase Amount": (p.totalAmountCents / 100).toFixed(2),
"Purchase Date": new Date(p.dateCreated).toLocaleDateString(),
"Line Item": "No line items",
"Line Item Amount": "N/A",
Category: "Undefined",
"Disaster Related": "Non-Disaster",
},
];
}

return p.lineItems.map((li) => ({
Merchant: p.vendor || "Unknown Vendor",
"Purchase Amount": (p.totalAmountCents / 100).toFixed(2),
"Purchase Date": new Date(p.dateCreated).toLocaleDateString(),
"Line Item": li.description || "Unknown Item",
"Line Item Amount": (li.amountCents / 100).toFixed(2),
Category: li.category || "Undefined",
"Disaster Related": DISASTER_TYPE_LABELS.get(li.type || "pending") ?? "Undefined",
}));
});

const csv = Papa.unparse(data);

const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `business-transactions.csv`;
link.click();
URL.revokeObjectURL(link.href);
};

export const handleExportClick = async (
setIsExporting: (bool: boolean) => void,
filters: FilteredPurchases,
total?: number
) => {
if (total) {
setIsExporting(true);
const allPurchases = await getAllPurchasesForExport(filters, total);
handleCSVCreation(allPurchases);
setIsExporting(false);
}
};
Loading