mirror of
https://github.com/marcogll/TaxHacker_s23.git
synced 2026-01-13 13:25:18 +00:00
chore: organize ts types, fix eslint errors
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use server"
|
||||
|
||||
import { ActionState } from "@/lib/actions"
|
||||
import OpenAI from "openai"
|
||||
import { AnalyzeAttachment } from "./attachments"
|
||||
|
||||
@@ -8,7 +9,7 @@ export async function analyzeTransaction(
|
||||
schema: Record<string, unknown>,
|
||||
attachments: AnalyzeAttachment[],
|
||||
apiKey: string
|
||||
): Promise<{ success: boolean; data?: Record<string, any>; error?: string }> {
|
||||
): Promise<ActionState<Record<string, string>>> {
|
||||
const openai = new OpenAI({
|
||||
apiKey,
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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>[]
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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.
|
||||
</p>
|
||||
<CrudTable
|
||||
items={fieldsWithActions}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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's set up a couple of settings to get started.</p>
|
||||
|
||||
<form action={selfHostedGetStartedAction} className="flex flex-col gap-8 pt-8">
|
||||
<div>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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're a small, indie project constantly improving. Here's what we'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>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function DashboardDropZoneWidget() {
|
||||
|
||||
// Submit the files using the server action
|
||||
startTransition(async () => {
|
||||
const result = await uploadFilesAction(null, formData)
|
||||
const result = await uploadFilesAction(formData)
|
||||
if (result.success) {
|
||||
showNotification({ code: "sidebar.unsorted", message: "new" })
|
||||
setTimeout(() => showNotification({ code: "sidebar.unsorted", message: "" }), 3000)
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function ScreenDropArea({ children }: { children: React.ReactNode
|
||||
startTransition(async () => {
|
||||
const result = transactionId
|
||||
? await uploadTransactionFilesAction(formData)
|
||||
: await uploadFilesAction(null, formData)
|
||||
: await uploadFilesAction(formData)
|
||||
|
||||
if (result.success) {
|
||||
showNotification({ code: "sidebar.unsorted", message: "new" })
|
||||
@@ -88,18 +88,18 @@ export default function ScreenDropArea({ children }: { children: React.ReactNode
|
||||
|
||||
// Add event listeners to document body
|
||||
useEffect(() => {
|
||||
document.body.addEventListener("dragenter", handleDragEnter as any)
|
||||
document.body.addEventListener("dragover", handleDragOver as any)
|
||||
document.body.addEventListener("dragleave", handleDragLeave as any)
|
||||
document.body.addEventListener("drop", handleDrop as any)
|
||||
document.body.addEventListener("dragenter", handleDragEnter as unknown as EventListener)
|
||||
document.body.addEventListener("dragover", handleDragOver as unknown as EventListener)
|
||||
document.body.addEventListener("dragleave", handleDragLeave as unknown as EventListener)
|
||||
document.body.addEventListener("drop", handleDrop as unknown as EventListener)
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener("dragenter", handleDragEnter as any)
|
||||
document.body.removeEventListener("dragover", handleDragOver as any)
|
||||
document.body.removeEventListener("dragleave", handleDragLeave as any)
|
||||
document.body.removeEventListener("drop", handleDrop as any)
|
||||
document.body.removeEventListener("dragenter", handleDragEnter as unknown as EventListener)
|
||||
document.body.removeEventListener("dragover", handleDragOver as unknown as EventListener)
|
||||
document.body.removeEventListener("dragleave", handleDragLeave as unknown as EventListener)
|
||||
document.body.removeEventListener("drop", handleDrop as unknown as EventListener)
|
||||
}
|
||||
}, [isDragging])
|
||||
}, [isDragging, handleDrop])
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen w-full">
|
||||
|
||||
@@ -28,7 +28,7 @@ export function UploadButton({ children, ...props }: { children: React.ReactNode
|
||||
|
||||
// Submit the files using the server action
|
||||
startTransition(async () => {
|
||||
const result = await uploadFilesAction(null, formData)
|
||||
const result = await uploadFilesAction(formData)
|
||||
if (result.success) {
|
||||
showNotification({ code: "sidebar.unsorted", message: "new" })
|
||||
setTimeout(() => showNotification({ code: "sidebar.unsorted", message: "" }), 3000)
|
||||
|
||||
@@ -16,16 +16,8 @@ export const FormConvertCurrency = ({
|
||||
date?: Date | undefined
|
||||
onChange?: (value: number) => void
|
||||
}) => {
|
||||
if (
|
||||
originalTotal === 0 ||
|
||||
!originalCurrencyCode ||
|
||||
!targetCurrencyCode ||
|
||||
originalCurrencyCode === targetCurrencyCode
|
||||
) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const normalizedDate = startOfDay(date || new Date(Date.now() - 24 * 60 * 60 * 1000))
|
||||
const normalizedDateString = format(normalizedDate, "yyyy-MM-dd")
|
||||
const [exchangeRate, setExchangeRate] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
@@ -46,7 +38,11 @@ export const FormConvertCurrency = ({
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [originalCurrencyCode, targetCurrencyCode, format(normalizedDate, "LLLL-mm-dd")])
|
||||
}, [originalCurrencyCode, targetCurrencyCode, normalizedDateString, originalTotal])
|
||||
|
||||
if (!originalTotal || !originalCurrencyCode || !targetCurrencyCode || originalCurrencyCode === targetCurrencyCode) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-2 items-center text-muted-foreground">
|
||||
|
||||
@@ -117,7 +117,7 @@ export const FormDate = ({
|
||||
if (!isNaN(newDate.getTime())) {
|
||||
setDate(newDate)
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -23,10 +23,11 @@ export function ImportCSVTable({ fields }: { fields: Field[] }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (parseState?.success && parseState.data) {
|
||||
setCSVData(parseState.data)
|
||||
if (parseState.data.length > 0) {
|
||||
const parsedData = parseState.data as string[][]
|
||||
setCSVData(parsedData)
|
||||
if (parsedData.length > 0) {
|
||||
setColumnMappings(
|
||||
parseState.data[0].map((value) => {
|
||||
parsedData[0].map((value) => {
|
||||
const field = fields.find((field) => field.code === value || field.name === value)
|
||||
return field?.code || ""
|
||||
})
|
||||
|
||||
@@ -44,8 +44,8 @@ export default function TransactionCreateForm({
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (createState?.success) {
|
||||
router.push(`/transactions/${createState.transactionId}`)
|
||||
if (createState?.success && createState.data) {
|
||||
router.push(`/transactions/${createState.data.id}`)
|
||||
}
|
||||
}, [createState, router])
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export const settingsFormSchema = z.object({
|
||||
default_project: z.string().optional(),
|
||||
openai_api_key: z.string().optional(),
|
||||
prompt_analyse_new_file: z.string().optional(),
|
||||
is_welcome_message_hidden: z.boolean().optional(),
|
||||
is_welcome_message_hidden: z.string().optional(),
|
||||
})
|
||||
|
||||
export const currencyFormSchema = z.object({
|
||||
|
||||
5
lib/actions.ts
Normal file
5
lib/actions.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type ActionState<T> = {
|
||||
success: boolean
|
||||
error?: string | null
|
||||
data?: T | null
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const getSettings = cache(async (userId: string): Promise<SettingsMap> =>
|
||||
}, {} as SettingsMap)
|
||||
})
|
||||
|
||||
export const updateSettings = cache(async (userId: string, code: string, value: any) => {
|
||||
export const updateSettings = cache(async (userId: string, code: string, value: string | undefined) => {
|
||||
return await prisma.setting.upsert({
|
||||
where: { userId_code: { code, userId } },
|
||||
update: { value },
|
||||
|
||||
151
package-lock.json
generated
151
package-lock.json
generated
@@ -20,7 +20,7 @@
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@sentry/nextjs": "^9.11.0",
|
||||
"@types/sharp": "^0.31.1",
|
||||
@@ -2625,6 +2625,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||
@@ -2691,6 +2709,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
@@ -2883,6 +2919,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz",
|
||||
@@ -2920,6 +2974,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
|
||||
@@ -3023,6 +3095,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
|
||||
@@ -3097,6 +3187,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
|
||||
@@ -3121,12 +3229,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -3138,6 +3246,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
|
||||
@@ -3172,6 +3295,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@sentry/nextjs": "^9.11.0",
|
||||
"@types/sharp": "^0.31.1",
|
||||
|
||||
Reference in New Issue
Block a user