(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:
Vasily Zubarev
2025-03-13 00:30:47 +01:00
commit 0b98a2c307
153 changed files with 17271 additions and 0 deletions

63
app/unsorted/actions.ts Normal file
View File

@@ -0,0 +1,63 @@
"use server"
import { deleteFile, getFileById, updateFile } from "@/data/files"
import { createTransaction, updateTransactionFiles } from "@/data/transactions"
import { transactionFormSchema } from "@/forms/transactions"
import { getTransactionFileUploadPath } from "@/lib/files"
import { mkdir, rename } from "fs/promises"
import { revalidatePath } from "next/cache"
import path from "path"
export async function saveFileAsTransactionAction(prevState: any, formData: FormData) {
try {
const validatedForm = transactionFormSchema.safeParse(Object.fromEntries(formData.entries()))
if (!validatedForm.success) {
return { success: false, error: validatedForm.error.message }
}
// Get the file record
const fileId = formData.get("fileId") as string
const file = await getFileById(fileId)
if (!file) throw new Error("File not found")
// Create transaction
const transaction = await createTransaction(validatedForm.data)
// Move file to processed location
const originalFileName = path.basename(file.path)
const { fileUuid, filePath: newFilePath } = await getTransactionFileUploadPath(originalFileName, transaction)
// Move file to new location and name
await mkdir(path.dirname(newFilePath), { recursive: true })
await rename(path.resolve(file.path), path.resolve(newFilePath))
// Update file record
await updateFile(file.id, {
id: fileUuid,
path: newFilePath,
isReviewed: true,
})
await updateTransactionFiles(transaction.id, [fileUuid])
revalidatePath("/unsorted")
revalidatePath("/transactions")
return { success: true, transactionId: transaction.id }
} catch (error) {
console.error("Failed to save transaction:", error)
return { success: false, error: `Failed to save transaction: ${error}` }
}
}
export async function deleteUnsortedFileAction(prevState: any, fileId: string) {
try {
await deleteFile(fileId)
revalidatePath("/unsorted")
return { success: true }
} catch (error) {
console.error("Failed to delete file:", error)
return { success: false, error: "Failed to delete file" }
}
}

3
app/unsorted/layout.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function UnsortedLayout({ children }: { children: React.ReactNode }) {
return <div className="flex flex-col gap-4 p-4 max-w-6xl">{children}</div>
}

103
app/unsorted/page.tsx Normal file
View File

@@ -0,0 +1,103 @@
import AnalyzeForm from "@/components/unsorted/analyze-form"
import { FilePreview } from "@/components/files/preview"
import { UploadButton } from "@/components/files/upload-button"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { getCategories } from "@/data/categories"
import { getCurrencies } from "@/data/currencies"
import { getFields } from "@/data/fields"
import { getUnsortedFiles } from "@/data/files"
import { getProjects } from "@/data/projects"
import { getSettings } from "@/data/settings"
import { FileText, PartyPopper, Settings, Upload } from "lucide-react"
import { Metadata } from "next"
import Link from "next/link"
export const metadata: Metadata = {
title: "Unsorted",
description: "Analyze unsorted files",
}
export default async function UnsortedPage() {
const files = await getUnsortedFiles()
const categories = await getCategories()
const projects = await getProjects()
const currencies = await getCurrencies()
const fields = await getFields()
const settings = await getSettings()
return (
<>
<header className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight">You have {files.length} unsorted files</h2>
</header>
{!settings.openai_api_key && (
<Alert>
<Settings className="h-4 w-4 mt-2" />
<div className="flex flex-row justify-between pt-2">
<div className="flex flex-col">
<AlertTitle>ChatGPT API Key is required for analyzing files</AlertTitle>
<AlertDescription>
Please set your OpenAI API key in the settings to use the analyze form.
</AlertDescription>
</div>
<Link href="/settings/llm">
<Button>Go to Settings</Button>
</Link>
</div>
</Alert>
)}
<main className="flex flex-col gap-5">
{files.map((file) => (
<Card
key={file.id}
id={file.id}
className="flex flex-row flex-wrap md:flex-nowrap justify-center items-start gap-5 p-5 bg-accent"
>
<div className="w-full max-w-[500px]">
<Card>
<FilePreview file={file} />
</Card>
</div>
<div className="w-full">
<AnalyzeForm
file={file}
categories={categories}
projects={projects}
currencies={currencies}
fields={fields}
settings={settings}
/>
</div>
</Card>
))}
{files.length == 0 && (
<div className="flex flex-col items-center justify-center gap-2 h-full min-h-[600px]">
<PartyPopper className="w-12 h-12 text-muted-foreground" />
<p className="pt-4 text-muted-foreground">Everything is clear! Congrats!</p>
<p className="flex flex-row gap-2 text-muted-foreground">
<span>Drag and drop new files here to analyze</span>
<Upload />
</p>
<div className="flex flex-row gap-5 mt-8">
<UploadButton>
<Upload /> Upload New File
</UploadButton>
<Button variant="outline" asChild>
<Link href="/transactions">
<FileText />
Go to Transactions
</Link>
</Button>
</div>
</div>
)}
</main>
</>
)
}