Skip to content
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
10 changes: 9 additions & 1 deletion src/components/form/form-date-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ interface FormDatePickerProps {
label?: string;
placeholder?: string;
disabled?: (date: Date) => boolean;
className?: string;
}

export function FormDatePicker({ name, label, placeholder = 'Pick a date', disabled }: FormDatePickerProps) {
export function FormDatePicker({
name,
label,
placeholder = 'Pick a date',
disabled,
className,
}: FormDatePickerProps) {
const form = useFormContext();

return (
Expand All @@ -28,6 +35,7 @@ export function FormDatePicker({ name, label, placeholder = 'Pick a date', disab
onChange={(date) => field.onChange(date ?? '')}
placeholder={placeholder}
calendarDisabled={disabled}
className={className}
/>
<FormMessage />
</FormItem>
Expand Down
12 changes: 8 additions & 4 deletions src/components/form/form-number-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useFormContext } from 'react-hook-form';

import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { NumberParser } from '@/lib/utils';
import { cn, NumberParser } from '@/lib/utils';

interface FormNumberInputProps {
name: string;
Expand All @@ -18,6 +18,7 @@ interface FormNumberInputProps {
minimumFractionDigits?: number;
maximumFractionDigits?: number;
autoFocus?: boolean;
className?: string;
}

export function FormNumberInput({
Expand All @@ -32,6 +33,7 @@ export function FormNumberInput({
minimumFractionDigits = 2,
maximumFractionDigits = 2,
autoFocus,
className,
}: FormNumberInputProps) {
const form = useFormContext();

Expand Down Expand Up @@ -77,9 +79,11 @@ export function FormNumberInput({
const value = parser.strip(event.target.value);
field.onChange(value);
}}
className={`[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ${
prefix ? 'pl-12 ' : ''
}`}
className={cn(
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
prefix && 'pl-12',
className,
)}
/>
</div>
</FormControl>
Expand Down
5 changes: 4 additions & 1 deletion src/components/form/form-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select';
import { cn } from '@/lib/utils';

type SelectOption =
| {
Expand All @@ -35,6 +36,7 @@ interface FormSelectProps {
align?: 'start' | 'center' | 'end';
customContent?: () => ReactNode;
clearable?: boolean;
className?: string;
}

export function FormSelect({
Expand All @@ -49,6 +51,7 @@ export function FormSelect({
align = 'start',
customContent,
clearable = false,
className,
}: FormSelectProps) {
const t = useTranslations('common');
const form = useFormContext();
Expand All @@ -63,7 +66,7 @@ export function FormSelect({
<div className="relative">
<Select disabled={disabled} onValueChange={field.onChange} value={field.value || undefined}>
<FormControl>
<SelectTrigger className={field.value ? '' : 'text-muted-foreground/60'}>
<SelectTrigger className={cn(!field.value && 'text-muted-foreground/60', className)}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
</FormControl>
Expand Down
2 changes: 1 addition & 1 deletion src/components/loans/loan-add-transaction-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function LoanAddTransactionControl({ loanId, loan, className }: LoanAddTr
<Plus className="mr-2 h-4 w-4" />
{commonT('terms.transaction')}
</Button>
<TransactionDialog loanId={loanId} open={open} onOpenChange={setOpen} />
<TransactionDialog loanId={loanId} loan={loan} open={open} onOpenChange={setOpen} />
</>
);
}
32 changes: 28 additions & 4 deletions src/components/loans/transaction-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@

import { zodResolver } from '@hookform/resolvers/zod';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { Wallet } from 'lucide-react';
import { useLocale, useTranslations } from 'next-intl';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';

import { addTransactionAction } from '@/actions/loans';
import { FormCheckbox } from '@/components/form/form-checkbox';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Form } from '@/components/ui/form';
import { getTransactionDialogLoanContext } from '@/lib/loans/transaction-dialog-loan-context';
import { transactionFormSchema } from '@/lib/schemas/transaction';
import type { LoanDetailsWithCalculations } from '@/types/loans';

import { TransactionFormFields } from './transaction-form-fields';

interface TransactionDialogProps {
loanId: string;
loan: LoanDetailsWithCalculations;
open: boolean;
onOpenChange: (open: boolean) => void;
}

export function TransactionDialog({ loanId, open, onOpenChange }: TransactionDialogProps) {
export function TransactionDialog({ loanId, loan, open, onOpenChange }: TransactionDialogProps) {
const t = useTranslations('dashboard.loans');
const commonT = useTranslations('common');
const locale = useLocale();
const queryClient = useQueryClient();

const defaultValues = {
Expand Down Expand Up @@ -61,11 +66,30 @@ export function TransactionDialog({ loanId, open, onOpenChange }: TransactionDia
queryClient.invalidateQueries({ queryKey: ['loans'] });
});

const { loanLabel, generalInfo, detailInfo } = getTransactionDialogLoanContext(
loan,
(key, values) => t(`transactions.${key}`, values),
locale,
);

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogHeader className="space-y-3">
<DialogTitle>{t('transactions.createTitle')}</DialogTitle>
<DialogDescription asChild className="text-foreground">
<div className="flex gap-3 rounded-md border border-border bg-white p-3 text-left text-sm text-foreground">
<Wallet className="mt-0.5 h-4 w-4 shrink-0 text-foreground" aria-hidden />
<div className="min-w-0 space-y-1 text-foreground">
<p>
<span>{loanLabel}</span>
{generalInfo ? ` ${generalInfo}` : null}
{!generalInfo ? ` ${detailInfo}` : null}
</p>
{generalInfo ? <p>{detailInfo}</p> : null}
</div>
</div>
</DialogDescription>
</DialogHeader>

<Form {...form}>
Expand Down
4 changes: 4 additions & 0 deletions src/components/loans/transaction-form-fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export function TransactionFormFields({ loanId }: { loanId: string }) {
name="type"
label={t('transactions.type')}
placeholder={commonT('ui.form.selectPlaceholder')}
className="bg-white"
options={[
createTypeOption(TransactionType.DEPOSIT, !!loanToDate && loanToDate?.deposits >= loanToDate.amount),
'divider',
Expand All @@ -120,6 +121,7 @@ export function TransactionFormFields({ loanId }: { loanId: string }) {
name="date"
label={t('transactions.date')}
placeholder={commonT('ui.form.enterPlaceholder')}
className="bg-white"
disabled={(date) => {
if (date > new Date()) {
return true;
Expand All @@ -141,6 +143,7 @@ export function TransactionFormFields({ loanId }: { loanId: string }) {
min={minAmount}
max={maxAmount}
prefix="€"
className="bg-white"
disabled={
!type ||
type === TransactionType.TERMINATION ||
Expand All @@ -154,6 +157,7 @@ export function TransactionFormFields({ loanId }: { loanId: string }) {
name="paymentType"
label={t('transactions.paymentType')}
placeholder={commonT('ui.form.selectPlaceholder')}
className="bg-white"
options={[
{
value: 'BANK',
Expand Down
85 changes: 85 additions & 0 deletions src/lib/loans/transaction-dialog-loan-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { SavingsRateType } from '@prisma/client';

import { formatCurrency, formatDateShort } from '@/lib/utils';
import type { LoanDetailsWithCalculations } from '@/types/loans';

type LoanForTransactionContext = Pick<
LoanDetailsWithCalculations,
| 'loanNumber'
| 'isSavingsContract'
| 'savingsRateType'
| 'savingsMonthlyAmount'
| 'savingsDepositCount'
| 'requiredDepositsCount'
| 'outstandingDepositsCount'
| 'outstandingDepositSum'
| 'outstandingDepositSinceDate'
>;

type TranslateTransactions = (
key:
| 'createContextLoanLabel'
| 'createContextSavingsGeneralFixed'
| 'createContextSavingsGeneralVarying'
| 'createContextSavingsOutstanding'
| 'createContextRegularOutstanding'
| 'createContextNotOutstanding',
values?: Record<string, string | number>,
) => string;

export type TransactionDialogLoanContext = {
loanLabel: string;
generalInfo: string | null;
detailInfo: string;
};

export function getTransactionDialogLoanContext(
loan: LoanForTransactionContext,
t: TranslateTransactions,
locale: string,
): TransactionDialogLoanContext {
const loanLabel = t('createContextLoanLabel', { loanNumber: loan.loanNumber });
const isOutstanding = loan.outstandingDepositsCount > 0 && loan.outstandingDepositSinceDate != null;
const date = isOutstanding ? formatDateShort(loan.outstandingDepositSinceDate, locale) : '';

if (!isOutstanding) {
return {
loanLabel,
generalInfo: null,
detailInfo: t('createContextNotOutstanding'),
};
}

if (loan.isSavingsContract) {
const total = loan.requiredDepositsCount || loan.savingsDepositCount || 0;
const current = Math.max(1, total - loan.outstandingDepositsCount + 1);
const count = total;
const detailInfo = t('createContextSavingsOutstanding', { current, total, date });

if (loan.savingsRateType === SavingsRateType.FIXED && loan.savingsMonthlyAmount != null) {
return {
loanLabel,
generalInfo: t('createContextSavingsGeneralFixed', {
count,
installmentAmount: formatCurrency(loan.savingsMonthlyAmount, locale),
}),
detailInfo,
};
}

return {
loanLabel,
generalInfo: t('createContextSavingsGeneralVarying', { count }),
detailInfo,
};
}

return {
loanLabel,
generalInfo: null,
detailInfo: t('createContextRegularOutstanding', {
amount: formatCurrency(loan.outstandingDepositSum ?? 0, locale),
date,
}),
};
}
6 changes: 6 additions & 0 deletions src/messages/de/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,12 @@
"deleteError": "Fehler beim Löschen der Zahlung",
"noTransactions": "Noch keine Zahlungen",
"createTitle": "Neue Zahlung",
"createContextLoanLabel": "Kredit #{loanNumber}:",
"createContextSavingsGeneralFixed": "Ansparvertrag mit {count, plural, one {# Rate} other {# Raten}} zu {installmentAmount}",
"createContextSavingsGeneralVarying": "Ansparvertrag mit {count, plural, one {# Rate} other {# Raten}}",
"createContextSavingsOutstanding": "Zahlung {current} von {total} ausstehend seit {date}",
"createContextRegularOutstanding": "Ausstehende Zahlung über {amount} seit {date}",
"createContextNotOutstanding": "Keine Zahlung ausstehend",
"createSuccess": "Zahlung erfolgreich erstellt",
"createError": "Fehler beim Erstellen der Zahlung",
"type": "Zahlungstyp",
Expand Down