Files
TaxHacker_s23/components/unsorted/analyze-form.tsx
Vasily Zubarev 0b98a2c307 (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
2025-03-16 21:29:20 +01:00

289 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { analyzeTransaction, retrieveAllAttachmentsForAI } from "@/app/ai/analyze"
import { useNotification } from "@/app/context"
import { deleteUnsortedFileAction, saveFileAsTransactionAction } from "@/app/unsorted/actions"
import { FormSelectCategory } from "@/components/forms/select-category"
import { FormSelectCurrency } from "@/components/forms/select-currency"
import { FormSelectProject } from "@/components/forms/select-project"
import { FormSelectType } from "@/components/forms/select-type"
import { FormInput, FormTextarea } from "@/components/forms/simple"
import { Button } from "@/components/ui/button"
import { Category, Currency, Field, File, Project } from "@prisma/client"
import { Brain, Loader2 } from "lucide-react"
import { startTransition, useActionState, useState } from "react"
import { FormConvertCurrency } from "../forms/convert-currency"
export default function AnalyzeForm({
file,
categories,
projects,
currencies,
fields,
settings,
}: {
file: File
categories: Category[]
projects: Project[]
currencies: Currency[]
fields: Field[]
settings: Record<string, string>
}) {
const { showNotification } = useNotification()
const [isAnalyzing, setIsAnalyzing] = useState(false)
const [analyzeStep, setAnalyzeStep] = useState<string>("")
const [analyzeError, setAnalyzeError] = useState<string>("")
const [deleteState, deleteAction, isDeleting] = useActionState(deleteUnsortedFileAction, null)
const [isSaving, setIsSaving] = useState(false)
const [saveError, setSaveError] = useState("")
const extraFields = fields.filter((field) => field.isExtra)
const [formData, setFormData] = useState({
name: file.filename,
merchant: "",
description: "",
type: settings.default_type,
total: 0.0,
currencyCode: settings.default_currency,
convertedTotal: 0.0,
convertedCurrencyCode: settings.default_currency,
categoryCode: settings.default_category,
projectCode: settings.default_project,
issuedAt: "",
note: "",
text: "",
...extraFields.reduce((acc, field) => {
acc[field.code] = ""
return acc
}, {} as Record<string, string>),
})
async function saveAsTransaction(formData: FormData) {
setSaveError("")
setIsSaving(true)
startTransition(async () => {
const result = await saveFileAsTransactionAction(null, formData)
setIsSaving(false)
if (result.success) {
showNotification({ code: "sidebar.transactions", message: "new" })
setTimeout(() => showNotification({ code: "sidebar.transactions", message: "" }), 3000)
} else {
setSaveError(result.error ? result.error : "Something went wrong...")
}
})
}
const startAnalyze = async () => {
setIsAnalyzing(true)
setAnalyzeStep("Retrieving files...")
setAnalyzeError("")
try {
const attachments = await retrieveAllAttachmentsForAI(file)
setAnalyzeStep("Analyzing...")
const results = await analyzeTransaction(
settings.prompt_analyse_new_file || process.env.PROMPT_ANALYSE_NEW_FILE || "",
settings,
fields,
categories,
projects,
attachments
)
console.log("Analysis results:", results)
if (!results.success) {
setAnalyzeError(results.error ? results.error : "Something went wrong...")
} else {
const nonEmptyFields = Object.fromEntries(
Object.entries(results.data || {}).filter(
([_, value]) => value !== null && value !== undefined && value !== ""
)
)
console.log("Setting form data:", nonEmptyFields)
setFormData({ ...formData, ...nonEmptyFields })
}
} catch (error) {
console.error("Analysis failed:", error)
setAnalyzeError(error instanceof Error ? error.message : "Analysis failed")
} finally {
setIsAnalyzing(false)
setAnalyzeStep("")
}
}
return (
<>
<Button className="w-full mb-6 py-6 text-lg" onClick={startAnalyze} disabled={isAnalyzing}>
{isAnalyzing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>{analyzeStep}</span>
</>
) : (
<>
<Brain className="mr-2 h-4 w-4" />
<span>Analyze with AI</span>
</>
)}
</Button>
{analyzeError && <div className="mb-6 p-4 text-red-500 bg-red-50 rounded-md"> {analyzeError}</div>}
<form className="space-y-4" action={saveAsTransaction}>
<input type="hidden" name="fileId" value={file.id} />
<FormInput
title="Name"
name="name"
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
/>
<FormInput
title="Merchant"
name="merchant"
value={formData.merchant}
onChange={(e) => setFormData((prev) => ({ ...prev, merchant: e.target.value }))}
/>
<FormInput
title="Description"
name="description"
value={formData.description}
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
hideIfEmpty={true}
/>
<div className="flex flex-wrap gap-4">
<FormInput
title="Total"
name="total"
type="number"
step="0.01"
value={formData.total.toFixed(2)}
onChange={(e) => setFormData((prev) => ({ ...prev, total: parseFloat(e.target.value) }))}
className="w-32"
/>
<FormSelectCurrency
title="Currency"
currencies={currencies}
name="currencyCode"
value={formData.currencyCode}
onValueChange={(value) => setFormData((prev) => ({ ...prev, currencyCode: value }))}
/>
<FormSelectType
title="Type"
name="type"
value={formData.type}
onValueChange={(value) => setFormData((prev) => ({ ...prev, type: value }))}
/>
</div>
{formData.total != 0 && formData.currencyCode && formData.currencyCode !== settings.default_currency && (
<>
<FormConvertCurrency
originalTotal={formData.total}
originalCurrencyCode={formData.currencyCode}
targetCurrencyCode={settings.default_currency}
date={formData.issuedAt ? new Date(formData.issuedAt) : undefined}
onChange={(value) => setFormData((prev) => ({ ...prev, convertedTotal: value }))}
/>
<input type="hidden" name="convertedCurrencyCode" value={settings.default_currency} />
</>
)}
<div className="flex flex-row gap-4">
<FormInput
title="Issued At"
type="date"
name="issuedAt"
value={formData.issuedAt}
onChange={(e) => setFormData((prev) => ({ ...prev, issuedAt: e.target.value }))}
hideIfEmpty={true}
/>
</div>
<div className="flex flex-row gap-4">
<FormSelectCategory
title="Category"
categories={categories}
name="categoryCode"
value={formData.categoryCode}
onValueChange={(value) => setFormData((prev) => ({ ...prev, categoryCode: value }))}
placeholder="Select Category"
/>
{projects.length >= 0 && (
<FormSelectProject
title="Project"
projects={projects}
name="projectCode"
value={formData.projectCode}
onValueChange={(value) => setFormData((prev) => ({ ...prev, projectCode: value }))}
placeholder="Select Project"
/>
)}
</div>
<FormInput
title="Note"
name="note"
value={formData.note}
onChange={(e) => setFormData((prev) => ({ ...prev, note: e.target.value }))}
hideIfEmpty={true}
/>
{extraFields.map((field) => (
<FormInput
key={field.code}
type={field.type}
title={field.name}
name={field.code}
value={formData[field.code as keyof typeof formData]}
onChange={(e) => setFormData((prev) => ({ ...prev, [field.code]: e.target.value }))}
hideIfEmpty={true}
/>
))}
<div className="hidden">
<FormTextarea
title="Recognized Text"
name="text"
value={formData.text}
onChange={(e) => setFormData((prev) => ({ ...prev, text: e.target.value }))}
hideIfEmpty={true}
/>
</div>
<div className="flex justify-end space-x-4 pt-6">
<Button
type="button"
onClick={() => startTransition(() => deleteAction(file.id))}
variant="outline"
disabled={isDeleting}
>
{isDeleting ? "⏳ Deleting..." : "Delete"}
</Button>
<Button type="submit" disabled={isSaving}>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
"Save as Transaction"
)}
</Button>
{deleteState?.error && <span className="text-red-500"> {deleteState.error}</span>}
{saveError && <span className="text-red-500"> {saveError}</span>}
</div>
</form>
</>
)
}