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 ( + + + + + + {t('title')} + {t('description')} + + + {t('yes')} + + + + + + + + ) +} 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,