Skip to content

Commit 14967e1

Browse files
committed
feat: bugfixes, spedup, bulk actions,
1 parent b27f070 commit 14967e1

34 files changed

+434
-226
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ next-env.d.ts
4949
*.db
5050
*.sqlite
5151
*.sqlite3
52+
*.sqlite-journal
53+
*.db-journal

README.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ A built-in system of powerful filters allows you to then export transactions wit
3131

3232
> \[!NOTE]
3333
>
34-
> TaxHacker is a single-user app. SaaS version will probably appear in the future if anyone is interested. Stay tuned for updates.
34+
> TaxHacker is a single-user app. SaaS or Electron version will probably be developed in the future if anyone is interested.
3535
3636
> \[!IMPORTANT]
3737
>
38-
> This project is still at a very early stage. **Star Us** to receive new release notifications from GitHub ⭐️
38+
> This project is still at a very early stage. Use it at your own risk! **Star Us** to receive notifications about new bugfixes and features from GitHub ⭐️
3939
4040
## ✨ Features
4141

@@ -45,10 +45,11 @@ A built-in system of powerful filters allows you to then export transactions wit
4545

4646
Take a photo on upload or a PDF and TaxHacker will automatically recognise, categorise and store transaction information.
4747

48-
- Extracts key information like date, amount, and vendor
49-
- Categorizes transactions based on content
50-
- Stores everything in a structured format for easy filtering and retrieval
51-
- Organizes documents for tax season
48+
- Upload multiple documents and store in “unsorted” until you get the time to sort them out with AI
49+
- Use LLM to extract key information like date, amount, and vendor
50+
- Categorize transactions based on content
51+
- Store everything in a structured format for easy filtering and retrieval
52+
- Organize your documents by a tax season
5253

5354
TaxHacker recognizes a wide variety of documents including store receipts, restaurant bills, invoices, bank checks, letters, even handwritten receipts.
5455

@@ -58,8 +59,8 @@ TaxHacker recognizes a wide variety of documents including store receipts, resta
5859

5960
TaxHacker automatically converts foreign currencies and even knows the historical exchange rates on the invoice date.
6061

61-
- Automatic detection of different currencies
62-
- Real-time currency conversion to your base currency
62+
- Automatically detect currency in your documents
63+
- Convert it to your base currency
6364
- Historical exchange rate lookup for past transactions
6465
- Support for over 170 world currencies and 14 popular cryptocurrencies (BTC, ETC, LTC, DOT, etc)!
6566

@@ -110,7 +111,7 @@ TaxHacker can be self-hosted on your own infrastructure for complete control ove
110111

111112
Deploy your own instance of TaxHacker with Vercel in just a few clicks:
112113

113-
1. Prepare your OpenAI API Key for the AI features
114+
1. Prepare your [OpenAI API Key](https://platform.openai.com/settings/organization/api-keys) for the AI features
114115
2. Click the deploy button below
115116
3. Configure your environment variables in the Vercel dashboard
116117
4. (Optional) Connect your custom domain
@@ -124,11 +125,10 @@ Deploy your own instance of TaxHacker with Vercel in just a few clicks:
124125
For server deployment, we provide a [Docker image](./Dockerfile) and [Docker Compose](./docker-compose.yml) files that makes setting up TaxHacker simple:
125126

126127
```bash
127-
# Clone the repository
128-
git clone https://github.com/vas3k/TaxHacker.git
129-
cd TaxHacker
128+
# Download docker-compose.yml file
129+
curl -O https://raw.githubusercontent.com/vas3k/TaxHacker/main/docker-compose.yml
130130

131-
# Or use docker-compose (recommended)
131+
# Run it
132132
docker compose up
133133
```
134134

app/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { WelcomeWidget } from "@/components/dashboard/welcome-widget"
55
import { Separator } from "@/components/ui/separator"
66
import { getUnsortedFiles } from "@/data/files"
77
import { getSettings } from "@/data/settings"
8-
import { StatsFilters } from "@/data/stats"
8+
import { TransactionFilters } from "@/data/transactions"
99

10-
export default async function Home({ searchParams }: { searchParams: Promise<StatsFilters> }) {
10+
export default async function Home({ searchParams }: { searchParams: Promise<TransactionFilters> }) {
1111
const filters = await searchParams
1212
const unsortedFiles = await getUnsortedFiles()
1313
const settings = await getSettings()

app/settings/categories/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { addCategoryAction, deleteCategoryAction, editCategoryAction } from "@/app/settings/actions"
22
import { CrudTable } from "@/components/settings/crud"
33
import { getCategories } from "@/data/categories"
4+
import { randomHexColor } from "@/lib/utils"
45

56
export default async function CategoriesSettingsPage() {
67
const categories = await getCategories()
@@ -18,7 +19,7 @@ export default async function CategoriesSettingsPage() {
1819
columns={[
1920
{ key: "name", label: "Name", editable: true },
2021
{ key: "llm_prompt", label: "LLM Prompt", editable: true },
21-
{ key: "color", label: "Color", editable: true },
22+
{ key: "color", label: "Color", defaultValue: randomHexColor(), editable: true },
2223
]}
2324
onDelete={async (code) => {
2425
"use server"

app/settings/currencies/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default async function CurrenciesSettingsPage() {
1616
<CrudTable
1717
items={currenciesWithActions}
1818
columns={[
19-
{ key: "code", label: "Code" },
19+
{ key: "code", label: "Code", editable: true },
2020
{ key: "name", label: "Name", editable: true },
2121
]}
2222
onDelete={async (code) => {

app/settings/projects/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { addProjectAction, deleteProjectAction, editProjectAction } from "@/app/settings/actions"
22
import { CrudTable } from "@/components/settings/crud"
33
import { getProjects } from "@/data/projects"
4+
import { randomHexColor } from "@/lib/utils"
45

56
export default async function ProjectsSettingsPage() {
67
const projects = await getProjects()
@@ -18,7 +19,7 @@ export default async function ProjectsSettingsPage() {
1819
columns={[
1920
{ key: "name", label: "Name", editable: true },
2021
{ key: "llm_prompt", label: "LLM Prompt", editable: true },
21-
{ key: "color", label: "Color", editable: true },
22+
{ key: "color", label: "Color", defaultValue: randomHexColor(), editable: true },
2223
]}
2324
onDelete={async (code) => {
2425
"use server"

app/transactions/[transactionId]/page.tsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ export default async function TransactionPage({ params }: { params: Promise<{ tr
2626
const projects = await getProjects()
2727

2828
return (
29-
<>
30-
<Card className="flex flex-col md:flex-row flex-wrap justify-center items-start gap-10 p-5 bg-accent max-w-6xl">
31-
<div className="flex-1">
29+
<div className="flex flex-wrap flex-row items-start justify-center gap-4 max-w-6xl">
30+
<Card className="w-full flex-1 flex flex-col flex-wrap justify-center items-start gap-10 p-5 bg-accent">
31+
<div className="w-full">
3232
<TransactionEditForm
3333
transaction={transaction}
3434
categories={categories}
@@ -37,26 +37,29 @@ export default async function TransactionPage({ params }: { params: Promise<{ tr
3737
fields={fields}
3838
projects={projects}
3939
/>
40-
</div>
4140

42-
<div className="max-w-[320px] space-y-4">
43-
<TransactionFiles transaction={transaction} files={files} />
41+
{transaction.text && (
42+
<details className="mt-10">
43+
<summary className="cursor-pointer text-sm font-medium">Recognized Text</summary>
44+
<Card className="flex items-stretch p-2 max-w-6xl">
45+
<div className="flex-1">
46+
<FormTextarea
47+
title=""
48+
name="text"
49+
defaultValue={transaction.text || ""}
50+
hideIfEmpty={true}
51+
className="w-full h-[400px]"
52+
/>
53+
</div>
54+
</Card>
55+
</details>
56+
)}
4457
</div>
4558
</Card>
4659

47-
{transaction.text && (
48-
<Card className="flex items-stretch p-5 mt-10 max-w-6xl">
49-
<div className="flex-1">
50-
<FormTextarea
51-
title="Recognized Text"
52-
name="text"
53-
defaultValue={transaction.text || ""}
54-
hideIfEmpty={true}
55-
className="w-full h-[400px]"
56-
/>
57-
</div>
58-
</Card>
59-
)}
60-
</>
60+
<div className="w-1/3 max-w-[380px] space-y-4">
61+
<TransactionFiles transaction={transaction} files={files} />
62+
</div>
63+
</div>
6164
)
6265
}

app/transactions/actions.ts

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { createFile, deleteFile } from "@/data/files"
44
import {
5+
bulkDeleteTransactions,
56
createTransaction,
67
deleteTransaction,
78
getTransactionById,
@@ -90,13 +91,13 @@ export async function deleteTransactionFileAction(
9091
return { success: true }
9192
}
9293

93-
export async function uploadTransactionFileAction(formData: FormData): Promise<{ success: boolean; error?: string }> {
94+
export async function uploadTransactionFilesAction(formData: FormData): Promise<{ success: boolean; error?: string }> {
9495
try {
9596
const transactionId = formData.get("transactionId") as string
96-
const file = formData.get("file") as File
97+
const files = formData.getAll("files") as File[]
9798

98-
if (!file || !transactionId) {
99-
return { success: false, error: "No file or transaction ID provided" }
99+
if (!files || !transactionId) {
100+
return { success: false, error: "No files or transaction ID provided" }
100101
}
101102

102103
const transaction = await getTransactionById(transactionId)
@@ -109,29 +110,36 @@ export async function uploadTransactionFileAction(formData: FormData): Promise<{
109110
await mkdir(FILE_UPLOAD_PATH, { recursive: true })
110111
}
111112

112-
// Save file to filesystem
113-
const { fileUuid, filePath } = await getTransactionFileUploadPath(file.name, transaction)
114-
const arrayBuffer = await file.arrayBuffer()
115-
const buffer = Buffer.from(arrayBuffer)
116-
await writeFile(filePath, buffer)
117-
118-
// Create file record in database
119-
const fileRecord = await createFile({
120-
id: fileUuid,
121-
filename: file.name,
122-
path: filePath,
123-
mimetype: file.type,
124-
isReviewed: true,
125-
metadata: {
126-
size: file.size,
127-
lastModified: file.lastModified,
128-
},
129-
})
113+
const fileRecords = await Promise.all(
114+
files.map(async (file) => {
115+
const { fileUuid, filePath } = await getTransactionFileUploadPath(file.name, transaction)
116+
const arrayBuffer = await file.arrayBuffer()
117+
const buffer = Buffer.from(arrayBuffer)
118+
await writeFile(filePath, buffer)
119+
120+
// Create file record in database
121+
const fileRecord = await createFile({
122+
id: fileUuid,
123+
filename: file.name,
124+
path: filePath,
125+
mimetype: file.type,
126+
isReviewed: true,
127+
metadata: {
128+
size: file.size,
129+
lastModified: file.lastModified,
130+
},
131+
})
132+
133+
return fileRecord
134+
})
135+
)
130136

131137
// Update invoice with the new file ID
132138
await updateTransactionFiles(
133139
transactionId,
134-
transaction.files ? [...(transaction.files as string[]), fileRecord.id] : [fileRecord.id]
140+
transaction.files
141+
? [...(transaction.files as string[]), ...fileRecords.map((file) => file.id)]
142+
: fileRecords.map((file) => file.id)
135143
)
136144

137145
revalidatePath(`/transactions/${transactionId}`)
@@ -141,3 +149,14 @@ export async function uploadTransactionFileAction(formData: FormData): Promise<{
141149
return { success: false, error: `File upload failed: ${error}` }
142150
}
143151
}
152+
153+
export async function bulkDeleteTransactionsAction(transactionIds: string[]) {
154+
try {
155+
await bulkDeleteTransactions(transactionIds)
156+
revalidatePath("/transactions")
157+
return { success: true }
158+
} catch (error) {
159+
console.error("Failed to delete transactions:", error)
160+
return { success: false, error: "Failed to delete transactions" }
161+
}
162+
}

app/transactions/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default async function TransactionsPage({ searchParams }: { searchParams:
2828
<header className="flex flex-wrap items-center justify-between gap-2 mb-8">
2929
<h2 className="text-3xl font-bold tracking-tight">Transactions</h2>
3030
<div className="flex gap-2">
31-
<ExportTransactionsDialog filters={filters} fields={fields} categories={categories} projects={projects}>
31+
<ExportTransactionsDialog fields={fields} categories={categories} projects={projects}>
3232
<Button variant="outline">
3333
<Download />
3434
<span className="hidden md:block">Export</span>

app/unsorted/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import AnalyzeForm from "@/components/unsorted/analyze-form"
21
import { FilePreview } from "@/components/files/preview"
32
import { UploadButton } from "@/components/files/upload-button"
43
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
54
import { Button } from "@/components/ui/button"
65
import { Card } from "@/components/ui/card"
6+
import AnalyzeForm from "@/components/unsorted/analyze-form"
77
import { getCategories } from "@/data/categories"
88
import { getCurrencies } from "@/data/currencies"
99
import { getFields } from "@/data/fields"

0 commit comments

Comments
 (0)