chore: organize ts types, fix eslint errors

This commit is contained in:
Vasily Zubarev
2025-04-09 12:45:56 +02:00
parent 707a030a0a
commit 416c45d08c
29 changed files with 277 additions and 84 deletions

View File

@@ -38,7 +38,7 @@ export async function GET(request: Request) {
// CSV rows
for (const transaction of transactions) {
const row: Record<string, any> = {}
const row: Record<string, unknown> = {}
for (const field of existingFields) {
let value
if (field.isExtra) {

View File

@@ -1,5 +1,6 @@
"use server"
import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth"
import { getUserUploadsDirectory, unsortedFilePath } from "@/lib/files"
import { createFile } from "@/models/files"
@@ -8,7 +9,7 @@ import { mkdir, writeFile } from "fs/promises"
import { revalidatePath } from "next/cache"
import path from "path"
export async function uploadFilesAction(prevState: any, formData: FormData) {
export async function uploadFilesAction(formData: FormData): Promise<ActionState<null>> {
const user = await getCurrentUser()
const files = formData.getAll("files")

View File

@@ -1,12 +1,17 @@
"use server"
import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth"
import { EXPORT_AND_IMPORT_FIELD_MAP } from "@/models/export_and_import"
import { createTransaction } from "@/models/transactions"
import { parse } from "@fast-csv/parse"
import { Transaction } from "@prisma/client"
import { revalidatePath } from "next/cache"
export async function parseCSVAction(prevState: any, formData: FormData) {
export async function parseCSVAction(
_prevState: ActionState<string[][]> | null,
formData: FormData
): Promise<ActionState<string[][]>> {
const file = formData.get("file") as File
if (!file) {
return { success: false, error: "No file uploaded" }
@@ -38,7 +43,10 @@ export async function parseCSVAction(prevState: any, formData: FormData) {
}
}
export async function saveTransactionsAction(prevState: any, formData: FormData) {
export async function saveTransactionsAction(
_prevState: ActionState<Transaction> | null,
formData: FormData
): Promise<ActionState<Transaction>> {
const user = await getCurrentUser()
try {
const rows = JSON.parse(formData.get("rows") as string) as Record<string, unknown>[]

View File

@@ -8,19 +8,23 @@ import {
settingsFormSchema,
} from "@/forms/settings"
import { userFormSchema } from "@/forms/users"
import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth"
import { codeFromName, randomHexColor } from "@/lib/utils"
import { createCategory, deleteCategory, updateCategory } from "@/models/categories"
import { createCurrency, deleteCurrency, updateCurrency } from "@/models/currencies"
import { createField, deleteField, updateField } from "@/models/fields"
import { createProject, deleteProject, updateProject } from "@/models/projects"
import { updateSettings } from "@/models/settings"
import { SettingsMap, updateSettings } from "@/models/settings"
import { updateUser } from "@/models/users"
import { Prisma } from "@prisma/client"
import { Prisma, User } from "@prisma/client"
import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
export async function saveSettingsAction(prevState: any, formData: FormData) {
export async function saveSettingsAction(
_prevState: ActionState<SettingsMap> | null,
formData: FormData
): Promise<ActionState<SettingsMap>> {
const user = await getCurrentUser()
const validatedForm = settingsFormSchema.safeParse(Object.fromEntries(formData))
@@ -29,7 +33,10 @@ export async function saveSettingsAction(prevState: any, formData: FormData) {
}
for (const key in validatedForm.data) {
await updateSettings(user.id, key, validatedForm.data[key as keyof typeof validatedForm.data])
const value = validatedForm.data[key as keyof typeof validatedForm.data]
if (value !== undefined) {
await updateSettings(user.id, key, value)
}
}
revalidatePath("/settings")
@@ -37,7 +44,10 @@ export async function saveSettingsAction(prevState: any, formData: FormData) {
// return { success: true }
}
export async function saveProfileAction(prevState: any, formData: FormData) {
export async function saveProfileAction(
_prevState: ActionState<User> | null,
formData: FormData
): Promise<ActionState<User>> {
const user = await getCurrentUser()
const validatedForm = userFormSchema.safeParse(Object.fromEntries(formData))

View File

@@ -1,5 +1,6 @@
"use server"
import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth"
import { prisma } from "@/lib/db"
import { getUserUploadsDirectory } from "@/lib/files"
@@ -12,7 +13,14 @@ const SUPPORTED_BACKUP_VERSIONS = ["1.0"]
const REMOVE_EXISTING_DATA = true
const MAX_BACKUP_SIZE = 256 * 1024 * 1024 // 256MB
export async function restoreBackupAction(prevState: any, formData: FormData) {
type BackupRestoreResult = {
counters: Record<string, number>
}
export async function restoreBackupAction(
_prevState: ActionState<BackupRestoreResult> | null,
formData: FormData
): Promise<ActionState<BackupRestoreResult>> {
const user = await getCurrentUser()
const userUploadsDirectory = await getUserUploadsDirectory(user)
const file = formData.get("file") as File
@@ -32,7 +40,7 @@ export async function restoreBackupAction(prevState: any, formData: FormData) {
const fileData = Buffer.from(fileBuffer)
zip = await JSZip.loadAsync(fileData)
} catch (error) {
return { success: false, error: "Bad zip archive" }
return { success: false, error: "Bad zip archive: " + (error as Error).message }
}
// Check metadata and start restoring
@@ -133,7 +141,7 @@ export async function restoreBackupAction(prevState: any, formData: FormData) {
}
}
return { success: true, message: "Restore completed successfully", counters }
return { success: true, data: { counters } }
} catch (error) {
console.error("Error restoring from backup:", error)
return {

View File

@@ -9,7 +9,7 @@ import path from "path"
const MAX_FILE_SIZE = 64 * 1024 * 1024 // 64MB
const BACKUP_VERSION = "1.0"
export async function GET(request: Request) {
export async function GET() {
const user = await getCurrentUser()
const userUploadsDirectory = await getUserUploadsDirectory(user)
@@ -87,7 +87,7 @@ export async function GET(request: Request) {
}
async function getAllFilePaths(dirPath: string): Promise<string[]> {
let filePaths: string[] = []
const filePaths: string[] = []
async function readDirectoryRecursively(currentPath: string) {
const isDirExists = await fileExists(currentPath)

View File

@@ -62,7 +62,7 @@ export default function BackupSettingsPage() {
<h2 className="text-xl font-semibold">Backup restored successfully</h2>
<p className="text-sm text-muted-foreground">You can now continue using the app. Import stats:</p>
<ul className="list-disc list-inside">
{Object.entries(restoreState.counters || {}).map(([key, value]) => (
{Object.entries(restoreState.data?.counters || {}).map(([key, value]) => (
<li key={key}>
<span className="font-bold">{key}</span>: {value} items
</li>

View File

@@ -17,8 +17,9 @@ export default async function FieldsSettingsPage() {
<div className="container">
<h1 className="text-2xl font-bold mb-2">Custom Fields</h1>
<p className="text-sm text-gray-500 mb-6 max-w-prose">
You can add new fields to your transactions. Standard fields can't be removed but you can tweak their prompts or
hide them. If you don't want a field to be analyzed by AI but filled in by hand, leave the "LLM prompt" empty.
You can add new fields to your transactions. Standard fields can&apos;t be removed but you can tweak their
prompts or hide them. If you don&apos;t want a field to be analyzed by AI but filled in by hand, leave the
&quot;LLM prompt&quot; empty.
</p>
<CrudTable
items={fieldsWithActions}

View File

@@ -1,6 +1,7 @@
"use server"
import { transactionFormSchema } from "@/forms/transactions"
import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth"
import { getTransactionFileUploadPath, getUserUploadsDirectory } from "@/lib/files"
import { updateField } from "@/models/fields"
@@ -13,12 +14,16 @@ import {
updateTransaction,
updateTransactionFiles,
} from "@/models/transactions"
import { Transaction } from "@prisma/client"
import { randomUUID } from "crypto"
import { mkdir, writeFile } from "fs/promises"
import { revalidatePath } from "next/cache"
import path from "path"
export async function createTransactionAction(prevState: any, formData: FormData) {
export async function createTransactionAction(
_prevState: ActionState<Transaction> | null,
formData: FormData
): Promise<ActionState<Transaction>> {
try {
const user = await getCurrentUser()
const validatedForm = transactionFormSchema.safeParse(Object.fromEntries(formData.entries()))
@@ -30,14 +35,17 @@ export async function createTransactionAction(prevState: any, formData: FormData
const transaction = await createTransaction(user.id, validatedForm.data)
revalidatePath("/transactions")
return { success: true, transactionId: transaction.id }
return { success: true, data: transaction }
} 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) {
export async function saveTransactionAction(
_prevState: ActionState<Transaction> | null,
formData: FormData
): Promise<ActionState<Transaction>> {
try {
const user = await getCurrentUser()
const transactionId = formData.get("transactionId") as string
@@ -50,14 +58,17 @@ export async function saveTransactionAction(prevState: any, formData: FormData)
const transaction = await updateTransaction(transactionId, user.id, validatedForm.data)
revalidatePath("/transactions")
return { success: true, transactionId: transaction.id }
return { success: true, data: transaction }
} 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) {
export async function deleteTransactionAction(
_prevState: ActionState<Transaction> | null,
transactionId: string
): Promise<ActionState<Transaction>> {
try {
const user = await getCurrentUser()
const transaction = await getTransactionById(transactionId, user.id)
@@ -67,7 +78,7 @@ export async function deleteTransactionAction(prevState: any, transactionId: str
revalidatePath("/transactions")
return { success: true, transactionId: transaction.id }
return { success: true, data: transaction }
} catch (error) {
console.error("Failed to delete transaction:", error)
return { success: false, error: "Failed to delete transaction" }
@@ -77,7 +88,7 @@ export async function deleteTransactionAction(prevState: any, transactionId: str
export async function deleteTransactionFileAction(
transactionId: string,
fileId: string
): Promise<{ success: boolean; error?: string }> {
): Promise<ActionState<Transaction>> {
if (!fileId || !transactionId) {
return { success: false, error: "File ID and transaction ID are required" }
}
@@ -96,10 +107,10 @@ export async function deleteTransactionFileAction(
await deleteFile(fileId, user.id)
revalidatePath(`/transactions/${transactionId}`)
return { success: true }
return { success: true, data: transaction }
}
export async function uploadTransactionFilesAction(formData: FormData): Promise<{ success: boolean; error?: string }> {
export async function uploadTransactionFilesAction(formData: FormData): Promise<ActionState<Transaction>> {
try {
const transactionId = formData.get("transactionId") as string
const files = formData.getAll("files") as File[]

View File

@@ -5,13 +5,14 @@ import { AnalyzeAttachment, loadAttachmentsForAI } from "@/ai/attachments"
import { buildLLMPrompt } from "@/ai/prompt"
import { fieldsToJsonSchema } from "@/ai/schema"
import { transactionFormSchema } from "@/forms/transactions"
import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth"
import config from "@/lib/config"
import { getTransactionFileUploadPath, getUserUploadsDirectory } from "@/lib/files"
import { DEFAULT_PROMPT_ANALYSE_NEW_FILE } from "@/models/defaults"
import { deleteFile, getFileById, updateFile } from "@/models/files"
import { createTransaction, updateTransactionFiles } from "@/models/transactions"
import { Category, Field, File, Project } from "@prisma/client"
import { Category, Field, File, Project, Transaction } from "@prisma/client"
import { mkdir, rename } from "fs/promises"
import { revalidatePath } from "next/cache"
import path from "path"
@@ -22,7 +23,7 @@ export async function analyzeFileAction(
fields: Field[],
categories: Category[],
projects: Project[]
): Promise<{ success: boolean; data?: Record<string, any>; error?: string }> {
): Promise<ActionState<Record<string, string>>> {
const user = await getCurrentUser()
if (!file || file.userId !== user.id) {
@@ -58,7 +59,10 @@ export async function analyzeFileAction(
return results
}
export async function saveFileAsTransactionAction(prevState: any, formData: FormData) {
export async function saveFileAsTransactionAction(
_prevState: ActionState<Transaction> | null,
formData: FormData
): Promise<ActionState<Transaction>> {
try {
const user = await getCurrentUser()
const validatedForm = transactionFormSchema.safeParse(Object.fromEntries(formData.entries()))
@@ -97,14 +101,17 @@ export async function saveFileAsTransactionAction(prevState: any, formData: Form
revalidatePath("/unsorted")
revalidatePath("/transactions")
return { success: true, transactionId: transaction.id }
return { success: true, data: transaction }
} 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) {
export async function deleteUnsortedFileAction(
_prevState: ActionState<Transaction> | null,
fileId: string
): Promise<ActionState<Transaction>> {
try {
const user = await getCurrentUser()
await deleteFile(fileId, user.id)

View File

@@ -15,12 +15,12 @@ export async function selfHostedGetStartedAction(formData: FormData) {
const openaiApiKey = formData.get("openai_api_key")
if (openaiApiKey) {
await updateSettings(user.id, "openai_api_key", openaiApiKey)
await updateSettings(user.id, "openai_api_key", openaiApiKey as string)
}
const defaultCurrency = formData.get("default_currency")
if (defaultCurrency) {
await updateSettings(user.id, "default_currency", defaultCurrency)
await updateSettings(user.id, "default_currency", defaultCurrency as string)
}
revalidatePath("/dashboard")

View File

@@ -2,6 +2,7 @@ import { LoginForm } from "@/components/auth/login-form"
import { Card, CardContent, CardTitle } from "@/components/ui/card"
import { ColoredText } from "@/components/ui/colored-text"
import config from "@/lib/config"
import Image from "next/image"
import { redirect } from "next/navigation"
export default async function LoginPage() {
@@ -11,7 +12,7 @@ export default async function LoginPage() {
return (
<Card className="w-full max-w-xl mx-auto p-8 flex flex-col items-center justify-center gap-4">
<img src="/logo/512.png" alt="Logo" className="w-36 h-36" />
<Image src="/logo/512.png" alt="Logo" width={144} height={144} className="w-36 h-36" />
<CardTitle className="text-3xl font-bold ">
<ColoredText>TaxHacker: Cloud Edition</ColoredText>
</CardTitle>

View File

@@ -1,16 +1,17 @@
import { X } from "lucide-react"
import Link from "next/link"
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-gray-900 flex flex-col relative">
<a
<Link
href="/"
className="absolute top-4 right-4 flex items-center justify-center w-10 h-10 rounded-full bg-gray-800 hover:bg-gray-700 transition-colors"
>
<span className="text-gray-300 font-bold text-xl">
<X />
</span>
</a>
</Link>
<div className="flex-grow flex flex-col justify-center items-center py-12 px-4 sm:px-6 lg:px-8">{children}</div>
</div>
)

View File

@@ -7,6 +7,7 @@ import config from "@/lib/config"
import { DEFAULT_CURRENCIES, DEFAULT_SETTINGS } from "@/models/defaults"
import { getSelfHostedUser } from "@/models/users"
import { ShieldAlert } from "lucide-react"
import Image from "next/image"
import { redirect } from "next/navigation"
import { selfHostedGetStartedAction } from "../actions"
@@ -36,12 +37,12 @@ export default async function SelfHostedWelcomePage() {
return (
<Card className="w-full max-w-xl mx-auto p-8 flex flex-col items-center justify-center gap-4">
<img src="/logo/512.png" alt="Logo" className="w-36 h-36" />
<Image src="/logo/512.png" alt="Logo" width={144} height={144} className="w-36 h-36" />
<CardTitle className="text-3xl font-bold ">
<ColoredText>TaxHacker: Self-Hosted Edition</ColoredText>
</CardTitle>
<CardDescription className="flex flex-col gap-4 text-center text-lg">
<p>Welcome to your own instance of TaxHacker. Let's set up a couple of settings to get started.</p>
<p>Welcome to your own instance of TaxHacker. Let&apos;s set up a couple of settings to get started.</p>
<form action={selfHostedGetStartedAction} className="flex flex-col gap-8 pt-8">
<div>

View File

@@ -4,7 +4,7 @@ import { getSelfHostedUser } from "@/models/users"
import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
export async function GET(request: Request) {
export async function GET() {
if (!config.selfHosted.isEnabled) {
redirect(config.auth.loginUrl)
}

View File

@@ -8,10 +8,10 @@ export default function LandingPage() {
<div className="min-h-screen flex flex-col bg-[#FAFAFA]">
<header className="py-6 px-8 bg-white/80 backdrop-blur-md shadow-sm fixed w-full z-10">
<div className="max-w-7xl mx-auto flex justify-between items-center">
<a href="/" className="flex items-center gap-2">
<img src="/logo/256.png" alt="Logo" className="h-8" />
<Link href="/" className="flex items-center gap-2">
<Image src="/logo/256.png" alt="Logo" width={32} height={32} className="h-8" />
<ColoredText className="text-2xl font-bold">TaxHacker</ColoredText>
</a>
</Link>
<div className="flex gap-4">
<Link
href="#start"
@@ -43,12 +43,12 @@ export default function LandingPage() {
>
Get Started
</Link>
<a
<Link
href="mailto:me@vas3k.ru"
className="px-8 py-3 border border-gray-200 text-gray-700 font-medium rounded-full hover:bg-gray-50 transition-all"
>
Contact Us
</a>
</Link>
</div>
</div>
<div className="relative aspect-auto rounded-2xl overflow-hidden shadow-2xl ring-8 ring-gray-100 hover:scale-105 transition-all duration-300">
@@ -323,7 +323,7 @@ export default function LandingPage() {
Upcoming Features
</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
We're a small, indie project constantly improving. Here's what we're working on next.
We&apos;re a small, indie project constantly improving. Here&apos;s what we&apos;re working on next.
</p>
</div>
@@ -429,9 +429,9 @@ export default function LandingPage() {
<footer className="py-8 px-8 bg-white">
<div className="max-w-7xl mx-auto text-center text-sm text-gray-500">
Made with in Berlin by{" "}
<a href="https://github.com/vas3k" className="underline">
<Link href="https://github.com/vas3k" className="underline">
vas3k
</a>
</Link>
</div>
</footer>
</div>