feat: mejorar la interfaz de usuario en las secciones de configuración de verifactu y detalles de la empresa

This commit is contained in:
David Arranz 2026-09-05 11:52:53 +02:00
parent bbca943c87
commit 81da489ca8
6 changed files with 290 additions and 244 deletions

View File

@ -77,7 +77,18 @@ mock.module("@repo/rdx-ui/components", {
RdxPanel: ({ children, title }: { children: ReactNode; title: string }) => (
<section data-title={title}>{children}</section>
),
RdxEditorStickyFooter: () => null,
RdxEditorActionBar: ({
primary,
secondary,
}: {
primary?: ReactNode;
secondary?: ReactNode;
}) => (
<div>
{secondary}
{primary}
</div>
),
RdxFormGrid: ({ children }: { children: ReactNode }) => <div>{children}</div>,
RhfSubmitActionButton: (props: Record<string, unknown>) => {
capturedSubmitProps = props;
@ -94,6 +105,14 @@ mock.module("@repo/rdx-ui/components", {
},
});
mock.module("@repo/shadcn-ui/components", {
namedExports: {
Card: ({ children }: { children: ReactNode }) => <section>{children}</section>,
CardContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
CardHeader: ({ children }: { children: ReactNode }) => <header>{children}</header>,
},
});
mock.module("react-hook-form", {
namedExports: {
FormProvider: ({ children }: { children: ReactNode }) => children,

View File

@ -10,7 +10,8 @@ import {
SelectField,
TextField,
} from "@repo/rdx-ui/components";
import { ContactIcon, LandmarkIcon, ScrollTextIcon } from "lucide-react";
import { Card, CardContent, CardHeader } from "@repo/shadcn-ui/components";
import { Building2Icon, ContactIcon, LandmarkIcon, ScrollTextIcon } from "lucide-react";
import { FormProvider } from "react-hook-form";
import { useTranslation } from "../../../../i18n";
@ -64,178 +65,194 @@ export const CompanyDetailsSettingsPage = () => {
return (
<FormProvider {...ctrl.form}>
<UnsavedChangesProvider isDirty={ctrl.form.formState.isDirty}>
<div className="flex min-h-[24rem] flex-col gap-6">
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">
{t("settings.companyDetails.title")}
</h2>
<p className="max-w-3xl text-sm text-muted-foreground">
{t("settings.companyDetails.description")}
<Card>
<CardHeader>
<div className="flex min-w-0 items-start gap-4">
<div className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<Building2Icon aria-hidden className="size-6" />
</div>
<div className="min-w-0 space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">
{t("settings.companyDetails.title")}
</h2>
<p className="text-sm text-muted-foreground">
{t("settings.companyDetails.description")}
</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{ctrl.inlineLoadError ? (
<ErrorAlert
message={ctrl.inlineLoadError.description}
title={ctrl.inlineLoadError.title}
/>
) : null}
<form
className="flex flex-col gap-6"
id={ctrl.formId}
noValidate
onKeyDown={ctrl.onFormKeyDown}
onSubmit={ctrl.onSubmit}
>
<RdxPanel
description={t("settings.companyDetails.sections.general.description")}
icon={<ScrollTextIcon className="size-5" />}
title={t("settings.companyDetails.sections.general.title")}
variant="plain"
>
<RdxFormGrid className="md:grid-cols-2">
<TextField<CompanySettingsForm>
className="col-span-full"
description={t(
"settings.companyDetails.sections.general.fields.legalName.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.general.fields.legalName.label")}
name="legalName"
placeholder={t(
"settings.companyDetails.sections.general.fields.legalName.placeholder"
)}
required
/>
<TextField<CompanySettingsForm>
description={t(
"settings.companyDetails.sections.general.fields.tradeName.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.general.fields.tradeName.label")}
name="tradeName"
placeholder={t(
"settings.companyDetails.sections.general.fields.tradeName.placeholder"
)}
/>
<TextField<CompanySettingsForm>
description={t(
"settings.companyDetails.sections.general.fields.tin.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.general.fields.tin.label")}
name="tin"
placeholder={t(
"settings.companyDetails.sections.general.fields.tin.placeholder"
)}
/>
</RdxFormGrid>
</RdxPanel>
<RdxPanel
description={t("settings.companyDetails.sections.contact.description")}
icon={<ContactIcon className="size-5" />}
title={t("settings.companyDetails.sections.contact.title")}
variant="plain"
>
<RdxFormGrid className="md:grid-cols-2">
<TextField<CompanySettingsForm>
description={t(
"settings.companyDetails.sections.contact.fields.email.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.contact.fields.email.label")}
name="email"
placeholder={t(
"settings.companyDetails.sections.contact.fields.email.placeholder"
)}
typePreset="email"
/>
<TextField<CompanySettingsForm>
description={t(
"settings.companyDetails.sections.contact.fields.phone.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.contact.fields.phone.label")}
name="phone"
placeholder={t(
"settings.companyDetails.sections.contact.fields.phone.placeholder"
)}
typePreset="phone"
/>
<TextField<CompanySettingsForm>
className="col-span-full"
description={t(
"settings.companyDetails.sections.contact.fields.website.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.contact.fields.website.label")}
name="website"
placeholder={t(
"settings.companyDetails.sections.contact.fields.website.placeholder"
)}
typePreset="url"
/>
</RdxFormGrid>
</RdxPanel>
<RdxPanel
description={t("settings.companyDetails.sections.paymentTerm.description")}
icon={<LandmarkIcon className="size-5" />}
title={t("settings.companyDetails.sections.paymentTerm.title")}
variant="plain"
>
<RdxFormGrid className="md:grid-cols-2">
<SelectField<CompanySettingsForm>
className="col-span-full"
clearable
clearLabel={t("settings.companyDetails.sections.paymentTerm.field.none")}
description={t(
"settings.companyDetails.sections.paymentTerm.field.description"
)}
disabled={ctrl.isSaving}
emptyValue=""
inputClassName="bg-background"
items={ctrl.paymentTermOptions}
label={t("settings.companyDetails.sections.paymentTerm.field.label")}
name="defaultPaymentTermId"
placeholder={t(
"settings.companyDetails.sections.paymentTerm.field.placeholder"
)}
/>
</RdxFormGrid>
</RdxPanel>
</form>
<p aria-live="polite" className="sr-only">
{ctrl.isSaving
? t("settings.companyDetails.feedback.saving")
: t("settings.companyDetails.feedback.idle")}
</p>
</div>
{ctrl.inlineLoadError ? (
<ErrorAlert
message={ctrl.inlineLoadError.description}
title={ctrl.inlineLoadError.title}
<RdxEditorActionBar
primary={
<RhfSubmitActionButton
busyLabel={t("settings.companyDetails.actions.saving")}
disabled={ctrl.isSaving || !ctrl.form.formState.isDirty}
formId={ctrl.formId}
isBusy={ctrl.form.formState.isSubmitting}
label={t("settings.companyDetails.actions.save")}
/>
}
secondary={
<CancelActionButton
disabled={ctrl.isSaving || !ctrl.form.formState.isDirty}
label={t("settings.companyDetails.actions.reset")}
onCancel={ctrl.resetForm}
variant="outline"
/>
}
/>
) : null}
<form
className="flex flex-col gap-6"
id={ctrl.formId}
noValidate
onKeyDown={ctrl.onFormKeyDown}
onSubmit={ctrl.onSubmit}
>
<RdxPanel
description={t("settings.companyDetails.sections.general.description")}
icon={<ScrollTextIcon className="size-5" />}
title={t("settings.companyDetails.sections.general.title")}
variant="card"
>
<RdxFormGrid className="md:grid-cols-2">
<TextField<CompanySettingsForm>
className="col-span-full"
description={t(
"settings.companyDetails.sections.general.fields.legalName.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.general.fields.legalName.label")}
name="legalName"
placeholder={t(
"settings.companyDetails.sections.general.fields.legalName.placeholder"
)}
required
/>
<TextField<CompanySettingsForm>
description={t(
"settings.companyDetails.sections.general.fields.tradeName.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.general.fields.tradeName.label")}
name="tradeName"
placeholder={t(
"settings.companyDetails.sections.general.fields.tradeName.placeholder"
)}
/>
<TextField<CompanySettingsForm>
description={t("settings.companyDetails.sections.general.fields.tin.description")}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.general.fields.tin.label")}
name="tin"
placeholder={t("settings.companyDetails.sections.general.fields.tin.placeholder")}
/>
</RdxFormGrid>
</RdxPanel>
<RdxPanel
description={t("settings.companyDetails.sections.contact.description")}
icon={<ContactIcon className="size-5" />}
title={t("settings.companyDetails.sections.contact.title")}
variant="card"
>
<RdxFormGrid className="md:grid-cols-2">
<TextField<CompanySettingsForm>
description={t(
"settings.companyDetails.sections.contact.fields.email.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.contact.fields.email.label")}
name="email"
placeholder={t(
"settings.companyDetails.sections.contact.fields.email.placeholder"
)}
typePreset="email"
/>
<TextField<CompanySettingsForm>
description={t(
"settings.companyDetails.sections.contact.fields.phone.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.contact.fields.phone.label")}
name="phone"
placeholder={t(
"settings.companyDetails.sections.contact.fields.phone.placeholder"
)}
typePreset="phone"
/>
<TextField<CompanySettingsForm>
className="col-span-full"
description={t(
"settings.companyDetails.sections.contact.fields.website.description"
)}
disabled={ctrl.isSaving}
inputClassName="bg-background"
label={t("settings.companyDetails.sections.contact.fields.website.label")}
name="website"
placeholder={t(
"settings.companyDetails.sections.contact.fields.website.placeholder"
)}
typePreset="url"
/>
</RdxFormGrid>
</RdxPanel>
<RdxPanel
description={t("settings.companyDetails.sections.paymentTerm.description")}
icon={<LandmarkIcon className="size-5" />}
title={t("settings.companyDetails.sections.paymentTerm.title")}
variant="card"
>
<RdxFormGrid className="md:grid-cols-2">
<SelectField<CompanySettingsForm>
className="col-span-full"
clearable
clearLabel={t("settings.companyDetails.sections.paymentTerm.field.none")}
description={t("settings.companyDetails.sections.paymentTerm.field.description")}
disabled={ctrl.isSaving}
emptyValue=""
inputClassName="bg-background"
items={ctrl.paymentTermOptions}
label={t("settings.companyDetails.sections.paymentTerm.field.label")}
name="defaultPaymentTermId"
placeholder={t("settings.companyDetails.sections.paymentTerm.field.placeholder")}
/>
</RdxFormGrid>
</RdxPanel>
</form>
<p aria-live="polite" className="sr-only">
{ctrl.isSaving
? t("settings.companyDetails.feedback.saving")
: t("settings.companyDetails.feedback.idle")}
</p>
<RdxEditorActionBar
primary={
<RhfSubmitActionButton
busyLabel={t("settings.companyDetails.actions.saving")}
disabled={ctrl.isSaving || !ctrl.form.formState.isDirty}
formId={ctrl.formId}
isBusy={ctrl.form.formState.isSubmitting}
label={t("settings.companyDetails.actions.save")}
/>
}
secondary={
<CancelActionButton
disabled={ctrl.isSaving || !ctrl.form.formState.isDirty}
label={t("settings.companyDetails.actions.reset")}
onCancel={ctrl.resetForm}
variant="outline"
/>
}
/>
</div>
</CardContent>
</Card>
</UnsavedChangesProvider>
</FormProvider>
);

View File

@ -6,6 +6,8 @@ import {
RdxEditorActionBar,
RhfSubmitActionButton,
} from "@repo/rdx-ui/components";
import { Card, CardContent, CardHeader } from "@repo/shadcn-ui/components";
import { ShieldCheckIcon } from "lucide-react";
import { FormProvider } from "react-hook-form";
import { useTranslation } from "../../../../i18n";
@ -51,80 +53,88 @@ export const CompanyVerifactuSettingsPage = () => {
return (
<FormProvider {...profileCtrl.form}>
<UnsavedChangesProvider isDirty={profileCtrl.form.formState.isDirty}>
<div className="flex min-h-[32rem] flex-col gap-6">
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">
{t("settings.verifactu.title")}
</h2>
<p className="max-w-3xl text-sm text-muted-foreground">
{t("settings.verifactu.description")}
<Card>
<CardHeader>
<div className="flex min-w-0 items-start gap-4">
<div className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<ShieldCheckIcon aria-hidden className="size-6" />
</div>
<div className="min-w-0 space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">
{t("settings.verifactu.title")}
</h2>
<p className="text-sm text-muted-foreground">
{t("settings.verifactu.description")}
</p>
{ctrl.mode === "initial" ? (
<p className="text-sm text-muted-foreground">
{t("settings.verifactu.initial_mode_hint")}
</p>
) : null}
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{profileCtrl.isUpdateError ? (
<ErrorAlert
message={
(profileCtrl.updateError as Error)?.message ??
t("settings.verifactu.feedback.error.message")
}
title={t("settings.verifactu.feedback.error.title")}
/>
) : null}
<form
className="flex flex-col gap-6"
id={profileCtrl.formId}
noValidate
onSubmit={profileCtrl.onSubmit}
>
<CompanyVerifactuStatusSection />
{profileCtrl.showExemptionFields || profileCtrl.showVerifactuFields ? (
<CompanyVerifactuConfigurationSection
disabled={profileCtrl.isUpdating}
showExemptionFields={profileCtrl.showExemptionFields}
showVerifactuFields={profileCtrl.showVerifactuFields}
/>
) : null}
<CompanyVerifactuDefaultsSection
disabled={profileCtrl.isUpdating || profileCtrl.isNotConfigured}
isTaxRegimesLoading={profileCtrl.isTaxRegimesLoading}
taxRegimeOptions={profileCtrl.taxRegimeOptions}
/>
</form>
<p aria-live="polite" className="sr-only">
{profileCtrl.isUpdating
? t("settings.verifactu.feedback.saving")
: t("settings.verifactu.feedback.idle")}
</p>
{ctrl.mode === "initial" ? (
<p className="text-sm text-muted-foreground">
{t("settings.verifactu.initial_mode_hint")}
</p>
) : null}
</div>
{profileCtrl.isUpdateError ? (
<ErrorAlert
message={
(profileCtrl.updateError as Error)?.message ??
t("settings.verifactu.feedback.error.message")
<RdxEditorActionBar
primary={
<RhfSubmitActionButton
busyLabel={t("settings.verifactu.actions.saving")}
disabled={profileCtrl.isUpdating}
formId={profileCtrl.formId}
isBusy={profileCtrl.form.formState.isSubmitting}
label={t("settings.verifactu.actions.save")}
/>
}
secondary={
<CancelActionButton
disabled={profileCtrl.isUpdating || !profileCtrl.form.formState.isDirty}
label={t("settings.verifactu.actions.reset")}
onCancel={profileCtrl.resetForm}
variant="outline"
/>
}
title={t("settings.verifactu.feedback.error.title")}
/>
) : null}
<form
className="flex flex-col gap-6"
id={profileCtrl.formId}
noValidate
onSubmit={profileCtrl.onSubmit}
>
<CompanyVerifactuStatusSection />
{profileCtrl.showExemptionFields || profileCtrl.showVerifactuFields ? (
<CompanyVerifactuConfigurationSection
disabled={profileCtrl.isUpdating}
showExemptionFields={profileCtrl.showExemptionFields}
showVerifactuFields={profileCtrl.showVerifactuFields}
/>
) : null}
<CompanyVerifactuDefaultsSection
disabled={profileCtrl.isUpdating || profileCtrl.isNotConfigured}
isTaxRegimesLoading={profileCtrl.isTaxRegimesLoading}
taxRegimeOptions={profileCtrl.taxRegimeOptions}
/>
</form>
<p aria-live="polite" className="sr-only">
{profileCtrl.isUpdating
? t("settings.verifactu.feedback.saving")
: t("settings.verifactu.feedback.idle")}
</p>
<RdxEditorActionBar
primary={
<RhfSubmitActionButton
busyLabel={t("settings.verifactu.actions.saving")}
disabled={profileCtrl.isUpdating}
formId={profileCtrl.formId}
isBusy={profileCtrl.form.formState.isSubmitting}
label={t("settings.verifactu.actions.save")}
/>
}
secondary={
<CancelActionButton
disabled={profileCtrl.isUpdating || !profileCtrl.form.formState.isDirty}
label={t("settings.verifactu.actions.reset")}
onCancel={profileCtrl.resetForm}
variant="outline"
/>
}
/>
</div>
</CardContent>
</Card>
</UnsavedChangesProvider>
</FormProvider>
);

View File

@ -32,7 +32,7 @@ export const CompanyVerifactuConfigurationSection = ({
description={t("settings.verifactu.configuration.description")}
icon={<ShieldCheckIcon className="size-5" />}
title={t("settings.verifactu.configuration.title")}
variant="card"
variant="plain"
>
<RdxFormGrid className="md:grid-cols-2">
{showExemptionFields ? (

View File

@ -21,7 +21,7 @@ export const CompanyVerifactuDefaultsSection = ({
description={t("settings.verifactu.defaults.description")}
icon={<LandmarkIcon className="size-5" />}
title={t("settings.verifactu.defaults.title")}
variant="card"
variant="plain"
>
<RdxFormGrid className="md:grid-cols-2">
<SelectField

View File

@ -23,7 +23,7 @@ export const CompanyVerifactuStatusSection = () => {
description={t("settings.verifactu.status.description")}
icon={<FileCogIcon className="size-5" />}
title={t("settings.verifactu.status.title")}
variant="card"
variant="plain"
>
<Controller
control={control}