mirror of
https://github.com/marcogll/TaxHacker_s23.git
synced 2026-01-13 21:35:19 +00:00
(squash) init
feat: filters, settings, backups fix: ts compile errors feat: new dashboard, webp previews and settings feat: use webp for pdfs feat: use webp fix: analyze resets old data fix: switch to corsproxy fix: switch to free cors fix: max upload limit fix: currency conversion feat: transaction export fix: currency conversion feat: refactor settings actions feat: new loader feat: README + LICENSE doc: update readme doc: update readme doc: update readme doc: update screenshots ci: bump prisma
This commit is contained in:
28
app/transactions/[transactionId]/layout.tsx
Normal file
28
app/transactions/[transactionId]/layout.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getTransactionById } from "@/data/transactions"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
export default async function TransactionLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ transactionId: string }>
|
||||
}) {
|
||||
const { transactionId } = await params
|
||||
const transaction = await getTransactionById(transactionId)
|
||||
|
||||
if (!transaction) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Transaction Details</h2>
|
||||
</header>
|
||||
<main>
|
||||
<div className="flex flex-1 flex-col gap-4 pt-0">{children}</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
7
app/transactions/[transactionId]/loading.tsx
Normal file
7
app/transactions/[transactionId]/loading.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<Skeleton className="flex flex-row flex-wrap md:flex-nowrap justify-center items-start gap-10 p-5 bg-accent max-w-[1200px] min-h-[800px]" />
|
||||
)
|
||||
}
|
||||
62
app/transactions/[transactionId]/page.tsx
Normal file
62
app/transactions/[transactionId]/page.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { FormTextarea } from "@/components/forms/simple"
|
||||
import TransactionEditForm from "@/components/transactions/edit"
|
||||
import TransactionFiles from "@/components/transactions/transaction-files"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { getCategories } from "@/data/categories"
|
||||
import { getCurrencies } from "@/data/currencies"
|
||||
import { getFields } from "@/data/fields"
|
||||
import { getFilesByTransactionId } from "@/data/files"
|
||||
import { getProjects } from "@/data/projects"
|
||||
import { getSettings } from "@/data/settings"
|
||||
import { getTransactionById } from "@/data/transactions"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
export default async function TransactionPage({ params }: { params: Promise<{ transactionId: string }> }) {
|
||||
const { transactionId } = await params
|
||||
const transaction = await getTransactionById(transactionId)
|
||||
if (!transaction) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const files = await getFilesByTransactionId(transactionId)
|
||||
const categories = await getCategories()
|
||||
const currencies = await getCurrencies()
|
||||
const settings = await getSettings()
|
||||
const fields = await getFields()
|
||||
const projects = await getProjects()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="flex flex-col md:flex-row flex-wrap justify-center items-start gap-10 p-5 bg-accent max-w-6xl">
|
||||
<div className="flex-1">
|
||||
<TransactionEditForm
|
||||
transaction={transaction}
|
||||
categories={categories}
|
||||
currencies={currencies}
|
||||
settings={settings}
|
||||
fields={fields}
|
||||
projects={projects}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-[320px] space-y-4">
|
||||
<TransactionFiles transaction={transaction} files={files} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{transaction.text && (
|
||||
<Card className="flex items-stretch p-5 mt-10 max-w-6xl">
|
||||
<div className="flex-1">
|
||||
<FormTextarea
|
||||
title="Recognized Text"
|
||||
name="text"
|
||||
defaultValue={transaction.text || ""}
|
||||
hideIfEmpty={true}
|
||||
className="w-full h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
143
app/transactions/actions.ts
Normal file
143
app/transactions/actions.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
"use server"
|
||||
|
||||
import { createFile, deleteFile } from "@/data/files"
|
||||
import {
|
||||
createTransaction,
|
||||
deleteTransaction,
|
||||
getTransactionById,
|
||||
updateTransaction,
|
||||
updateTransactionFiles,
|
||||
} from "@/data/transactions"
|
||||
import { transactionFormSchema } from "@/forms/transactions"
|
||||
import { FILE_UPLOAD_PATH, getTransactionFileUploadPath } from "@/lib/files"
|
||||
import { existsSync } from "fs"
|
||||
import { mkdir, writeFile } from "fs/promises"
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
export async function createTransactionAction(prevState: any, formData: FormData) {
|
||||
try {
|
||||
const validatedForm = transactionFormSchema.safeParse(Object.fromEntries(formData.entries()))
|
||||
|
||||
if (!validatedForm.success) {
|
||||
return { success: false, error: validatedForm.error.message }
|
||||
}
|
||||
|
||||
const transaction = await createTransaction(validatedForm.data)
|
||||
|
||||
revalidatePath("/transactions")
|
||||
return { success: true, transactionId: transaction.id }
|
||||
} catch (error) {
|
||||
console.error("Failed to create transaction:", error)
|
||||
return { success: false, error: "Failed to create transaction" }
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveTransactionAction(prevState: any, formData: FormData) {
|
||||
try {
|
||||
const transactionId = formData.get("transactionId") as string
|
||||
const validatedForm = transactionFormSchema.safeParse(Object.fromEntries(formData.entries()))
|
||||
|
||||
if (!validatedForm.success) {
|
||||
return { success: false, error: validatedForm.error.message }
|
||||
}
|
||||
|
||||
const transaction = await updateTransaction(transactionId, validatedForm.data)
|
||||
|
||||
revalidatePath("/transactions")
|
||||
return { success: true, transactionId: transaction.id }
|
||||
} catch (error) {
|
||||
console.error("Failed to update transaction:", error)
|
||||
return { success: false, error: "Failed to save transaction" }
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTransactionAction(prevState: any, transactionId: string) {
|
||||
try {
|
||||
const transaction = await getTransactionById(transactionId)
|
||||
if (!transaction) throw new Error("Transaction not found")
|
||||
|
||||
await deleteTransaction(transaction.id)
|
||||
|
||||
revalidatePath("/transactions")
|
||||
|
||||
return { success: true, transactionId: transaction.id }
|
||||
} catch (error) {
|
||||
console.error("Failed to delete transaction:", error)
|
||||
return { success: false, error: "Failed to delete transaction" }
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTransactionFileAction(
|
||||
transactionId: string,
|
||||
fileId: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
if (!fileId || !transactionId) {
|
||||
return { success: false, error: "File ID and transaction ID are required" }
|
||||
}
|
||||
|
||||
const transaction = await getTransactionById(transactionId)
|
||||
if (!transaction) {
|
||||
return { success: false, error: "Transaction not found" }
|
||||
}
|
||||
|
||||
await updateTransactionFiles(
|
||||
transactionId,
|
||||
transaction.files ? (transaction.files as string[]).filter((id) => id !== fileId) : []
|
||||
)
|
||||
|
||||
await deleteFile(fileId)
|
||||
revalidatePath(`/transactions/${transactionId}`)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function uploadTransactionFileAction(formData: FormData): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const transactionId = formData.get("transactionId") as string
|
||||
const file = formData.get("file") as File
|
||||
|
||||
if (!file || !transactionId) {
|
||||
return { success: false, error: "No file or transaction ID provided" }
|
||||
}
|
||||
|
||||
const transaction = await getTransactionById(transactionId)
|
||||
if (!transaction) {
|
||||
return { success: false, error: "Transaction not found" }
|
||||
}
|
||||
|
||||
// Make sure upload dir exists
|
||||
if (!existsSync(FILE_UPLOAD_PATH)) {
|
||||
await mkdir(FILE_UPLOAD_PATH, { recursive: true })
|
||||
}
|
||||
|
||||
// Save file to filesystem
|
||||
const { fileUuid, filePath } = await getTransactionFileUploadPath(file.name, transaction)
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
await writeFile(filePath, buffer)
|
||||
|
||||
// Create file record in database
|
||||
const fileRecord = await createFile({
|
||||
id: fileUuid,
|
||||
filename: file.name,
|
||||
path: filePath,
|
||||
mimetype: file.type,
|
||||
isReviewed: true,
|
||||
metadata: {
|
||||
size: file.size,
|
||||
lastModified: file.lastModified,
|
||||
},
|
||||
})
|
||||
|
||||
// Update invoice with the new file ID
|
||||
await updateTransactionFiles(
|
||||
transactionId,
|
||||
transaction.files ? [...(transaction.files as string[]), fileRecord.id] : [fileRecord.id]
|
||||
)
|
||||
|
||||
revalidatePath(`/transactions/${transactionId}`)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error)
|
||||
return { success: false, error: `File upload failed: ${error}` }
|
||||
}
|
||||
}
|
||||
3
app/transactions/layout.tsx
Normal file
3
app/transactions/layout.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default async function TransactionsLayout({ children }: { children: React.ReactNode }) {
|
||||
return <div className="flex flex-col gap-4 p-4">{children}</div>
|
||||
}
|
||||
30
app/transactions/loading.tsx
Normal file
30
app/transactions/loading.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Download, Plus } from "lucide-react"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<>
|
||||
<header className="flex items-center justify-between mb-12">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Transactions</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<Download />
|
||||
Export
|
||||
</Button>
|
||||
<Button>
|
||||
<Plus /> Add Transaction
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-8" />
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
71
app/transactions/page.tsx
Normal file
71
app/transactions/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ExportTransactionsDialog } from "@/components/export/transactions"
|
||||
import { UploadButton } from "@/components/files/upload-button"
|
||||
import { TransactionSearchAndFilters } from "@/components/transactions/filters"
|
||||
import { TransactionList } from "@/components/transactions/list"
|
||||
import { NewTransactionDialog } from "@/components/transactions/new"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { getCategories } from "@/data/categories"
|
||||
import { getFields } from "@/data/fields"
|
||||
import { getProjects } from "@/data/projects"
|
||||
import { getTransactions, TransactionFilters } from "@/data/transactions"
|
||||
import { Download, Plus, Upload } from "lucide-react"
|
||||
import { Metadata } from "next"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Transactions",
|
||||
description: "Manage your transactions",
|
||||
}
|
||||
|
||||
export default async function TransactionsPage({ searchParams }: { searchParams: Promise<TransactionFilters> }) {
|
||||
const filters = await searchParams
|
||||
const transactions = await getTransactions(filters)
|
||||
const categories = await getCategories()
|
||||
const projects = await getProjects()
|
||||
const fields = await getFields()
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="flex flex-wrap items-center justify-between gap-2 mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Transactions</h2>
|
||||
<div className="flex gap-2">
|
||||
<ExportTransactionsDialog filters={filters} fields={fields} categories={categories} projects={projects}>
|
||||
<Button variant="outline">
|
||||
<Download />
|
||||
<span className="hidden md:block">Export</span>
|
||||
</Button>
|
||||
</ExportTransactionsDialog>
|
||||
<NewTransactionDialog>
|
||||
<Button>
|
||||
<Plus /> <span className="hidden md:block">Add Transaction</span>
|
||||
</Button>
|
||||
</NewTransactionDialog>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<TransactionSearchAndFilters categories={categories} projects={projects} />
|
||||
|
||||
<main>
|
||||
<TransactionList transactions={transactions} />
|
||||
|
||||
{transactions.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center gap-2 h-full min-h-[400px]">
|
||||
<p className="text-muted-foreground">
|
||||
You don't seem to have any transactions yet. Let's start and create the first one!
|
||||
</p>
|
||||
<div className="flex flex-row gap-5 mt-8">
|
||||
<UploadButton>
|
||||
<Upload /> Analyze New Invoice
|
||||
</UploadButton>
|
||||
<NewTransactionDialog>
|
||||
<Button variant="outline">
|
||||
<Plus />
|
||||
Add Manually
|
||||
</Button>
|
||||
</NewTransactionDialog>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user