mirror of
https://github.com/marcogll/TaxHacker_s23.git
synced 2026-01-13 13:25:18 +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:
288
components/unsorted/analyze-form.tsx
Normal file
288
components/unsorted/analyze-form.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
"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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user