feat: agregar soporte para descripciones en métodos de pago y mejorar la interfaz de usuario

This commit is contained in:
David Arranz 2026-09-04 20:35:28 +02:00
parent 685472b3ef
commit 021c19fc40
17 changed files with 317 additions and 239 deletions

View File

@ -24,13 +24,13 @@ export const SettingsLayout = ({ sections }: SettingsLayoutProps) => {
return ( return (
<RdxPageFrame> <RdxPageFrame>
<RdxPageContainer width="full"> <RdxPageContainer className="space-y-6" width="full">
<RdxPageStack spacing="default"> <RdxPageHeader
<RdxPageHeader description={t("web.settings.description")}
description={t("web.settings.description")} title={t("web.settings.title")}
title={t("web.settings.title")} />
/>
<RdxPageStack spacing="default">
<div className="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]"> <div className="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
<aside className="min-w-0 lg:sticky lg:top-6 lg:self-start"> <aside className="min-w-0 lg:sticky lg:top-6 lg:self-start">
<SettingsNavigation sections={sections} /> <SettingsNavigation sections={sections} />

View File

@ -4,6 +4,7 @@ export type PaymentMethodSummary = {
id: UniqueID; id: UniqueID;
companyId: UniqueID; companyId: UniqueID;
name: Name; name: Name;
description: string | null;
isActive: boolean; isActive: boolean;
isSystem: boolean; isSystem: boolean;
isDefault: boolean; isDefault: boolean;

View File

@ -13,6 +13,7 @@ export class PaymentMethodSummarySnapshotBuilder implements IPaymentMethodSummar
company_id: paymentMethod.companyId.toString(), company_id: paymentMethod.companyId.toString(),
name: paymentMethod.name.toString(), name: paymentMethod.name.toString(),
description: paymentMethod.description,
is_system: paymentMethod.isSystem, is_system: paymentMethod.isSystem,
is_active: paymentMethod.isActive, is_active: paymentMethod.isActive,

View File

@ -0,0 +1,39 @@
import assert from "node:assert/strict";
import test from "node:test";
import { Sequelize } from "sequelize";
import { PaymentMethodSummarySchema } from "../../../../../../common/dto/payment-methods/shared/payment-method-summary.dto";
import { PaymentMethodSummarySnapshotBuilder } from "../../../../../application/payment-methods/snapshot-builders/summary/payment-method-summary-snapshot-builder";
import initializePaymentMethodModel, {
PaymentMethodModel,
} from "../models/sequelize-payment-method.model";
import { SequelizePaymentMethodSummaryMapper } from "./sequelize-payment-method-summary.mapper";
const database = new Sequelize({ dialect: "mysql", logging: false });
initializePaymentMethodModel(database);
for (const description of [
"Cargo automático en cuenta bancaria.",
"Descripción extensa ".repeat(10),
null,
]) {
test(`conserva la descripción ${description === null ? "ausente" : "textual"} desde persistencia hasta el DTO (${description?.length ?? 0})`, () => {
const source = {
id: "11111111-1111-4111-8111-111111111111",
company_id: "22222222-2222-4222-8222-222222222222",
name: "Método de prueba",
description,
is_active: true,
is_system: false,
is_default: true,
};
const result = new SequelizePaymentMethodSummaryMapper().mapToReadModel(
PaymentMethodModel.build(source)
);
assert.equal(result.isFailure, false);
const dto = new PaymentMethodSummarySnapshotBuilder().toOutput(result.data);
assert.deepEqual(PaymentMethodSummarySchema.parse(dto), source);
});
}

View File

@ -40,6 +40,7 @@ export class SequelizePaymentMethodSummaryMapper extends SequelizeQueryMapper<
id: id!, id: id!,
companyId: companyId!, companyId: companyId!,
name: name!, name: name!,
description: raw.description ?? null,
isActive: isActive, isActive: isActive,
isSystem: isSystem, isSystem: isSystem,
isDefault: isDefault, isDefault: isDefault,

View File

@ -5,6 +5,7 @@ export const PaymentMethodSummarySchema = z.object({
company_id: z.uuid(), company_id: z.uuid(),
name: z.string(), name: z.string(),
description: z.string().nullable(),
is_active: z.boolean(), is_active: z.boolean(),
is_system: z.boolean(), is_system: z.boolean(),
is_default: z.boolean(), is_default: z.boolean(),

View File

@ -3,6 +3,10 @@
"paymentMethods": { "paymentMethods": {
"title": "Payment methods", "title": "Payment methods",
"description": "Manage the payment methods available for the company.", "description": "Manage the payment methods available for the company.",
"add": {
"title": "Add a new payment method",
"description": "Set up a new payment method for your company."
},
"search": { "search": {
"placeholder": "Search method..." "placeholder": "Search method..."
}, },
@ -46,6 +50,7 @@
"active": "Active", "active": "Active",
"inactive": "Inactive", "inactive": "Inactive",
"system": "System", "system": "System",
"manual": "Manual",
"default": "Default" "default": "Default"
}, },
"create": { "create": {

View File

@ -3,6 +3,10 @@
"paymentMethods": { "paymentMethods": {
"title": "Métodos de pago", "title": "Métodos de pago",
"description": "Gestiona las formas de pago disponibles para la empresa.", "description": "Gestiona las formas de pago disponibles para la empresa.",
"add": {
"title": "Añadir un nuevo método de pago",
"description": "Configura una nueva forma de pago para tu empresa."
},
"search": { "search": {
"placeholder": "Buscar método..." "placeholder": "Buscar método..."
}, },
@ -46,6 +50,7 @@
"active": "Activo", "active": "Activo",
"inactive": "Inactivo", "inactive": "Inactivo",
"system": "Sistema", "system": "Sistema",
"manual": "Manual",
"default": "Predeterminado" "default": "Predeterminado"
}, },
"create": { "create": {

View File

@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import test from "node:test";
import type { ListPaymentMethodsResponseDTO } from "../../../../common";
import { ListPaymentMethodsAdapter } from "./list-payment-methods.adapter";
test("conserva descripciones largas y null sin alterar estados ni paginación", () => {
const descriptions = [
"Pago por transferencia bancaria.",
"Descripción extensa ".repeat(10),
null,
];
const dto: ListPaymentMethodsResponseDTO = {
page: 0,
per_page: 10,
total_pages: 1,
total_items: 3,
metadata: {
entity: "payment_methods",
criteria: {
q: "",
filters: [],
orderBy: "name",
orderType: "ASC",
pageSize: 10,
pageNumber: 0,
},
},
items: descriptions.map((description, index) => ({
id: `11111111-1111-4111-8111-11111111111${index}`,
company_id: "22222222-2222-4222-8222-222222222222",
name: "Método de prueba",
description,
is_active: index !== 2,
is_system: index === 0,
is_default: index === 0,
})),
};
const result = ListPaymentMethodsAdapter.fromDto(dto);
assert.deepEqual(
result.items,
dto.items.map((item) => ({
id: item.id,
companyId: item.company_id,
name: item.name,
description: item.description,
isActive: item.is_active,
isSystem: item.is_system,
isDefault: item.is_default,
}))
);
assert.equal(result.page, dto.page);
assert.equal(result.perPage, dto.per_page);
assert.equal(result.totalPages, dto.total_pages);
assert.equal(result.totalItems, dto.total_items);
});

View File

@ -45,6 +45,7 @@ const PaymentMehodListRowAdapter = {
companyId: dto.company_id, companyId: dto.company_id,
name: dto.name, name: dto.name,
description: dto.description,
isSystem: dto.is_system, isSystem: dto.is_system,
isActive: dto.is_active, isActive: dto.is_active,

View File

@ -10,6 +10,7 @@ export interface PaymentMethodListRow {
companyId: string; companyId: string;
name: string; name: string;
description: string | null;
isSystem: boolean; isSystem: boolean;
isActive: boolean; isActive: boolean;

View File

@ -1,4 +1,5 @@
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { CirclePlusIcon } from "lucide-react";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
@ -6,17 +7,18 @@ export const PaymentMethodsEmptyState = ({ onCreate }: { onCreate: () => void })
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="rounded-xl border border-dashed border-border bg-muted/20 p-8 text-center"> <Button
<div className="space-y-2"> className="h-auto min-h-28 w-full flex-col gap-2 whitespace-normal rounded-xl border-dashed px-4 py-6 text-center shadow-none transition-colors hover:bg-muted/50"
<h3 className="text-lg font-semibold">{t("settings.paymentMethods.empty.title")}</h3> onClick={onCreate}
<p className="text-sm text-muted-foreground"> variant="outline"
{t("settings.paymentMethods.empty.description")} >
</p> <span className="flex items-center justify-center gap-2">
</div> <CirclePlusIcon aria-hidden className="size-5 text-muted-foreground" />
{t("settings.paymentMethods.add.title")}
<Button className="mt-6" onClick={onCreate}> </span>
{t("settings.paymentMethods.actions.create")} <span className="text-sm font-normal text-muted-foreground">
</Button> {t("settings.paymentMethods.add.description")}
</div> </span>
</Button>
); );
}; };

View File

@ -1,7 +1,15 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { RdxBadge, RdxPageContainer } from "@repo/rdx-ui/components"; import { RdxBadge, RdxPageContainer } from "@repo/rdx-ui/components";
import { Button, Card, CardContent, RadioGroup, RadioGroupItem } from "@repo/shadcn-ui/components"; import {
import { PlusIcon } from "lucide-react"; Button,
Card,
CardContent,
CardHeader,
RadioGroup,
RadioGroupItem,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { CircleIcon, CreditCardIcon, PlusIcon, StarIcon } from "lucide-react";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
import { usePaymentMethodsSettingsPageController } from "../../controllers"; import { usePaymentMethodsSettingsPageController } from "../../controllers";
@ -71,46 +79,55 @@ export const PaymentMethodsSettingsPage = () => {
}; };
return ( return (
<RdxPageContainer width="full"> <>
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6"> <Card>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between"> <CardHeader className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-2"> <div className="flex min-w-0 items-start gap-4">
<h2 className="text-2xl font-semibold tracking-tight"> <div className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
{t("settings.paymentMethods.title")} <CreditCardIcon aria-hidden className="size-6" />
</h2> </div>
<p className="max-w-3xl text-sm text-muted-foreground"> <div className="min-w-0 space-y-1">
{t("settings.paymentMethods.description")} <h2 className="text-2xl font-semibold tracking-tight">
</p> {t("settings.paymentMethods.title")}
</h2>
<p className="text-sm text-muted-foreground">
{t("settings.paymentMethods.description")}
</p>
</div>
</div> </div>
{ctrl.list.length > 0 && (
<Button className="shrink-0 self-start" onClick={featureCtrl.openCreateDialog}>
<PlusIcon aria-hidden className="size-4" />
{t("settings.paymentMethods.actions.create")}
</Button>
)}
</CardHeader>
<CardContent className="@container space-y-3">
{ctrl.list.length > 0 && (
<RadioGroup
aria-label={t("settings.paymentMethods.default.label")}
className="min-w-0 grid-cols-1 gap-3"
disabled={featureCtrl.isChangingDefault}
onValueChange={(paymentMethodId) => {
const paymentMethod = ctrl.list.find((item) => item.id === paymentMethodId);
<Button onClick={featureCtrl.openCreateDialog}> if (!paymentMethod) {
<PlusIcon aria-hidden className="mr-2 size-4" /> return;
{t("settings.paymentMethods.actions.create")} }
</Button>
</div>
{ctrl.list.length === 0 ? ( featureCtrl.setDefault(paymentMethod);
<PaymentMethodsEmptyState onCreate={featureCtrl.openCreateDialog} /> }}
) : ( value={ctrl.list.find((item) => item.isDefault)?.id}
<RadioGroup >
aria-label={t("settings.paymentMethods.default.label")} {ctrl.list.map((paymentMethod) => (
className="space-y-3" <div
disabled={featureCtrl.isChangingDefault} className={cn(
onValueChange={(paymentMethodId) => { "flex min-w-0 flex-wrap items-center gap-4 rounded-xl border border-border p-4 @min-[52rem]:flex-nowrap",
const paymentMethod = ctrl.list.find((item) => item.id === paymentMethodId); paymentMethod.isDefault ? "border-primary/20 bg-primary/5" : "bg-card"
)}
if (!paymentMethod) { key={paymentMethod.id}
return; >
} <div className="flex min-w-0 basis-full items-center gap-3 @min-[52rem]:basis-0 @min-[52rem]:flex-1">
featureCtrl.setDefault(paymentMethod);
}}
value={ctrl.list.find((item) => item.isDefault)?.id}
>
{ctrl.list.map((paymentMethod) => (
<Card key={paymentMethod.id}>
<CardContent className="flex flex-col gap-4 p-4 md:flex-row md:items-center md:justify-between">
<div className="flex min-w-0 items-start gap-4">
<RadioGroupItem <RadioGroupItem
aria-label={t("settings.paymentMethods.default.rowLabel", { aria-label={t("settings.paymentMethods.default.rowLabel", {
name: paymentMethod.name, name: paymentMethod.name,
@ -118,116 +135,131 @@ export const PaymentMethodsSettingsPage = () => {
id={`payment-method-default-${paymentMethod.id}`} id={`payment-method-default-${paymentMethod.id}`}
value={paymentMethod.id} value={paymentMethod.id}
/> />
<div className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<div className="min-w-0 space-y-1"> <CreditCardIcon aria-hidden className="size-5" />
<div className="flex flex-wrap items-center gap-2"> </div>
<label <div className="min-w-0 flex-1 space-y-1">
className="cursor-pointer text-sm font-medium" <label
htmlFor={`payment-method-default-${paymentMethod.id}`} className="cursor-pointer break-words text-sm font-semibold text-foreground"
htmlFor={`payment-method-default-${paymentMethod.id}`}
>
{paymentMethod.name}
</label>
{paymentMethod.description && (
<p
className="truncate text-sm text-muted-foreground"
title={paymentMethod.description}
> >
{paymentMethod.name} {paymentMethod.description}
</label> </p>
)}
{paymentMethod.isSystem ? (
<RdxBadge variant="secondary">
{t("settings.paymentMethods.status.system")}
</RdxBadge>
) : null}
{paymentMethod.isDefault ? (
<RdxBadge variant="outline">
{t("settings.paymentMethods.status.default")}
</RdxBadge>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span>
{paymentMethod.isActive
? t("settings.paymentMethods.status.active")
: t("settings.paymentMethods.status.inactive")}
</span>
</div>
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center gap-2">
<PaymentMethodRowActions {paymentMethod.isDefault && (
activeDisabledReason={activeDisabledReason( <RdxBadge className="gap-1.5 rounded-full bg-primary/10 px-3 text-primary">
paymentMethod.isDefault, <StarIcon aria-hidden className="fill-current" />
paymentMethod.isSystem {t("settings.paymentMethods.status.default")}
</RdxBadge>
)} )}
deleteDisabledReason={deleteDisabledReason( <RdxBadge
paymentMethod.isDefault, className={cn(
paymentMethod.isSystem "gap-1.5 rounded-full px-3",
)} paymentMethod.isActive && "bg-primary/10 text-primary"
isTogglingActive={featureCtrl.isTogglingActive} )}
onDelete={featureCtrl.openDeleteDialog} variant={paymentMethod.isActive ? "secondary" : "destructive"}
onEdit={featureCtrl.openEditDialog} >
onToggleActive={(item) => { <CircleIcon aria-hidden className="fill-current" />
if (item.isActive) { {paymentMethod.isActive
featureCtrl.disable(item); ? t("settings.paymentMethods.status.active")
return; : t("settings.paymentMethods.status.inactive")}
} </RdxBadge>
<RdxBadge className="rounded-full px-3" variant="secondary">
{paymentMethod.isSystem
? t("settings.paymentMethods.status.system")
: t("settings.paymentMethods.status.manual")}
</RdxBadge>
</div>
<div className="ml-auto shrink-0">
<PaymentMethodRowActions
activeDisabledReason={activeDisabledReason(
paymentMethod.isDefault,
paymentMethod.isSystem
)}
deleteDisabledReason={deleteDisabledReason(
paymentMethod.isDefault,
paymentMethod.isSystem
)}
isTogglingActive={featureCtrl.isTogglingActive}
onDelete={featureCtrl.openDeleteDialog}
onEdit={featureCtrl.openEditDialog}
onToggleActive={(item) => {
if (item.isActive) {
featureCtrl.disable(item);
return;
}
featureCtrl.enable(item); featureCtrl.enable(item);
}} }}
paymentMethod={paymentMethod} paymentMethod={paymentMethod}
/> />
</CardContent> </div>
</Card> </div>
))} ))}
</RadioGroup> </RadioGroup>
)} )}
<PaymentMethodsEmptyState onCreate={featureCtrl.openCreateDialog} />
</CardContent>
</Card>
<PaymentMethodFormDialog <PaymentMethodFormDialog
description={t("settings.paymentMethods.create.description")} description={t("settings.paymentMethods.create.description")}
form={featureCtrl.form} form={featureCtrl.form}
formId={featureCtrl.formId} formId={featureCtrl.formId}
isEditMode={false} isEditMode={false}
isSubmitting={featureCtrl.isSaving} isSubmitting={featureCtrl.isSaving}
onSubmit={featureCtrl.onSubmit} onSubmit={featureCtrl.onSubmit}
open={featureCtrl.isCreateOpen} open={featureCtrl.isCreateOpen}
openChange={(open) => { openChange={(open) => {
if (!open) { if (!open) {
featureCtrl.closeCreateDialog(); featureCtrl.closeCreateDialog();
}
}}
submitLabel={t("settings.paymentMethods.actions.create")}
title={t("settings.paymentMethods.create.title")}
/>
<PaymentMethodFormDialog
description={t("settings.paymentMethods.edit.description")}
disableActiveField={featureCtrl.editingPaymentMethod?.isDefault === true}
form={featureCtrl.form}
formId={`${featureCtrl.formId}-edit`}
isEditMode
isSubmitting={featureCtrl.isSaving || featureCtrl.isEditingPaymentMethodLoading}
onSubmit={featureCtrl.onSubmit}
open={
Boolean(featureCtrl.editingPaymentMethod) || featureCtrl.isEditingPaymentMethodLoading
} }
openChange={(open) => { }}
if (!open) { submitLabel={t("settings.paymentMethods.actions.create")}
featureCtrl.closeEditDialog(); title={t("settings.paymentMethods.create.title")}
} />
}}
submitLabel={t("settings.paymentMethods.actions.save")}
title={t("settings.paymentMethods.edit.title")}
/>
<DeletePaymentMethodDialog <PaymentMethodFormDialog
isSubmitting={featureCtrl.isDeleting} description={t("settings.paymentMethods.edit.description")}
onConfirm={featureCtrl.confirmDelete} disableActiveField={featureCtrl.editingPaymentMethod?.isDefault === true}
onOpenChange={(open) => { form={featureCtrl.form}
if (!open) { formId={`${featureCtrl.formId}-edit`}
featureCtrl.closeDeleteDialog(); isEditMode
} isSubmitting={featureCtrl.isSaving || featureCtrl.isEditingPaymentMethodLoading}
}} onSubmit={featureCtrl.onSubmit}
open={Boolean(featureCtrl.deletingPaymentMethod)} open={
target={featureCtrl.deletingPaymentMethod} Boolean(featureCtrl.editingPaymentMethod) || featureCtrl.isEditingPaymentMethodLoading
/> }
</div> openChange={(open) => {
</RdxPageContainer> if (!open) {
featureCtrl.closeEditDialog();
}
}}
submitLabel={t("settings.paymentMethods.actions.save")}
title={t("settings.paymentMethods.edit.title")}
/>
<DeletePaymentMethodDialog
isSubmitting={featureCtrl.isDeleting}
onConfirm={featureCtrl.confirmDelete}
onOpenChange={(open) => {
if (!open) {
featureCtrl.closeDeleteDialog();
}
}}
open={Boolean(featureCtrl.deletingPaymentMethod)}
target={featureCtrl.deletingPaymentMethod}
/>
</>
); );
}; };

View File

@ -2,5 +2,4 @@ export * from "./empty-cell-value";
export * from "./error-alert"; export * from "./error-alert";
export * from "./error-state"; export * from "./error-state";
export * from "./form"; export * from "./form";
export * from "./page-header";
export * from "./page-error-state"; export * from "./page-error-state";

View File

@ -1,71 +0,0 @@
import { Button } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { ChevronLeftIcon } from "lucide-react";
import type { ReactNode } from "react";
interface PageHeaderProps {
title: ReactNode;
description?: ReactNode;
rightSlot?: ReactNode;
showBackButton?: boolean;
onBackClick?: () => void;
backButtonLabel?: string;
statusSlot?: ReactNode;
className?: string;
}
export const PageHeader = ({
title,
description,
rightSlot,
showBackButton = false,
onBackClick,
backButtonLabel = "Volver",
statusSlot,
className,
}: PageHeaderProps) => {
return (
<header
className={cn(
"mb-6 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between",
className
)}
>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-start gap-4">
{showBackButton && (
<Button
aria-label={backButtonLabel}
className="shrink-0 cursor-pointer"
onClick={onBackClick}
size="default"
type="button"
variant="outline"
>
<ChevronLeftIcon aria-hidden="true" className="size-5" />
</Button>
)}
<div className="min-w-0 space-y-2">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<h1 className="min-w-0 truncate text-2xl font-semibold tracking-tight text-foreground xl:text-3xl font-heading">
{title}
</h1>
{statusSlot}
</div>
{description && (
<p className="text-sm font-medium text-muted-foreground">{description}</p>
)}
</div>
</div>
</div>
{rightSlot && <div className="flex shrink-0 items-center lg:pt-0">{rightSlot}</div>}
</header>
);
};

View File

@ -1,6 +1,11 @@
import { ErrorAlert, PageErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components"; import { ErrorAlert, PageErrorState, SimpleSearchInput } from "@erp/core/components";
import { useReturnToNavigation } from "@erp/core/hooks"; import { useReturnToNavigation } from "@erp/core/hooks";
import { LogoVerifactu, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components"; import {
LogoVerifactu,
RdxPageContainer,
RdxPageFrame,
RdxPageHeader,
} from "@repo/rdx-ui/components";
import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components"; import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@ -96,9 +101,8 @@ export const ListIssuedInvoicesPage = () => {
return ( return (
<RdxPageFrame> <RdxPageFrame>
<RdxPageContainer className="space-y-6" width="full"> <RdxPageContainer className="space-y-6" width="full">
<PageHeader <RdxPageHeader
description={t("pages.issued_invoices.list.description")} actions={
rightSlot={
<Button <Button
aria-label={t("pages.issued_invoices.create.title")} aria-label={t("pages.issued_invoices.create.title")}
className="hidden" className="hidden"
@ -108,6 +112,7 @@ export const ListIssuedInvoicesPage = () => {
{t("pages.issued_invoices.create.title")} {t("pages.issued_invoices.create.title")}
</Button> </Button>
} }
description={t("pages.issued_invoices.list.description")}
title={t("pages.issued_invoices.list.title")} title={t("pages.issued_invoices.list.title")}
/> />

View File

@ -39,14 +39,13 @@ export const RdxPageHeader = ({
{leading ? <div className="shrink-0">{leading}</div> : null} {leading ? <div className="shrink-0">{leading}</div> : null}
<div className="min-w-0 space-y-2"> <div className="min-w-0 space-y-2">
<h1 className="min-w-0 text-2xl font-semibold tracking-tight text-foreground font-heading xl:text-3xl"> <h1 className="min-w-0 text-2xl font-bold tracking-tight text-foreground font-heading xl:text-3xl">
{title} {title}
</h1> </h1>
{description ? ( {description ? (
<p className="text-sm font-medium text-muted-foreground">{description}</p> <p className="text-sm font-medium text-muted-foreground">{description}</p>
) : null} ) : null}
{metadata ? ( {metadata ? (
<div className="flex min-w-0 flex-wrap items-center gap-2 text-sm text-muted-foreground"> <div className="flex min-w-0 flex-wrap items-center gap-2 text-sm text-muted-foreground">
{metadata} {metadata}