Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add recurring expense functionality #263

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions messages/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
"label": "Empfangen von",
"description": "Wähle das Mitglied, das die Einnahme erhalten hat."
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"paidFor": {
"title": "Empfangen für",
"description": "Wähle für wen die Einnahme empfangen wurde."
Expand Down
9 changes: 9 additions & 0 deletions messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@
"label": "Paid by",
"description": "Select the participant who paid the expense."
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"paidFor": {
"title": "Paid for",
"description": "Select who the expense was paid for."
Expand Down
9 changes: 9 additions & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
"label": "Recibido por",
"description": "Seleccione el participante que recibió los ingresos."
},
"recurrenceRule": {
"label": "Recurrencia del gasto",
"description": "Seleccione con qué frecuencia debe repetirse el gasto.",

"none": "Ninguno",
"daily": "Diario",
"weekly": "Semanal",
"monthly": "Mensual"
},
"paidFor": {
"title": "Recibido para for",
"description": "Seleccione para quién se recibió el ingreso."
Expand Down
9 changes: 9 additions & 0 deletions messages/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
"label": "Vastaanottaja",
"description": "Valitse kuka vastaanotti tulon."
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"paidFor": {
"title": "Tulon jakaminen",
"description": "Valitse kenelle tulo jaetaan."
Expand Down
9 changes: 9 additions & 0 deletions messages/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
"label": "Reçu par",
"description": "Sélectionnez le participant qui a reçu le revenu."
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"paidFor": {
"title": "Reçu pour",
"description": "Sélectionnez pour qui le revenu a été reçu."
Expand Down
9 changes: 9 additions & 0 deletions messages/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
"label": "Ricevuto da",
"description": "Seleziona partecipante che ha ricevuto l'entrata."
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"paidFor": {
"title": "Ricevuto per",
"description": "Seleziona per chi è stato ricevuta l'entrata."
Expand Down
9 changes: 9 additions & 0 deletions messages/pl-PL.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
"label": "Otrzymane przez",
"description": "Wybierz członka, który otrzymał wpływ."
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"paidFor": {
"title": "Otrzymany dla",
"description": "Podaj dla kogo wpływ był przeznaczony."
Expand Down
9 changes: 9 additions & 0 deletions messages/ro.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@
"placeholder": "Cina de luni seară",
"description": "Adaugă o descriere pentru venit."
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"DateField": {
"label": "Data venitului",
"description": "Adaugă data la care venitul a fost primit."
Expand Down
9 changes: 9 additions & 0 deletions messages/ru-RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
"label": "Получивший",
"description": "Выберите участника, который получил этот доход."
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"paidFor": {
"title": "Участники",
"description": "Выберите тех, между кем этот доход будет распределен."
Expand Down
9 changes: 9 additions & 0 deletions messages/ua-UA.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
"label": "Отримав",
"description": "Оберіть учасника, який отримав дохід"
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"paidFor": {
"title": "Учасники",
"description": "Виберіть тих, між ким цей дохід буде розподілено"
Expand Down
9 changes: 9 additions & 0 deletions messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
"label": "接收到",
"description": "选择接收到这笔收入的群组成员。"
},
"recurrenceRule": {
"label": "Expense Recurrence",
"description": "Select how often the expense should repeat.",

"none": "None",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly"
},
"paidFor": {
"title": "接收给",
"description": "选择收入是为谁而收。"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- CreateEnum
CREATE TYPE "RecurrenceRule" AS ENUM ('NONE', 'DAILY', 'WEEKLY', 'MONTHLY');

-- AlterTable
ALTER TABLE "Expense" ADD COLUMN "recurrenceRule" "RecurrenceRule" DEFAULT 'NONE',
ADD COLUMN "recurringExpenseLinkId" TEXT;

-- CreateTable
CREATE TABLE "RecurringExpenseLink" (
"id" TEXT NOT NULL,
"groupId" TEXT NOT NULL,
"currentFrameExpenseId" TEXT NOT NULL,
"nextExpenseCreatedAt" TIMESTAMP(3),
"nextExpenseDate" TIMESTAMP(3) NOT NULL,

CONSTRAINT "RecurringExpenseLink_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "RecurringExpenseLink_currentFrameExpenseId_key" ON "RecurringExpenseLink"("currentFrameExpenseId");

-- CreateIndex
CREATE INDEX "RecurringExpenseLink_groupId_idx" ON "RecurringExpenseLink"("groupId");

-- CreateIndex
CREATE INDEX "RecurringExpenseLink_groupId_nextExpenseCreatedAt_nextExpen_idx" ON "RecurringExpenseLink"("groupId", "nextExpenseCreatedAt", "nextExpenseDate" DESC);

-- AddForeignKey
ALTER TABLE "RecurringExpenseLink" ADD CONSTRAINT "RecurringExpenseLink_currentFrameExpenseId_fkey" FOREIGN KEY ("currentFrameExpenseId") REFERENCES "Expense"("id") ON DELETE CASCADE ON UPDATE CASCADE;
27 changes: 27 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ model Expense {
createdAt DateTime @default(now())
documents ExpenseDocument[]
notes String?

recurrenceRule RecurrenceRule? @default(NONE)
recurringExpenseLink RecurringExpenseLink?
recurringExpenseLinkId String?
}

model ExpenseDocument {
Expand All @@ -73,6 +77,29 @@ enum SplitMode {
BY_AMOUNT
}

model RecurringExpenseLink {
id String @id
groupId String
currentFrameExpense Expense @relation(fields: [currentFrameExpenseId], references: [id], onDelete: Cascade)
currentFrameExpenseId String @unique

// Note: We do not want to link to the next expense because once it is created, it should be
// treated as it's own independent entity. This means that if a user wants to delete an Expense
// and any prior related recurring expenses, they'll need to delete them one by one.
nextExpenseCreatedAt DateTime?
nextExpenseDate DateTime

@@index([groupId])
@@index([groupId, nextExpenseCreatedAt, nextExpenseDate(sort: Desc)])
}

enum RecurrenceRule {
NONE
DAILY
WEEKLY
MONTHLY
}

model ExpensePaidFor {
expense Expense @relation(fields: [expenseId], references: [id], onDelete: Cascade)
participant Participant @relation(fields: [participantId], references: [id], onDelete: Cascade)
Expand Down
45 changes: 45 additions & 0 deletions src/app/groups/[groupId]/expenses/expense-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { match } from 'ts-pattern'
import { DeletePopup } from '../../../../components/delete-popup'
import { extractCategoryFromTitle } from '../../../../components/expense-form-actions'
import { Textarea } from '../../../../components/ui/textarea'
import { RecurrenceRule } from '@prisma/client'

const enforceCurrencyPattern = (value: string) =>
value
Expand Down Expand Up @@ -165,6 +166,10 @@ export function ExpenseForm({
}
return field?.value
}

const getSelectedRecurrenceRule = (field?: { value: string }) => {
return field?.value as RecurrenceRule
}
const defaultSplittingOptions = getDefaultSplittingOptions(group)
const form = useForm<ExpenseFormValues>({
resolver: zodResolver(expenseFormSchema),
Expand All @@ -184,6 +189,7 @@ export function ExpenseForm({
isReimbursement: expense.isReimbursement,
documents: expense.documents,
notes: expense.notes ?? '',
recurrenceRule: expense.recurrenceRule,
}
: searchParams.get('reimbursement')
? {
Expand All @@ -207,6 +213,7 @@ export function ExpenseForm({
saveDefaultSplittingOptions: false,
documents: [],
notes: '',
recurrenceRule: RecurrenceRule.NONE,
}
: {
title: searchParams.get('title') ?? '',
Expand Down Expand Up @@ -234,6 +241,7 @@ export function ExpenseForm({
]
: [],
notes: '',
recurrenceRule: RecurrenceRule.NONE,
},
})
const [isCategoryLoading, setCategoryLoading] = useState(false)
Expand Down Expand Up @@ -494,6 +502,43 @@ export function ExpenseForm({
</FormItem>
)}
/>
<FormField
control={form.control}
name="recurrenceRule"
render={({ field }) => (
<FormItem className="sm:order-5">
<FormLabel>{t(`${sExpense}.recurrenceRule.label`)}</FormLabel>
<Select
onValueChange={(value) => {
form.setValue('recurrenceRule', value as RecurrenceRule)
}}
defaultValue={getSelectedRecurrenceRule(field)}
>
<SelectTrigger>
<SelectValue placeholder="NONE"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="NONE">
{t(`${sExpense}.recurrenceRule.none`)}
</SelectItem>
<SelectItem value="DAILY">
{t(`${sExpense}.recurrenceRule.daily`)}
</SelectItem>
<SelectItem value="WEEKLY">
{t(`${sExpense}.recurrenceRule.weekly`)}
</SelectItem>
<SelectItem value="MONTHLY">
{t(`${sExpense}.recurrenceRule.monthly`)}
</SelectItem>
</SelectContent>
</Select>
<FormDescription>
{t(`${sExpense}.recurrenceRule.description`)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>

Expand Down
1 change: 1 addition & 0 deletions src/app/groups/[groupId]/expenses/export/json/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export async function GET(
paidFor: { select: { participantId: true, shares: true } },
isReimbursement: true,
splitMode: true,
recurrenceRule: true,
},
},
participants: { select: { id: true, name: true } },
Expand Down
Loading