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

Import Bank Statements #256

Merged
merged 4 commits into from
Jul 23, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ All seamlessly integrated with the Badget to accelerate the development.

## Directory Structure

Dingify is a monorepo managed by [Turborepo](https://turbo.build/repo). The monorepo is split between `apps` and `packages` directories.
Badget is a monorepo managed by [Turborepo](https://turbo.build/repo). The monorepo is split between `apps` and `packages` directories.

.
β”œβ”€β”€ apps # Its app workspace which contains
Expand Down
1 change: 1 addition & 0 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"react": "18.2.0",
"react-activity-calendar": "^2.2.8",
"react-copy-to-clipboard": "^5.1.0",
"react-csv-importer": "^0.8.1",
"react-day-picker": "^8.9.1",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
Expand Down
11 changes: 11 additions & 0 deletions apps/www/public/testing.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"Date","Name","Amount"
"2016-01-01","Walmart","10.00"
"2016-01-01","Metro Transit","5.00"
"2016-01-02","Costco","20.00"
"2016-01-02","Uber","10.00"
"2016-01-03","Chipotle","30.00"
"2016-01-03","Amtrak","15.00"
"2016-01-04","CVS Pharmacy","40.00"
"2016-01-04","Shell Gas Station","20.00"
"2016-01-05","Nike Store","50.00"
"2016-01-05","City Parking","25.00"
62 changes: 14 additions & 48 deletions apps/www/src/actions/create-populate-transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,54 +23,20 @@ export async function createPopulateTransactions(bankAccountId: string) {
},
});

// Categories to be created
const categories = [
{ name: "Car", icon: "πŸš—" },
{ name: "Transportation", icon: "🚌" },
{ name: "Clothing", icon: "πŸ‘—" },
{ name: "Entertainment", icon: "🎬" },
{ name: "Groceries", icon: "πŸ₯‘" },
{ name: "Other", icon: "πŸ”§" },
{ name: "Rent", icon: "🏠" },
{ name: "Restaurants", icon: "🍽️" },
{ name: "Shops", icon: "πŸ›οΈ" },
{ name: "Subscriptions", icon: "πŸ“Ί" },
{ name: "Utilities", icon: "πŸ’‘" },
];

// Populate categories with userId
await Promise.all(
categories.map(async (category) => {
await prisma.category.upsert({
where: {
name_userId: {
name: category.name,
userId: user.id,
},
},
update: {},
create: {
name: category.name,
icon: category.icon,
userId: user.id,
},
});
}),
);

// Fetch category IDs
const createdCategories = await prisma.category.findMany({
// Fetch existing categories
const categories = await prisma.category.findMany({
where: { userId: user.id },
});
const categoryMap = createdCategories.reduce((acc, category) => {

const categoryMap = categories.reduce((acc, category) => {
acc[category.name] = category.id;
return acc;
}, {});

// Sample transactions with various realistic descriptions, dates, and categories
const transactions = [
{
amount: 45.99,
amount: 46,
date: new Date(),
description: "Groceries at Walmart",
categoryId: categoryMap["Groceries"],
Expand All @@ -79,7 +45,7 @@ export async function createPopulateTransactions(bankAccountId: string) {
review: true, // Mark as reviewed
},
{
amount: 16.75,
amount: 17,
date: subDays(new Date(), 1), // Yesterday
description: "Lunch at Chipotle",
categoryId: categoryMap["Restaurants"],
Expand All @@ -88,63 +54,63 @@ export async function createPopulateTransactions(bankAccountId: string) {
review: true, // Mark as reviewed
},
{
amount: 9.99,
amount: 10,
date: subDays(new Date(), 2),
description: "Netflix Subscription",
categoryId: categoryMap["Subscriptions"],
accountId: bankAccountId,
currencyIso: currencyIso,
},
{
amount: 55.2,
amount: 55,
date: subDays(new Date(), 3),
description: "Gas at Chevron",
categoryId: categoryMap["Car"],
accountId: bankAccountId,
currencyIso: currencyIso,
},
{
amount: 120.0,
amount: 120,
date: subDays(new Date(), 4),
description: "New Shoes from Nike",
categoryId: categoryMap["Shops"],
accountId: bankAccountId,
currencyIso: currencyIso,
},
{
amount: 2000.0,
amount: 2000,
date: subDays(new Date(), 5),
description: "Monthly Salary",
categoryId: categoryMap["Income"],
accountId: bankAccountId,
currencyIso: currencyIso,
},
{
amount: 35.5,
amount: 36,
date: subDays(new Date(), 6),
description: "Dinner at Olive Garden",
categoryId: categoryMap["Restaurants"],
accountId: bankAccountId,
currencyIso: currencyIso,
},
{
amount: 15.0,
amount: 15,
date: subDays(new Date(), 7),
description: "Movie Ticket",
categoryId: categoryMap["Entertainment"],
accountId: bankAccountId,
currencyIso: currencyIso,
},
{
amount: 89.99,
amount: 90,
date: subDays(new Date(), 8),
description: "Electricity Bill",
categoryId: categoryMap["Utilities"],
accountId: bankAccountId,
currencyIso: currencyIso,
},
{
amount: 50.0,
amount: 50,
date: subDays(new Date(), 9),
description: "Gym Membership",
categoryId: categoryMap["Fitness"],
Expand Down
87 changes: 87 additions & 0 deletions apps/www/src/actions/import-transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use server";

import { prisma } from "@/lib/db";
import { getCurrentUser } from "@/lib/session";

interface Transaction {
date: string;
description: string;
amount: number;
category: string;
}

export async function importTransactions(
bankAccountId: string,
transactions: Transaction[],
) {
const user = await getCurrentUser();
if (!user) {
throw new Error("User not authenticated");
}

const currencyIso = "USD"; // Set the currency ISO code according to your requirements

// Ensure default categories are created
const defaultCategories = [
{ name: "Car", icon: "πŸš—" },
{ name: "Transportation", icon: "🚌" },
{ name: "Clothing", icon: "πŸ‘—" },
{ name: "Entertainment", icon: "🎬" },
{ name: "Groceries", icon: "πŸ₯‘" },
{ name: "Other", icon: "πŸ”§" },
{ name: "Rent", icon: "🏠" },
{ name: "Restaurants", icon: "🍽️" },
{ name: "Shops", icon: "πŸ›οΈ" },
{ name: "Subscriptions", icon: "πŸ“Ί" },
{ name: "Utilities", icon: "πŸ’‘" },
];

await Promise.all(
defaultCategories.map(async (category) => {
await prisma.category.upsert({
where: {
name_userId: {
name: category.name,
userId: user.id,
},
},
update: {},
create: {
name: category.name,
icon: category.icon,
userId: user.id,
},
});
}),
);

// Fetch or create categories
const categories = await prisma.category.findMany({
where: { userId: user.id },
});

const categoryMap = categories.reduce((acc, category) => {
acc[category.name] = category.id;
return acc;
}, {});

// Prepare transactions for insertion
const transactionsData = transactions.map((transaction) => ({
amount: transaction.amount,
date: new Date(transaction.date),
description: transaction.description,
categoryId: categoryMap[transaction.category] || categoryMap["Other"],
accountId: bankAccountId,
currencyIso: currencyIso,
}));

try {
await prisma.transaction.createMany({
data: transactionsData,
});
return { success: true };
} catch (error) {
console.error(error);
return { success: false, error: error.message };
}
}
6 changes: 4 additions & 2 deletions apps/www/src/app/(dashboard)/dashboard/banking/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import BankingDashboardDetails from "@/components/banking/BankingDashboardDetail
import { AddTransactionsButton } from "@/components/buttons/AddTransactionsButton";
import { DashboardHeader } from "@/components/dashboard/header";
import { DashboardShell } from "@/components/dashboard/shell";
import { CSVUploader } from "@/components/import/CsvImporter";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";

export default async function BankAccountPage({
Expand All @@ -30,7 +31,6 @@ export default async function BankAccountPage({
const bankAccountDetails = await getBankAccountDetails(bankAccountId);
const transactions = await getBankAccountTransactions(bankAccountId);
console.log(transactions);
console.log(bankAccountDetails);

if (!bankAccountDetails) {
return (
Expand All @@ -48,7 +48,9 @@ export default async function BankAccountPage({
<DashboardHeader
heading={bankAccountDetails.name}
text="Here are your recent transactions"
/>
>
<CSVUploader bankAccountId={bankAccountId} />
</DashboardHeader>
<div>
{transactions.length === 0 ? (
<EmptyPlaceholder>
Expand Down
1 change: 1 addition & 0 deletions apps/www/src/app/(dashboard)/dashboard/banking/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AreaChartBanking } from "@/components/charts/AreaChart";
import { OverallUseageChart } from "@/components/charts/OverallUseageChart";
import { DashboardHeader } from "@/components/dashboard/header";
import { DashboardShell } from "@/components/dashboard/shell";
import { CSVUploader } from "@/components/import/CsvImporter";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";

export const metadata = {
Expand Down
69 changes: 69 additions & 0 deletions apps/www/src/components/import/CsvImporter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import * as React from "react";

import { Button } from "@dingify/ui/components/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@dingify/ui/components/dialog";

import { cn } from "@/lib/utils";

import CSVParser from "./generic-csv-parser";

export function CSVUploader({ bankAccountId }: { bankAccountId: string }) {
const handleTestFileImport = () => {
// Create a link element
const link = document.createElement("a");
// Set the URL to the test file
link.href = "/testing.csv";
// Set the download attribute to trigger download
link.download = "testing.csv";
// Append link to the body
document.body.appendChild(link);
// Trigger click event on the link
link.click();
// Remove link from body
document.body.removeChild(link);
};

return (
<Dialog>
<DialogTrigger asChild>
<Button>Import File</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>Import File</DialogTitle>
<DialogDescription>
Export transactions data as a file from bank's website.
</DialogDescription>
</DialogHeader>
<UploadForm bankAccountId={bankAccountId} />
<DialogFooter>
<Button onClick={handleTestFileImport}>Download Test File</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

function UploadForm({
className,
bankAccountId,
}: {
className?: string;
bankAccountId: string;
}) {
return (
<div className={cn("items-center py-4", className)}>
<CSVParser bankAccountId={bankAccountId} />
</div>
);
}
Loading