diff --git a/messages/de-DE.json b/messages/de-DE.json
index db9d3f01..bb69f7df 100644
--- a/messages/de-DE.json
+++ b/messages/de-DE.json
@@ -198,6 +198,13 @@
"yes": "Ja",
"cancel": "Abbrechen"
},
+ "ClonePopup": {
+ "label": "Clone",
+ "title": "Clone this expense?",
+ "description": "Do you really want to clone this expense?",
+ "yes": "Yes",
+ "cancel": "Cancel"
+ },
"attachDocuments": "Dokument hinzufügen",
"create": "Erstellen",
"creating": "Erstellt…",
diff --git a/messages/en-US.json b/messages/en-US.json
index f1ba2767..e34031d9 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -198,6 +198,13 @@
"yes": "Yes",
"cancel": "Cancel"
},
+ "ClonePopup": {
+ "label": "Clone",
+ "title": "Clone this expense?",
+ "description": "Do you really want to clone this expense?",
+ "yes": "Yes",
+ "cancel": "Cancel"
+ },
"attachDocuments": "Attach documents",
"create": "Create",
"creating": "Creating…",
diff --git a/messages/es.json b/messages/es.json
index 8c4edb56..bed9e153 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -198,6 +198,13 @@
"yes": "Si",
"cancel": "Cancelar"
},
+ "ClonePopup": {
+ "label": "Clone",
+ "title": "Clone this expense?",
+ "description": "Do you really want to clone this expense?",
+ "yes": "Yes",
+ "cancel": "Cancel"
+ },
"attachDocuments": "Adjuntar documentos",
"create": "Crear",
"creating": "Creando",
diff --git a/messages/fi.json b/messages/fi.json
index d47a86ce..ba68e7f6 100644
--- a/messages/fi.json
+++ b/messages/fi.json
@@ -198,6 +198,13 @@
"yes": "Kyllä",
"cancel": "Peruuta"
},
+ "ClonePopup": {
+ "label": "Clone",
+ "title": "Clone this expense?",
+ "description": "Do you really want to clone this expense?",
+ "yes": "Kyllä",
+ "cancel": "Peruuta"
+ },
"attachDocuments": "Liitä dokumenttejä",
"create": "Lisää kulu",
"creating": "Luodaan kulua…",
diff --git a/messages/fr-FR.json b/messages/fr-FR.json
index e3504005..5f4bbbf3 100644
--- a/messages/fr-FR.json
+++ b/messages/fr-FR.json
@@ -198,6 +198,13 @@
"yes": "Oui",
"cancel": "Annuler"
},
+ "ClonePopup": {
+ "label": "Clone",
+ "title": "Clone this expense?",
+ "description": "Do you really want to clone this expense?",
+ "yes": "Yes",
+ "cancel": "Cancel"
+ },
"attachDocuments": "Joindre des documents",
"create": "Créer",
"creating": "Création…",
diff --git a/messages/it-IT.json b/messages/it-IT.json
index 6a6220eb..7b83bcc3 100644
--- a/messages/it-IT.json
+++ b/messages/it-IT.json
@@ -198,6 +198,13 @@
"yes": "Si",
"cancel": "Annulla"
},
+ "ClonePopup": {
+ "label": "Clone",
+ "title": "Clone this expense?",
+ "description": "Do you really want to clone this expense?",
+ "yes": "Yes",
+ "cancel": "Cancel"
+ },
"attachDocuments": "Documenti allegati",
"create": "Crea",
"creating": "Sto creando…",
diff --git a/messages/ru-RU.json b/messages/ru-RU.json
index 9a9923cf..8309ab64 100644
--- a/messages/ru-RU.json
+++ b/messages/ru-RU.json
@@ -198,6 +198,13 @@
"yes": "Удалить",
"cancel": "Отмена"
},
+ "ClonePopup": {
+ "label": "Clone",
+ "title": "Clone this expense?",
+ "description": "Do you really want to clone this expense?",
+ "yes": "Yes",
+ "cancel": "Cancel"
+ },
"attachDocuments": "Прикрепить документы",
"create": "Создать",
"creating": "Создание…",
diff --git a/messages/zh-CN.json b/messages/zh-CN.json
index 2065369d..e7cd7823 100644
--- a/messages/zh-CN.json
+++ b/messages/zh-CN.json
@@ -198,6 +198,13 @@
"yes": "确定",
"cancel": "取消"
},
+ "ClonePopup": {
+ "label": "Clone",
+ "title": "Clone this expense?",
+ "description": "Do you really want to clone this expense?",
+ "yes": "Yes",
+ "cancel": "Cancel"
+ },
"attachDocuments": "附加文档",
"create": "创建",
"creating": "创建中……",
diff --git a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
index a91de1f8..6e2bb2d8 100644
--- a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
+++ b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
@@ -1,6 +1,7 @@
import { cached } from '@/app/cached-functions'
import { ExpenseForm } from '@/components/expense-form'
import {
+ cloneExpense,
deleteExpense,
getCategories,
getExpense,
@@ -39,6 +40,11 @@ export default async function EditExpensePage({
await deleteExpense(groupId, expenseId, participantId)
redirect(`/groups/${groupId}`)
}
+ async function cloneExpenseAction(participantId?: string) {
+ 'use server'
+ await cloneExpense(groupId, expenseId, participantId)
+ redirect(`/groups/${groupId}`)
+ }
return (
@@ -48,6 +54,7 @@ export default async function EditExpensePage({
categories={categories}
onSubmit={updateExpenseAction}
onDelete={deleteExpenseAction}
+ onClone={cloneExpenseAction}
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
/>
diff --git a/src/components/clone-popup.tsx b/src/components/clone-popup.tsx
new file mode 100644
index 00000000..366f6481
--- /dev/null
+++ b/src/components/clone-popup.tsx
@@ -0,0 +1,46 @@
+'use client'
+
+import { Copy } from 'lucide-react'
+import { useTranslations } from 'next-intl'
+import { AsyncButton } from './async-button'
+import { Button } from './ui/button'
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogTitle,
+ DialogTrigger,
+} from './ui/dialog'
+
+export function ClonePopup({ onClone }: { onClone: () => Promise }) {
+ const t = useTranslations('ExpenseForm.ClonePopup')
+ return (
+
+ )
+}
diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx
index 855041bd..777cce12 100644
--- a/src/components/expense-form.tsx
+++ b/src/components/expense-form.tsx
@@ -50,6 +50,7 @@ import { useSearchParams } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { match } from 'ts-pattern'
+import { ClonePopup } from './clone-popup'
import { DeletePopup } from './delete-popup'
import { extractCategoryFromTitle } from './expense-form-actions'
import { Textarea } from './ui/textarea'
@@ -60,6 +61,7 @@ export type Props = {
categories: NonNullable>>
onSubmit: (values: ExpenseFormValues, participantId?: string) => Promise
onDelete?: (participantId?: string) => Promise
+ onClone?: (participantId?: string) => Promise
runtimeFeatureFlags: RuntimeFeatureFlags
}
@@ -150,6 +152,7 @@ export function ExpenseForm({
categories,
onSubmit,
onDelete,
+ onClone,
runtimeFeatureFlags,
}: Props) {
const t = useTranslations('ExpenseForm')
@@ -816,6 +819,11 @@ export function ExpenseForm({
onDelete={() => onDelete(activeUserId ?? undefined)}
>
)}
+ {!isCreate && onClone && (
+ onClone(activeUserId ?? undefined)}
+ >
+ )}
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 92eaefc5..b1ddb308 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -2,6 +2,7 @@ import { prisma } from '@/lib/prisma'
import { ExpenseFormValues, GroupFormValues } from '@/lib/schemas'
import { ActivityType, Expense } from '@prisma/client'
import { nanoid } from 'nanoid'
+import { notFound } from 'next/navigation'
export function randomId() {
return nanoid()
@@ -84,6 +85,43 @@ export async function createExpense(
})
}
+export async function cloneExpense(
+ groupId: string,
+ expenseId: string,
+ participantId?: string,
+) {
+ const group = await getGroup(groupId)
+ if (!group) notFound()
+
+ const expense = await getExpense(groupId, expenseId)
+ if (!expense) notFound()
+
+ const paidForCloned = []
+ for (const participant of expense.paidFor) {
+ const participantObject = {
+ participant: participant.participantId,
+ shares: participant.shares,
+ }
+ paidForCloned.push(participantObject)
+ }
+
+ const formClone: ExpenseFormValues = {
+ expenseDate: new Date(),
+ category: expense.categoryId,
+ amount: expense.amount,
+ title: expense.title + ' clone',
+ paidBy: expense.paidBy.id,
+ splitMode: expense.splitMode,
+ paidFor: paidForCloned,
+ isReimbursement: expense.isReimbursement,
+ documents: expense.documents,
+ notes: expense.notes ? expense.notes : undefined,
+ saveDefaultSplittingOptions: false,
+ }
+
+ await createExpense(formClone, groupId, participantId)
+}
+
export async function deleteExpense(
groupId: string,
expenseId: string,