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
27 changes: 22 additions & 5 deletions src/components/configuration/project-system-templates-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import { toast } from 'sonner';
import { deleteTemplateAction } from '@/actions/templates/mutations/delete-template';
import { duplicateTemplateAction } from '@/actions/templates/mutations/duplicate-template';
import { getProjectSystemTemplatesOverviewAction } from '@/actions/templates/queries/get-project-system-templates-overview';
import type { ProjectSystemTemplateOverviewRow } from '@/lib/templates/project-system-templates-overview';
import { ConfirmDialog } from '@/components/generic/confirm-dialog';
import { Badge } from '@/components/ui/badge';
import { DataTable } from '@/components/ui/data-table';
import { DataTableColumnHeader } from '@/components/ui/data-table-column-header';
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { getDatasetDisplayName } from '@/lib/templates/merge-tags';
import type { ProjectSystemTemplateOverviewRow } from '@/lib/templates/project-system-templates-overview';

interface ProjectSystemTemplatesTableProps {
projectId: string;
Expand Down Expand Up @@ -92,7 +93,8 @@ export function ProjectSystemTemplatesTable({ projectId, initialRows }: ProjectS
() => [
{
accessorKey: 'name',
header: t('list.columns.name'),
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader column={column} title={t('list.columns.name')} />,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<span className="font-medium">{row.original.name}</span>
Expand All @@ -115,17 +117,32 @@ export function ProjectSystemTemplatesTable({ projectId, initialRows }: ProjectS
},
{
accessorKey: 'type',
header: t('list.columns.type'),
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader column={column} title={t('list.columns.type')} />,
cell: ({ row }) => <span>{row.original.type === 'EMAIL' ? t('types.email') : t('types.document')}</span>,
},
{
accessorKey: 'dataset',
header: t('list.columns.dataset'),
enableSorting: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={t('list.columns.dataset')}
description={t('list.columns.datasetDescription')}
/>
),
cell: ({ row }) => <span>{getDatasetDisplayName(row.original.dataset)}</span>,
},
{
accessorKey: 'createdAt',
header: t('list.columns.createdAt'),
enableSorting: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={t('list.columns.createdAtShort')}
longTitle={t('list.columns.createdAt')}
/>
),
cell: ({ row }) => <span>{new Intl.DateTimeFormat('de-DE').format(new Date(row.original.createdAt))}</span>,
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function EntityFilterControl({
<NumberFilter
filterState={filterState}
allowEmpty={definition.allowEmpty}
defaultOperator={definition.defaultOperator}
onFilterChange={(v) => onChange(v)}
variant="stacked"
size="sm"
Expand Down
9 changes: 7 additions & 2 deletions src/components/filters/date-filter-with-operator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,16 @@ function RelativeDateAmountFields({
<Input
type="number"
step={1}
value={value.amount}
value={value.amount ?? ''}
onChange={(e) => {
const raw = e.target.value;
if (raw === '') {
onChange({ amount: null, unit: value.unit });
return;
}
const parsed = Number.parseInt(raw, 10);
onChange({
amount: raw === '' ? 0 : Number.parseInt(raw, 10) || 0,
amount: Number.isFinite(parsed) ? parsed : null,
unit: value.unit,
});
}}
Expand Down
8 changes: 8 additions & 0 deletions src/components/filters/filter-field-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ export function filterValueSegmentClass(size: FilterFieldSize = 'default') {
);
}

/** Compact fixed-width number inputs (narrower than flex text fields). */
export function filterNumberValueSegmentClass(size: FilterFieldSize = 'default') {
return cn(
'w-24 shrink-0 shadow-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',
filterSizeClass(size),
);
}

/** Comparison symbols (=, >, <, …) read small at default select size. */
export const FILTER_SYMBOL_OPERATORS = new Set(['eq', 'gt', 'lt', 'gte', 'lte']);

Expand Down
42 changes: 36 additions & 6 deletions src/components/filters/number-filter-with-operator.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
'use client';

import { useTranslations } from 'next-intl';
import { useMemo, type ReactNode } from 'react';
import { useMemo, type KeyboardEvent, type ReactNode } from 'react';

import {
FilterFieldGroup,
FilterStackedFields,
filterNumberValueSegmentClass,
filterOperatorSegmentClass,
filterSymbolOperatorLabelClass,
filterSymbolOperatorTriggerClass,
filterValueSegmentClass,
type FilterFieldSize,
type FilterFieldVariant,
} from '@/components/filters/filter-field-group';
Expand All @@ -20,11 +20,18 @@ import {
createDefaultNumberFilterValueForOperator,
NUMBER_FILTER_EMPTY_OPERATORS,
NUMBER_FILTER_OPERATORS,
type NumberFilterOperator,
type NumberFilterOperatorWithEmpty,
type NumberFilterValue,
parseNumberFilterValue,
} from '@/types/number-filter-value';

const NUMBER_OPERATOR_SHORTCUTS: Record<string, 'eq' | 'gt' | 'lt'> = {
'=': 'eq',
'>': 'gt',
'<': 'lt',
};

function hasPayload(operator: NumberFilterOperatorWithEmpty): boolean {
return operator !== 'empty' && operator !== 'notEmpty';
}
Expand All @@ -33,19 +40,24 @@ export function NumberFilterWithOperator({
value,
onChange,
allowEmpty = false,
defaultOperator = 'between',
translationNamespace = 'dataTable',
variant = 'row',
size = 'default',
}: {
value: unknown;
onChange: (value: NumberFilterValue) => void;
allowEmpty?: boolean;
defaultOperator?: NumberFilterOperator;
translationNamespace?: string;
variant?: FilterFieldVariant;
size?: FilterFieldSize;
}) {
const t = useTranslations(translationNamespace);
const parsed = useMemo(() => parseNumberFilterValue(value), [value]);
const parsed = useMemo(
() => parseNumberFilterValue(value, defaultOperator),
[value, defaultOperator],
);

const availableOperators = useMemo(() => {
const emptyOps = allowEmpty ? [...NUMBER_FILTER_EMPTY_OPERATORS] : [];
Expand All @@ -59,6 +71,21 @@ export function NumberFilterWithOperator({
onChange(createDefaultNumberFilterValueForOperator(operator));
};

const handleOperatorShortcut = (
event: KeyboardEvent<HTMLInputElement>,
fieldValue: number | null,
) => {
const operator = NUMBER_OPERATOR_SHORTCUTS[event.key];
if (!operator) {
return;
}
event.preventDefault();
if (parsed.operator === operator) {
return;
}
onChange({ operator, value: fieldValue });
};

const showPayload = hasPayload(parsed.operator);
const useStackedLayout = variant === 'stacked' && showPayload;

Expand Down Expand Up @@ -95,6 +122,7 @@ export function NumberFilterWithOperator({
type="number"
placeholder={t('numberFilterMin')}
value={parsed.min ?? ''}
onKeyDown={(e) => handleOperatorShortcut(e, parsed.min)}
onChange={(e) => {
const next = e.target.value === '' ? null : Number(e.target.value);
onChange({
Expand All @@ -103,12 +131,13 @@ export function NumberFilterWithOperator({
max: parsed.max,
});
}}
className={filterValueSegmentClass(size)}
className={filterNumberValueSegmentClass(size)}
/>
<Input
type="number"
placeholder={t('numberFilterMax')}
value={parsed.max ?? ''}
onKeyDown={(e) => handleOperatorShortcut(e, parsed.max)}
onChange={(e) => {
const next = e.target.value === '' ? null : Number(e.target.value);
onChange({
Expand All @@ -117,7 +146,7 @@ export function NumberFilterWithOperator({
max: next,
});
}}
className={filterValueSegmentClass(size)}
className={filterNumberValueSegmentClass(size)}
/>
</>
);
Expand All @@ -133,14 +162,15 @@ export function NumberFilterWithOperator({
type="number"
placeholder={t('numberFilterValue')}
value={parsed.value ?? ''}
onKeyDown={(e) => handleOperatorShortcut(e, parsed.value)}
onChange={(e) => {
const next = e.target.value === '' ? null : Number(e.target.value);
onChange({
operator: parsed.operator,
value: next,
});
}}
className={filterValueSegmentClass(size)}
className={filterNumberValueSegmentClass(size)}
/>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/lenders/loan-selector-item-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function LoanSelectorItemRow({ loan, highlightActive = true }: LoanSelect
<span className="font-medium text-foreground">{interestRateStr}</span>
</div>
<div>
{tLoan('table.balanceShort')} <span className="font-medium text-foreground">{balanceStr}</span>
{tLoan('table.balanceLabel')}: <span className="font-medium text-foreground">{balanceStr}</span>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/lenders/loan-selector-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function LoanSelectorItem({ loan }: LoanSelectorItemProps) {
<span className="font-medium text-foreground">{interestRateStr}</span>
</div>
<div>
{tLoan('table.balanceShort')} <span className="font-medium text-foreground">{balanceStr}</span>
{tLoan('table.balanceLabel')}: <span className="font-medium text-foreground">{balanceStr}</span>
</div>
</div>
</div>
Expand Down
25 changes: 21 additions & 4 deletions src/components/templates/template-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { deleteTemplateAction } from '@/actions/templates/mutations/delete-templ
import { duplicateTemplateAction } from '@/actions/templates/mutations/duplicate-template';
import { Badge } from '@/components/ui/badge';
import { DataTable } from '@/components/ui/data-table';
import { DataTableColumnHeader } from '@/components/ui/data-table-column-header';
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useRouter } from '@/i18n/navigation';
Expand Down Expand Up @@ -103,7 +104,8 @@ export function TemplateList({ project, templates: externalTemplates, isAdmin }:
() => [
{
accessorKey: 'name',
header: t('list.columns.name'),
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader column={column} title={t('list.columns.name')} />,
cell: ({ row }) => (
<div className="flex items-center gap-2">
{row.original.type === 'EMAIL' ? (
Expand Down Expand Up @@ -132,17 +134,32 @@ export function TemplateList({ project, templates: externalTemplates, isAdmin }:
},
{
accessorKey: 'type',
header: t('list.columns.type'),
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader column={column} title={t('list.columns.type')} />,
cell: ({ row }) => <span>{row.original.type === 'EMAIL' ? t('types.email') : t('types.document')}</span>,
},
{
accessorKey: 'dataset',
header: t('list.columns.dataset'),
enableSorting: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={t('list.columns.dataset')}
description={t('list.columns.datasetDescription')}
/>
),
cell: ({ row }) => <span>{getDatasetDisplayName(row.original.dataset)}</span>,
},
{
accessorKey: 'createdAt',
header: t('list.columns.createdAt'),
enableSorting: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={t('list.columns.createdAtShort')}
longTitle={t('list.columns.createdAt')}
/>
),
cell: ({ row }) => <span>{new Intl.DateTimeFormat('de-DE').format(new Date(row.original.createdAt))}</span>,
},
],
Expand Down
30 changes: 23 additions & 7 deletions src/components/transactions/bank-import-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox';
import type { BulkAction } from '@/components/ui/data-table';
import { DataTable } from '@/components/ui/data-table';
import { DataTableColumnHeader } from '@/components/ui/data-table-column-header';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useRouter } from '@/i18n/navigation';
Expand Down Expand Up @@ -201,12 +202,14 @@ export function BankImportClient({ projectId, accounts, initialRows, initialAcco
() => [
{
accessorKey: 'bookingDate',
header: t('columns.date'),
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader column={column} title={t('columns.date')} />,
cell: ({ row }) => format.dateTime(row.original.bookingDate, { dateStyle: 'medium' }),
},
{
id: 'counterparty',
header: t('columns.counterparty'),
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader column={column} title={t('columns.counterparty')} />,
cell: ({ row }) => (
<div className="min-w-[140px]">
<div className="font-medium truncate">{row.original.counterpartyName ?? '—'}</div>
Expand All @@ -218,14 +221,22 @@ export function BankImportClient({ projectId, accounts, initialRows, initialAcco
},
{
accessorKey: 'remittanceInfo',
header: t('columns.remittance'),
enableSorting: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={t('columns.remittanceShort')}
longTitle={t('columns.remittance')}
/>
),
cell: ({ row }) => (
<span className="line-clamp-2 max-w-[220px] text-sm">{row.original.remittanceInfo ?? '—'}</span>
),
},
{
accessorKey: 'amount',
header: t('columns.amount'),
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader column={column} title={t('columns.amount')} />,
cell: ({ row }) => (
<span className={row.original.amount >= 0 ? 'text-success-foreground' : 'text-destructive'}>
{formatCurrency(row.original.amount)} {row.original.currency}
Expand All @@ -234,7 +245,10 @@ export function BankImportClient({ projectId, accounts, initialRows, initialAcco
},
{
id: 'lender',
header: t('columns.lender'),
enableSorting: false,
header: ({ column }) => (
<DataTableColumnHeader column={column} title={t('columns.lenderShort')} longTitle={t('columns.lender')} />
),
cell: ({ row }) => (
<div className="min-w-[200px]">
<Combobox
Expand All @@ -249,7 +263,8 @@ export function BankImportClient({ projectId, accounts, initialRows, initialAcco
},
{
id: 'loan',
header: t('columns.loan'),
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader column={column} title={t('columns.loan')} />,
cell: ({ row }) => {
const lenderId = row.original.selectedLenderId;
const lenderLoans = lenderId ? (loansByLenderId.get(lenderId) ?? EMPTY_LOANS) : EMPTY_LOANS;
Expand All @@ -266,7 +281,8 @@ export function BankImportClient({ projectId, accounts, initialRows, initialAcco
},
{
id: 'type',
header: t('columns.type'),
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader column={column} title={t('columns.type')} />,
cell: ({ row }) => {
if (row.original.amount > 0) {
return <span className="text-sm">{commonT('enums.transaction.type.DEPOSIT')}</span>;
Expand Down
Loading