feat: invoice generator

This commit is contained in:
Vasily Zubarev
2025-05-07 14:53:13 +02:00
parent 287abbb219
commit 8b5a2e8056
59 changed files with 2606 additions and 124 deletions

View File

@@ -10,6 +10,7 @@ import {
import { userFormSchema } from "@/forms/users"
import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth"
import { uploadStaticImage } from "@/lib/uploads"
import { codeFromName, randomHexColor } from "@/lib/utils"
import { createCategory, deleteCategory, updateCategory } from "@/models/categories"
import { createCurrency, deleteCurrency, updateCurrency } from "@/models/currencies"
@@ -19,7 +20,7 @@ import { SettingsMap, updateSettings } from "@/models/settings"
import { updateUser } from "@/models/users"
import { Prisma, User } from "@/prisma/client"
import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
import path from "path"
export async function saveSettingsAction(
_prevState: ActionState<SettingsMap> | null,
@@ -40,8 +41,7 @@ export async function saveSettingsAction(
}
revalidatePath("/settings")
redirect("/settings")
// return { success: true }
return { success: true }
}
export async function saveProfileAction(
@@ -55,12 +55,47 @@ export async function saveProfileAction(
return { success: false, error: validatedForm.error.message }
}
// Upload avatar
let avatarUrl = user.avatar
const avatarFile = formData.get("avatar") as File | null
if (avatarFile instanceof File && avatarFile.size > 0) {
try {
const uploadedAvatarPath = await uploadStaticImage(user, avatarFile, "avatar.webp", 500, 500)
avatarUrl = `/files/static/${path.basename(uploadedAvatarPath)}`
} catch (error) {
return { success: false, error: "Failed to upload avatar: " + error }
}
}
// Upload business logo
let businessLogoUrl = user.businessLogo
const businessLogoFile = formData.get("businessLogo") as File | null
if (businessLogoFile instanceof File && businessLogoFile.size > 0) {
try {
const uploadedBusinessLogoPath = await uploadStaticImage(user, businessLogoFile, "businessLogo.png", 500, 500)
businessLogoUrl = `/files/static/${path.basename(uploadedBusinessLogoPath)}`
} catch (error) {
return { success: false, error: "Failed to upload business logo: " + error }
}
}
// Update user
await updateUser(user.id, {
name: validatedForm.data.name,
name: validatedForm.data.name !== undefined ? validatedForm.data.name : user.name,
avatar: avatarUrl,
businessName: validatedForm.data.businessName !== undefined ? validatedForm.data.businessName : user.businessName,
businessAddress:
validatedForm.data.businessAddress !== undefined ? validatedForm.data.businessAddress : user.businessAddress,
businessBankDetails:
validatedForm.data.businessBankDetails !== undefined
? validatedForm.data.businessBankDetails
: user.businessBankDetails,
businessLogo: businessLogoUrl,
})
revalidatePath("/settings/profile")
redirect("/settings/profile")
revalidatePath("/settings/business")
return { success: true }
}
export async function addProjectAction(userId: string, data: Prisma.ProjectCreateInput) {

View File

@@ -3,7 +3,7 @@
import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth"
import { prisma } from "@/lib/db"
import { getUserUploadsDirectory } from "@/lib/files"
import { getUserUploadsDirectory, safePathJoin } from "@/lib/files"
import { MODEL_BACKUP, modelFromJSON } from "@/models/backups"
import fs from "fs/promises"
import JSZip from "jszip"
@@ -22,7 +22,7 @@ export async function restoreBackupAction(
formData: FormData
): Promise<ActionState<BackupRestoreResult>> {
const user = await getCurrentUser()
const userUploadsDirectory = await getUserUploadsDirectory(user)
const userUploadsDirectory = getUserUploadsDirectory(user)
const file = formData.get("file") as File
if (!file || file.size === 0) {
@@ -98,7 +98,7 @@ export async function restoreBackupAction(
},
})
const userUploadsDirectory = await getUserUploadsDirectory(user)
const userUploadsDirectory = getUserUploadsDirectory(user)
for (const file of files) {
const filePathWithoutPrefix = path.normalize(file.path.replace(/^.*\/uploads\//, ""))
@@ -110,7 +110,7 @@ export async function restoreBackupAction(
}
const fileContents = await zipFile.async("nodebuffer")
const fullFilePath = path.join(userUploadsDirectory, filePathWithoutPrefix)
const fullFilePath = safePathJoin(userUploadsDirectory, filePathWithoutPrefix)
if (!fullFilePath.startsWith(path.normalize(userUploadsDirectory))) {
console.error(`Attempted path traversal detected for file ${file.path}`)
continue

View File

@@ -11,7 +11,7 @@ const BACKUP_VERSION = "1.0"
export async function GET() {
const user = await getCurrentUser()
const userUploadsDirectory = await getUserUploadsDirectory(user)
const userUploadsDirectory = getUserUploadsDirectory(user)
try {
const zip = new JSZip()

View File

@@ -0,0 +1,14 @@
import BusinessSettingsForm from "@/components/settings/business-settings-form"
import { getCurrentUser } from "@/lib/auth"
export default async function BusinessSettingsPage() {
const user = await getCurrentUser()
return (
<>
<div className="w-full max-w-2xl">
<BusinessSettingsForm user={user} />
</div>
</>
)
}

View File

@@ -16,6 +16,10 @@ const settingsCategories = [
title: "Profile & Plan",
href: "/settings/profile",
},
{
title: "Business Details",
href: "/settings/business",
},
{
title: "LLM settings",
href: "/settings/llm",