mirror of
https://github.com/marcogll/TaxHacker_s23.git
synced 2026-01-13 21:35:19 +00:00
338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
"use client"
|
||
|
||
import { useNotification } from "@/app/(app)/context"
|
||
import { analyzeFileAction, deleteUnsortedFileAction, saveFileAsTransactionAction } from "@/app/(app)/unsorted/actions"
|
||
import { FormConvertCurrency } from "@/components/forms/convert-currency"
|
||
import { FormError } from "@/components/forms/error"
|
||
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 { format } from "date-fns"
|
||
import { Brain, Loader2, Trash2, ArrowDownToLine } from "lucide-react"
|
||
import { startTransition, useActionState, useMemo, useState } from "react"
|
||
import ToolWindow from "../agents/tool-window"
|
||
|
||
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 fieldMap = useMemo(() => {
|
||
return fields.reduce(
|
||
(acc, field) => {
|
||
acc[field.code] = field
|
||
return acc
|
||
},
|
||
{} as Record<string, Field>
|
||
)
|
||
}, [fields])
|
||
|
||
const extraFields = useMemo(() => fields.filter((field) => field.isExtra), [fields])
|
||
const initialFormState = useMemo(() => {
|
||
const baseState = {
|
||
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: "",
|
||
}
|
||
|
||
// Add extra fields
|
||
const extraFieldsState = extraFields.reduce(
|
||
(acc, field) => {
|
||
acc[field.code] = ""
|
||
return acc
|
||
},
|
||
{} as Record<string, string>
|
||
)
|
||
|
||
// Load cached results if they exist
|
||
const cachedResults = file.cachedParseResult
|
||
? Object.fromEntries(
|
||
Object.entries(file.cachedParseResult as Record<string, string>).filter(
|
||
([_, value]) => value !== null && value !== undefined && value !== ""
|
||
)
|
||
)
|
||
: {}
|
||
|
||
return {
|
||
...baseState,
|
||
...extraFieldsState,
|
||
...cachedResults,
|
||
}
|
||
}, [file.filename, settings, extraFields, file.cachedParseResult])
|
||
const [formData, setFormData] = useState(initialFormState)
|
||
|
||
async function saveAsTransaction(formData: FormData) {
|
||
setSaveError("")
|
||
setIsSaving(true)
|
||
startTransition(async () => {
|
||
const result = await saveFileAsTransactionAction(null, formData)
|
||
setIsSaving(false)
|
||
|
||
if (result.success) {
|
||
showNotification({ code: "global.banner", message: "Saved!", type: "success" })
|
||
showNotification({ code: "sidebar.transactions", message: "new" })
|
||
setTimeout(() => showNotification({ code: "sidebar.transactions", message: "" }), 3000)
|
||
} else {
|
||
setSaveError(result.error ? result.error : "Something went wrong...")
|
||
showNotification({ code: "global.banner", message: "Failed to save", type: "failed" })
|
||
}
|
||
})
|
||
}
|
||
|
||
const startAnalyze = async () => {
|
||
setIsAnalyzing(true)
|
||
setAnalyzeError("")
|
||
try {
|
||
setAnalyzeStep("Analyzing...")
|
||
const results = await analyzeFileAction(file, settings, fields, categories, projects)
|
||
|
||
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?.output || {}).filter(
|
||
([_, value]) => value !== null && value !== undefined && value !== ""
|
||
)
|
||
)
|
||
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-1 h-4 w-4 animate-spin" />
|
||
<span>{analyzeStep}</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Brain className="mr-1 h-4 w-4" />
|
||
<span>Analyze with AI</span>
|
||
</>
|
||
)}
|
||
</Button>
|
||
|
||
{analyzeError && <FormError>⚠️ {analyzeError}</FormError>}
|
||
|
||
<form className="space-y-4" action={saveAsTransaction}>
|
||
<input type="hidden" name="fileId" value={file.id} />
|
||
<FormInput
|
||
title={fieldMap.name.name}
|
||
name="name"
|
||
value={formData.name}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
|
||
required={fieldMap.name.isRequired}
|
||
/>
|
||
|
||
<FormInput
|
||
title={fieldMap.merchant.name}
|
||
name="merchant"
|
||
value={formData.merchant}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, merchant: e.target.value }))}
|
||
hideIfEmpty={!fieldMap.merchant.isVisibleInAnalysis}
|
||
required={fieldMap.merchant.isRequired}
|
||
/>
|
||
|
||
<FormInput
|
||
title={fieldMap.description.name}
|
||
name="description"
|
||
value={formData.description}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
|
||
hideIfEmpty={!fieldMap.description.isVisibleInAnalysis}
|
||
required={fieldMap.description.isRequired}
|
||
/>
|
||
|
||
<div className="flex flex-wrap gap-4">
|
||
<FormInput
|
||
title={fieldMap.total.name}
|
||
name="total"
|
||
type="number"
|
||
step="0.01"
|
||
value={formData.total || ""}
|
||
onChange={(e) => {
|
||
const newValue = parseFloat(e.target.value || "0")
|
||
!isNaN(newValue) && setFormData((prev) => ({ ...prev, total: newValue }))
|
||
}}
|
||
className="w-32"
|
||
required={fieldMap.total.isRequired}
|
||
/>
|
||
|
||
<FormSelectCurrency
|
||
title={fieldMap.currencyCode.name}
|
||
currencies={currencies}
|
||
name="currencyCode"
|
||
value={formData.currencyCode}
|
||
onValueChange={(value) => setFormData((prev) => ({ ...prev, currencyCode: value }))}
|
||
hideIfEmpty={!fieldMap.currencyCode.isVisibleInAnalysis}
|
||
required={fieldMap.currencyCode.isRequired}
|
||
/>
|
||
|
||
<FormSelectType
|
||
title={fieldMap.type.name}
|
||
name="type"
|
||
value={formData.type}
|
||
onValueChange={(value) => setFormData((prev) => ({ ...prev, type: value }))}
|
||
hideIfEmpty={!fieldMap.type.isVisibleInAnalysis}
|
||
required={fieldMap.type.isRequired}
|
||
/>
|
||
</div>
|
||
|
||
{formData.total != 0 && formData.currencyCode && formData.currencyCode !== settings.default_currency && (
|
||
<ToolWindow title={`Exchange rate on ${format(new Date(formData.issuedAt || Date.now()), "LLLL dd, yyyy")}`}>
|
||
<FormConvertCurrency
|
||
originalTotal={formData.total}
|
||
originalCurrencyCode={formData.currencyCode}
|
||
targetCurrencyCode={settings.default_currency}
|
||
date={new Date(formData.issuedAt || Date.now())}
|
||
onChange={(value) => setFormData((prev) => ({ ...prev, convertedTotal: value }))}
|
||
/>
|
||
<input type="hidden" name="convertedCurrencyCode" value={settings.default_currency} />
|
||
</ToolWindow>
|
||
)}
|
||
|
||
<div className="flex flex-row gap-4">
|
||
<FormInput
|
||
title={fieldMap.issuedAt.name}
|
||
type="date"
|
||
name="issuedAt"
|
||
value={formData.issuedAt}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, issuedAt: e.target.value }))}
|
||
hideIfEmpty={!fieldMap.issuedAt.isVisibleInAnalysis}
|
||
required={fieldMap.issuedAt.isRequired}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex flex-row gap-4">
|
||
<FormSelectCategory
|
||
title={fieldMap.categoryCode.name}
|
||
categories={categories}
|
||
name="categoryCode"
|
||
value={formData.categoryCode}
|
||
onValueChange={(value) => setFormData((prev) => ({ ...prev, categoryCode: value }))}
|
||
placeholder="Select Category"
|
||
hideIfEmpty={!fieldMap.categoryCode.isVisibleInAnalysis}
|
||
required={fieldMap.categoryCode.isRequired}
|
||
/>
|
||
|
||
{projects.length > 0 && (
|
||
<FormSelectProject
|
||
title={fieldMap.projectCode.name}
|
||
projects={projects}
|
||
name="projectCode"
|
||
value={formData.projectCode}
|
||
onValueChange={(value) => setFormData((prev) => ({ ...prev, projectCode: value }))}
|
||
placeholder="Select Project"
|
||
hideIfEmpty={!fieldMap.projectCode.isVisibleInAnalysis}
|
||
required={fieldMap.projectCode.isRequired}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
<FormInput
|
||
title={fieldMap.note.name}
|
||
name="note"
|
||
value={formData.note}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, note: e.target.value }))}
|
||
hideIfEmpty={!fieldMap.note.isVisibleInAnalysis}
|
||
required={fieldMap.note.isRequired}
|
||
/>
|
||
|
||
{extraFields.map((field) => (
|
||
<FormInput
|
||
key={field.code}
|
||
type="text"
|
||
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={!field.isVisibleInAnalysis}
|
||
required={field.isRequired}
|
||
/>
|
||
))}
|
||
|
||
<div className="hidden">
|
||
<FormTextarea
|
||
title={fieldMap.text.name}
|
||
name="text"
|
||
value={formData.text}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, text: e.target.value }))}
|
||
hideIfEmpty={!fieldMap.text.isVisibleInAnalysis}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex justify-between gap-4 pt-6">
|
||
<Button
|
||
type="button"
|
||
onClick={() => startTransition(() => deleteAction(file.id))}
|
||
variant="destructive"
|
||
disabled={isDeleting}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
{isDeleting ? "⏳ Deleting..." : "Delete"}
|
||
</Button>
|
||
|
||
<Button type="submit" disabled={isSaving}>
|
||
{isSaving ? (
|
||
<>
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
Saving...
|
||
</>
|
||
) : (
|
||
<>
|
||
<ArrowDownToLine className="h-4 w-4" />
|
||
Save as Transaction
|
||
</>
|
||
)}
|
||
</Button>
|
||
|
||
{deleteState?.error && <FormError>⚠️ {deleteState.error}</FormError>}
|
||
{saveError && <FormError>⚠️ {saveError}</FormError>}
|
||
</div>
|
||
</form>
|
||
</>
|
||
)
|
||
}
|