feat: refactor badge component and update related usages
- Replaced instances of Badge with RdxBadge in SupplierStatusBadge, DataTableFacetedFilter, MultiSelectField, and other components. - Updated RdxEditorPageLayout to use width prop instead of complexity. - Added new recurring proforma operational summary feature with tests. - Introduced recurring proforma status entity and related UI helpers. - Enhanced recurring proforma list and detail views with new components and hooks. - Added tests for new functionality and components.
This commit is contained in:
parent
e670ff4b33
commit
d92ab05f35
@ -1,11 +1,11 @@
|
||||
import { useTheme } from "@/lib/hooks";
|
||||
import {
|
||||
RdxBadge,
|
||||
RdxContextPanel,
|
||||
RdxEditorPageLayout,
|
||||
RdxEditorSection,
|
||||
RdxEditorStickyFooter,
|
||||
RdxFormGrid,
|
||||
RdxPageActions,
|
||||
RdxContextPanel,
|
||||
RdxPageContainer,
|
||||
RdxPageHeader,
|
||||
RdxPageStack,
|
||||
@ -13,7 +13,6 @@ import {
|
||||
RightPanel,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
@ -34,6 +33,8 @@ import {
|
||||
import { ArrowLeftIcon, MoreHorizontalIcon, PlusIcon, XIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { useTheme } from "@/lib/hooks";
|
||||
|
||||
import { UiKitSection } from "./sections/ui-kit-section";
|
||||
|
||||
type ThemeValue = "dark" | "light" | "system";
|
||||
@ -60,11 +61,7 @@ const ExampleCard = ({ title, notes, children }: ExampleCardProps) => {
|
||||
};
|
||||
|
||||
const SurfaceExample = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-background p-4">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className="rounded-lg border border-border bg-background p-4">{children}</div>;
|
||||
};
|
||||
|
||||
const OverflowMenuExample = () => {
|
||||
@ -94,27 +91,34 @@ export default function UiKitPage() {
|
||||
<RdxPageHeader
|
||||
actions={
|
||||
<RdxPageActions
|
||||
overflow={<OverflowMenuExample />}
|
||||
primary={
|
||||
<Button type="button">
|
||||
<PlusIcon className="size-4" />
|
||||
Primary action
|
||||
</Button>
|
||||
}
|
||||
secondary={<Button type="button" variant="outline">Secondary</Button>}
|
||||
overflow={<OverflowMenuExample />}
|
||||
secondary={
|
||||
<Button type="button" variant="outline">
|
||||
Secondary
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
description="Infraestructura mínima del sistema visual Rdx para validar foundations estructurales antes de migrar pantallas funcionales."
|
||||
metadata={
|
||||
<>
|
||||
<Badge variant="outline">Development only</Badge>
|
||||
<Badge variant="outline">Static data</Badge>
|
||||
<RdxBadge variant="outline">Development only</RdxBadge>
|
||||
<RdxBadge variant="outline">Static data</RdxBadge>
|
||||
</>
|
||||
}
|
||||
title="Rdx UI Kit"
|
||||
/>
|
||||
|
||||
<nav aria-label="UI Kit sections" className="rounded-lg border border-border bg-background p-4">
|
||||
<nav
|
||||
aria-label="UI Kit sections"
|
||||
className="rounded-lg border border-border bg-background p-4"
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<a
|
||||
className="inline-flex h-8 items-center rounded-md px-2.5 text-sm font-medium hover:bg-muted"
|
||||
@ -242,19 +246,28 @@ export default function UiKitPage() {
|
||||
>
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
<ExampleCard notes="`width="full"`" title="Full">
|
||||
<RdxPageContainer className="rounded-lg border border-dashed border-border bg-muted/40" width="full">
|
||||
<RdxPageContainer
|
||||
className="rounded-lg border border-dashed border-border bg-muted/40"
|
||||
width="full"
|
||||
>
|
||||
<div className="text-sm text-foreground">Available width</div>
|
||||
</RdxPageContainer>
|
||||
</ExampleCard>
|
||||
|
||||
<ExampleCard notes="`width="wide"`" title="Wide">
|
||||
<RdxPageContainer className="rounded-lg border border-dashed border-border bg-muted/40" width="wide">
|
||||
<RdxPageContainer
|
||||
className="rounded-lg border border-dashed border-border bg-muted/40"
|
||||
width="wide"
|
||||
>
|
||||
<div className="text-sm text-foreground">Max width screen 2xl</div>
|
||||
</RdxPageContainer>
|
||||
</ExampleCard>
|
||||
|
||||
<ExampleCard notes="`width="content"`" title="Content">
|
||||
<RdxPageContainer className="rounded-lg border border-dashed border-border bg-muted/40" width="content">
|
||||
<RdxPageContainer
|
||||
className="rounded-lg border border-dashed border-border bg-muted/40"
|
||||
width="content"
|
||||
>
|
||||
<div className="text-sm text-foreground">Centered content editor</div>
|
||||
</RdxPageContainer>
|
||||
</ExampleCard>
|
||||
@ -270,7 +283,10 @@ export default function UiKitPage() {
|
||||
>
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
<ExampleCard notes="`spacing="compact"`" title="Compact">
|
||||
<RdxPageStack className="rounded-lg border border-border bg-background p-4" spacing="compact">
|
||||
<RdxPageStack
|
||||
className="rounded-lg border border-border bg-background p-4"
|
||||
spacing="compact"
|
||||
>
|
||||
<SurfaceExample>Block 1</SurfaceExample>
|
||||
<SurfaceExample>Block 2</SurfaceExample>
|
||||
<SurfaceExample>Block 3</SurfaceExample>
|
||||
@ -278,7 +294,10 @@ export default function UiKitPage() {
|
||||
</ExampleCard>
|
||||
|
||||
<ExampleCard notes="`spacing="default"`" title="Default">
|
||||
<RdxPageStack className="rounded-lg border border-border bg-background p-4" spacing="default">
|
||||
<RdxPageStack
|
||||
className="rounded-lg border border-border bg-background p-4"
|
||||
spacing="default"
|
||||
>
|
||||
<SurfaceExample>Block 1</SurfaceExample>
|
||||
<SurfaceExample>Block 2</SurfaceExample>
|
||||
<SurfaceExample>Block 3</SurfaceExample>
|
||||
@ -286,7 +305,10 @@ export default function UiKitPage() {
|
||||
</ExampleCard>
|
||||
|
||||
<ExampleCard notes="`spacing="relaxed"`" title="Relaxed">
|
||||
<RdxPageStack className="rounded-lg border border-border bg-background p-4" spacing="relaxed">
|
||||
<RdxPageStack
|
||||
className="rounded-lg border border-border bg-background p-4"
|
||||
spacing="relaxed"
|
||||
>
|
||||
<SurfaceExample>Block 1</SurfaceExample>
|
||||
<SurfaceExample>Block 2</SurfaceExample>
|
||||
<SurfaceExample>Block 3</SurfaceExample>
|
||||
@ -323,9 +345,13 @@ export default function UiKitPage() {
|
||||
<RdxPageHeader
|
||||
actions={
|
||||
<RdxPageActions
|
||||
primary={<Button type="button">Save</Button>}
|
||||
secondary={<Button type="button" variant="outline">Cancel</Button>}
|
||||
overflow={<OverflowMenuExample />}
|
||||
primary={<Button type="button">Save</Button>}
|
||||
secondary={
|
||||
<Button type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
description="Header with leading control, metadata and grouped actions."
|
||||
@ -336,7 +362,7 @@ export default function UiKitPage() {
|
||||
}
|
||||
metadata={
|
||||
<>
|
||||
<Badge variant="outline">Draft</Badge>
|
||||
<RdxBadge variant="outline">Draft</RdxBadge>
|
||||
<span>Last updated 5 minutes ago</span>
|
||||
</>
|
||||
}
|
||||
@ -376,7 +402,11 @@ export default function UiKitPage() {
|
||||
<SurfaceExample>
|
||||
<RdxPageActions
|
||||
primary={<Button type="button">Save</Button>}
|
||||
secondary={<Button type="button" variant="outline">Cancel</Button>}
|
||||
secondary={
|
||||
<Button type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SurfaceExample>
|
||||
</ExampleCard>
|
||||
@ -386,7 +416,11 @@ export default function UiKitPage() {
|
||||
<RdxPageActions
|
||||
overflow={<OverflowMenuExample />}
|
||||
primary={<Button type="button">Approve</Button>}
|
||||
secondary={<Button type="button" variant="outline">Back to list</Button>}
|
||||
secondary={
|
||||
<Button type="button" variant="outline">
|
||||
Back to list
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SurfaceExample>
|
||||
</ExampleCard>
|
||||
@ -398,8 +432,12 @@ export default function UiKitPage() {
|
||||
primary={<Button type="button">Primary action with long label</Button>}
|
||||
secondary={
|
||||
<>
|
||||
<Button type="button" variant="outline">Secondary one</Button>
|
||||
<Button type="button" variant="outline">Secondary two</Button>
|
||||
<Button type="button" variant="outline">
|
||||
Secondary one
|
||||
</Button>
|
||||
<Button type="button" variant="outline">
|
||||
Secondary two
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -495,12 +533,19 @@ export default function UiKitPage() {
|
||||
<div className="rounded-lg border border-border bg-background p-4 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-3 border-b border-border pb-3">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-semibold text-foreground">Ficha de cliente</div>
|
||||
<div className="text-sm font-semibold text-foreground">
|
||||
Ficha de cliente
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Panel contextual opcional
|
||||
</div>
|
||||
</div>
|
||||
<Button aria-label="Cerrar panel" size="icon-sm" type="button" variant="ghost">
|
||||
<Button
|
||||
aria-label="Cerrar panel"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@ -819,7 +864,7 @@ export default function UiKitPage() {
|
||||
Sticky header remains outside `rdx-ui`.
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline">Modified</Badge>
|
||||
<RdxBadge variant="outline">Modified</RdxBadge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -897,7 +942,11 @@ export default function UiKitPage() {
|
||||
</div>
|
||||
}
|
||||
primary={<Button type="button">Guardar</Button>}
|
||||
secondary={<Button type="button" variant="outline">Cancelar</Button>}
|
||||
secondary={
|
||||
<Button type="button" variant="outline">
|
||||
Cancelar
|
||||
</Button>
|
||||
}
|
||||
status="Modificada"
|
||||
/>
|
||||
</div>
|
||||
@ -935,10 +984,7 @@ export default function UiKitPage() {
|
||||
<div className="rounded-lg border border-border bg-muted/30">
|
||||
<RdxPageContainer width="content">
|
||||
<RdxPageStack>
|
||||
<RdxPageHeader
|
||||
description="Simple centered editor"
|
||||
title="Editor page"
|
||||
/>
|
||||
<RdxPageHeader description="Simple centered editor" title="Editor page" />
|
||||
<SurfaceExample>Form content</SurfaceExample>
|
||||
</RdxPageStack>
|
||||
</RdxPageContainer>
|
||||
@ -947,10 +993,7 @@ export default function UiKitPage() {
|
||||
<div className="rounded-lg border border-border bg-muted/30">
|
||||
<RdxPageContainer width="wide">
|
||||
<RdxPageStack>
|
||||
<RdxPageHeader
|
||||
description="Wide detail composition"
|
||||
title="Detail page"
|
||||
/>
|
||||
<RdxPageHeader description="Wide detail composition" title="Detail page" />
|
||||
<SurfaceExample>Detail content</SurfaceExample>
|
||||
</RdxPageStack>
|
||||
</RdxPageContainer>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@ -8,7 +9,6 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
@ -218,9 +218,9 @@ export default function ShadcnShowcasePage() {
|
||||
</FieldGroup>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Badge>Badge</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<RdxBadge>Badge</RdxBadge>
|
||||
<RdxBadge variant="secondary">Secondary</RdxBadge>
|
||||
<RdxBadge variant="outline">Outline</RdxBadge>
|
||||
</div>
|
||||
<RadioGroup className="ml-auto flex w-fit gap-3" defaultValue="apple">
|
||||
<RadioGroupItem value="apple" />
|
||||
|
||||
BIN
docs/recurring-proformas/ChatGPT Image 2 sept 2026, 12_27_14.png
Normal file
BIN
docs/recurring-proformas/ChatGPT Image 2 sept 2026, 12_27_14.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/recurring-proformas/recurring-proformas-view-mockup.png
Normal file
BIN
docs/recurring-proformas/recurring-proformas-view-mockup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@ -1,6 +1,6 @@
|
||||
import { PageErrorState } from "@erp/core/components";
|
||||
import { RdxPageContainer } from "@repo/rdx-ui/components";
|
||||
import { Badge, Button, Card, CardContent, RadioGroup, RadioGroupItem } from "@repo/shadcn-ui/components";
|
||||
import { RdxBadge, RdxPageContainer } from "@repo/rdx-ui/components";
|
||||
import { Button, Card, CardContent, RadioGroup, RadioGroupItem } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -129,15 +129,15 @@ export const PaymentMethodsSettingsPage = () => {
|
||||
</label>
|
||||
|
||||
{paymentMethod.isSystem ? (
|
||||
<Badge variant="secondary">
|
||||
<RdxBadge variant="secondary">
|
||||
{t("settings.paymentMethods.status.system")}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
) : null}
|
||||
|
||||
{paymentMethod.isDefault ? (
|
||||
<Badge variant="outline">
|
||||
<RdxBadge variant="outline">
|
||||
{t("settings.paymentMethods.status.default")}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@ -204,7 +204,9 @@ export const PaymentMethodsSettingsPage = () => {
|
||||
isEditMode
|
||||
isSubmitting={featureCtrl.isSaving || featureCtrl.isEditingPaymentMethodLoading}
|
||||
onSubmit={featureCtrl.onSubmit}
|
||||
open={Boolean(featureCtrl.editingPaymentMethod) || featureCtrl.isEditingPaymentMethodLoading}
|
||||
open={
|
||||
Boolean(featureCtrl.editingPaymentMethod) || featureCtrl.isEditingPaymentMethodLoading
|
||||
}
|
||||
openChange={(open) => {
|
||||
if (!open) {
|
||||
featureCtrl.closeEditDialog();
|
||||
|
||||
@ -1,16 +1,11 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import type { FormEventHandler } from "react";
|
||||
import { FormProvider, useFieldArray, useWatch } from "react-hook-form";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import {
|
||||
RdxBadge,
|
||||
RdxFormGrid,
|
||||
SwitchField,
|
||||
TextAreaField,
|
||||
TextField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -21,6 +16,10 @@ import {
|
||||
Spinner,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon, Trash2Icon } from "lucide-react";
|
||||
import type { FormEventHandler } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormProvider, useFieldArray, useWatch } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { PaymentTermForm } from "../../entities";
|
||||
@ -74,10 +73,12 @@ export const PaymentTermFormDialog = ({
|
||||
});
|
||||
const dueRules = useMemo(() => {
|
||||
return fields.map((field, index) => {
|
||||
return watchedDueRules?.[index] ?? {
|
||||
dueDays: field.dueDays ?? "",
|
||||
percentage: field.percentage ?? "",
|
||||
};
|
||||
return (
|
||||
watchedDueRules?.[index] ?? {
|
||||
dueDays: field.dueDays ?? "",
|
||||
percentage: field.percentage ?? "",
|
||||
}
|
||||
);
|
||||
});
|
||||
}, [fields, watchedDueRules]);
|
||||
|
||||
@ -140,13 +141,15 @@ export const PaymentTermFormDialog = ({
|
||||
<section className="space-y-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold">{t("settings.paymentTerms.form.rules.title")}</h3>
|
||||
<h3 className="text-sm font-semibold">
|
||||
{t("settings.paymentTerms.form.rules.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("settings.paymentTerms.form.rules.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className={`border px-3 py-1 text-xs ${totalToneClassNames[totalStatus.tone]}`}
|
||||
variant="outline"
|
||||
>
|
||||
@ -156,7 +159,7 @@ export const PaymentTermFormDialog = ({
|
||||
i18n.language
|
||||
),
|
||||
})}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -173,10 +176,7 @@ export const PaymentTermFormDialog = ({
|
||||
|
||||
<div className="space-y-2">
|
||||
{fields.map((field, index) => (
|
||||
<div
|
||||
className="rounded-xl border border-border bg-muted/20 p-3"
|
||||
key={field.id}
|
||||
>
|
||||
<div className="rounded-xl border border-border bg-muted/20 p-3" key={field.id}>
|
||||
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] md:items-start">
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-medium text-muted-foreground md:hidden">
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Badge, Card, CardContent } from "@repo/shadcn-ui/components";
|
||||
import { Card, CardContent, RdxBadge } from "@repo/shadcn-ui/components";
|
||||
import { CalendarClockIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -61,14 +61,16 @@ export const PaymentTermListItem = ({
|
||||
{paymentTerm.name}
|
||||
</h3>
|
||||
|
||||
<Badge variant={paymentTerm.isActive ? "outline" : "secondary"}>
|
||||
<RdxBadge variant={paymentTerm.isActive ? "outline" : "secondary"}>
|
||||
{paymentTerm.isActive
|
||||
? t("settings.paymentTerms.status.active")
|
||||
: t("settings.paymentTerms.status.inactive")}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
|
||||
{paymentTerm.isSystem ? (
|
||||
<Badge variant="secondary">{t("settings.paymentTerms.status.system")}</Badge>
|
||||
<RdxBadge variant="secondary">
|
||||
{t("settings.paymentTerms.status.system")}
|
||||
</RdxBadge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PageErrorState } from "@erp/core/components";
|
||||
import { RdxPageContainer } from "@repo/rdx-ui/components";
|
||||
import { Badge, Button, Card, CardContent } from "@repo/shadcn-ui/components";
|
||||
import { Button, Card, CardContent, RdxBadge } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -95,14 +95,16 @@ export const TaxRegimesSettingsPage = () => {
|
||||
</span>
|
||||
|
||||
{taxRegime.isSystem ? (
|
||||
<Badge variant="secondary">{t("settings.taxRegimes.status.system")}</Badge>
|
||||
<RdxBadge variant="secondary">
|
||||
{t("settings.taxRegimes.status.system")}
|
||||
</RdxBadge>
|
||||
) : null}
|
||||
|
||||
<Badge variant={taxRegime.isActive ? "outline" : "secondary"}>
|
||||
<RdxBadge variant={taxRegime.isActive ? "outline" : "secondary"}>
|
||||
{taxRegime.isActive
|
||||
? t("settings.taxRegimes.status.active")
|
||||
: t("settings.taxRegimes.status.inactive")}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// packages/rdx-ui/src/components/form-actions/erp-form-header.tsx
|
||||
import { Badge, Button } from "@repo/shadcn-ui/components";
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
@ -62,14 +62,14 @@ export const PageFormHeader = ({
|
||||
</h1>
|
||||
|
||||
{statusLabel ? (
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className={cn(
|
||||
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
|
||||
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
|
||||
)}
|
||||
>
|
||||
{statusLabel}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -50,6 +50,7 @@ export type RecurringProformaDetailReadModel = {
|
||||
name: string;
|
||||
status: RecurringProformaStatus;
|
||||
customerId: UniqueID | null;
|
||||
customer: CustomerPublicSummary | null;
|
||||
description: string | null;
|
||||
paymentTermId: UniqueID | null;
|
||||
paymentMethodId: UniqueID | null;
|
||||
@ -60,6 +61,7 @@ export type RecurringProformaDetailReadModel = {
|
||||
globalDiscountPercentage: DiscountPercentage | null;
|
||||
schedule: RecurringProformaScheduleReadModel | null;
|
||||
nextOccurrenceDate: Date | null;
|
||||
hasIssue: boolean;
|
||||
items: readonly RecurringProformaItemReadModel[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
RecurringProformaOccurrence,
|
||||
} from "../../../domain";
|
||||
import { toRecurringProformaAuditTarget } from "../audit";
|
||||
|
||||
import {
|
||||
RecurringProformaCommands,
|
||||
RecurringProformaReader,
|
||||
@ -146,6 +147,7 @@ test("RecurringProformaReader propaga company, filtros, paginación y conserva e
|
||||
|
||||
test("RecurringProformaReader conserva overrides nulos, schedule e items ordenados en detail", async () => {
|
||||
let receivedCompany: UniqueID | undefined;
|
||||
let receivedOccurrenceTransaction: unknown;
|
||||
const recurring = buildRecurring();
|
||||
const reader = new RecurringProformaReader(
|
||||
{
|
||||
@ -156,13 +158,25 @@ test("RecurringProformaReader conserva overrides nulos, schedule e items ordenad
|
||||
return Result.ok(recurring);
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
{
|
||||
hasFailedOccurrences: async (
|
||||
id: UniqueID,
|
||||
companyId: UniqueID,
|
||||
currentTransaction: unknown
|
||||
) => {
|
||||
assert.equal(id.equals(recurringId), true);
|
||||
assert.equal(companyId.equals(companyA), true);
|
||||
receivedOccurrenceTransaction = currentTransaction;
|
||||
return Result.ok(true);
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
transactionManager()
|
||||
);
|
||||
const result = await reader.detail({ companyId: companyA, id: recurringId });
|
||||
assert.equal(result.isSuccess, true);
|
||||
assert.equal(receivedCompany?.equals(companyA), true);
|
||||
assert.equal(receivedOccurrenceTransaction, transaction);
|
||||
assert.equal(result.data.customerId, null);
|
||||
assert.equal(result.data.paymentTermId, null);
|
||||
assert.equal(result.data.language, null);
|
||||
@ -172,6 +186,7 @@ test("RecurringProformaReader conserva overrides nulos, schedule e items ordenad
|
||||
assert.equal(result.data.items[0]?.ivaCode, "general");
|
||||
assert.equal(result.data.items[0]?.recCode, "rec");
|
||||
assert.equal(result.data.items[0]?.retentionCode, "ret");
|
||||
assert.equal(result.data.hasIssue, true);
|
||||
});
|
||||
|
||||
test("RecurringProformaReader propaga parent, empresa y paginación de occurrences", async () => {
|
||||
|
||||
@ -81,7 +81,28 @@ export class RecurringProformaReader {
|
||||
params.id,
|
||||
transaction
|
||||
);
|
||||
return found.isFailure ? Result.fail(found.error) : Result.ok(this.toDetail(found.data));
|
||||
if (found.isFailure) return Result.fail(found.error);
|
||||
|
||||
const hasIssue = await this.occurrenceRepository.hasFailedOccurrences(
|
||||
params.id,
|
||||
params.companyId,
|
||||
transaction
|
||||
);
|
||||
if (hasIssue.isFailure) return Result.fail(hasIssue.error);
|
||||
|
||||
const customerId = found.data.customerId.match(
|
||||
(value) => value,
|
||||
() => null
|
||||
);
|
||||
if (!customerId) return Result.ok(this.toDetail(found.data, hasIssue.data, null));
|
||||
|
||||
const customers = await this.customerServices.findSummariesByIds([customerId], {
|
||||
companyId: params.companyId,
|
||||
transaction,
|
||||
});
|
||||
if (customers.isFailure) return Result.fail(customers.error);
|
||||
|
||||
return Result.ok(this.toDetail(found.data, hasIssue.data, customers.data[0] ?? null));
|
||||
});
|
||||
}
|
||||
|
||||
@ -93,7 +114,11 @@ export class RecurringProformaReader {
|
||||
);
|
||||
}
|
||||
|
||||
private toDetail(source: RecurringProforma): RecurringProformaDetailReadModel {
|
||||
private toDetail(
|
||||
source: RecurringProforma,
|
||||
hasIssue: boolean,
|
||||
customer: RecurringProformaDetailReadModel["customer"]
|
||||
): RecurringProformaDetailReadModel {
|
||||
const schedule = source.schedule.map<RecurringProformaScheduleReadModel>((value) => ({
|
||||
startDate: value.startDate,
|
||||
endDate: value.endDate,
|
||||
@ -110,6 +135,7 @@ export class RecurringProformaReader {
|
||||
(value) => value,
|
||||
() => null
|
||||
),
|
||||
customer,
|
||||
description: source.description.match(
|
||||
(value) => value,
|
||||
() => null
|
||||
@ -150,6 +176,7 @@ export class RecurringProformaReader {
|
||||
(value) => value,
|
||||
() => null
|
||||
),
|
||||
hasIssue,
|
||||
items: source.items.getAll().map((item, index) => ({
|
||||
id: item.id,
|
||||
position: index + 1,
|
||||
|
||||
@ -44,6 +44,13 @@ export class RecurringProformaDetailPresenter {
|
||||
name: item.name,
|
||||
status: item.status,
|
||||
customer_id: id(item.customerId),
|
||||
customer: item.customer
|
||||
? {
|
||||
id: id(item.customer.id),
|
||||
name: item.customer.name,
|
||||
tin: item.customer.tin,
|
||||
}
|
||||
: null,
|
||||
description: item.description,
|
||||
payment_term_id: id(item.paymentTermId),
|
||||
payment_method_id: id(item.paymentMethodId),
|
||||
@ -62,6 +69,7 @@ export class RecurringProformaDetailPresenter {
|
||||
}
|
||||
: null,
|
||||
next_occurrence_date: dateOnly(item.nextOccurrenceDate),
|
||||
has_issue: item.hasIssue,
|
||||
items: item.items.map((recurringItem: any) => ({
|
||||
...recurringItem,
|
||||
id: id(recurringItem.id),
|
||||
|
||||
@ -10,11 +10,13 @@ import { RecurringProformaDetailPresenter } from "./index";
|
||||
|
||||
test("RecurringProformaDetailPresenter conserva PercentageSchema como strings value y scale", () => {
|
||||
const discount = DiscountPercentage.create({ value: 500 }).data;
|
||||
const customerId = UniqueID.generateNewID();
|
||||
const dto = new RecurringProformaDetailPresenter().present({
|
||||
id: UniqueID.generateNewID(),
|
||||
name: "Cuota",
|
||||
status: "draft",
|
||||
customerId: null,
|
||||
customerId,
|
||||
customer: { id: customerId, name: "ACME Ingeniería", tin: "B12345678" },
|
||||
description: null,
|
||||
paymentTermId: null,
|
||||
paymentMethodId: null,
|
||||
@ -25,6 +27,7 @@ test("RecurringProformaDetailPresenter conserva PercentageSchema como strings va
|
||||
globalDiscountPercentage: discount,
|
||||
schedule: null,
|
||||
nextOccurrenceDate: null,
|
||||
hasIssue: false,
|
||||
items: [],
|
||||
createdAt: new Date("2027-01-01T10:00:00.000Z"),
|
||||
updatedAt: new Date("2027-01-01T10:00:00.000Z"),
|
||||
@ -32,6 +35,12 @@ test("RecurringProformaDetailPresenter conserva PercentageSchema como strings va
|
||||
assert.deepEqual(dto.global_discount_percentage, { value: "500", scale: "2" });
|
||||
assert.equal(typeof dto.global_discount_percentage.value, "string");
|
||||
assert.equal(typeof dto.global_discount_percentage.scale, "string");
|
||||
assert.equal(dto.has_issue, false);
|
||||
assert.deepEqual(dto.customer, {
|
||||
id: customerId.toPrimitive(),
|
||||
name: "ACME Ingeniería",
|
||||
tin: "B12345678",
|
||||
});
|
||||
});
|
||||
|
||||
test("RecurringProformaDetailPresenter serializa Value Objects antes de responder HTTP", () => {
|
||||
@ -45,6 +54,7 @@ test("RecurringProformaDetailPresenter serializa Value Objects antes de responde
|
||||
name: "Cuota",
|
||||
status: "draft",
|
||||
customerId: null,
|
||||
customer: null,
|
||||
description: null,
|
||||
paymentTermId: null,
|
||||
paymentMethodId: null,
|
||||
@ -55,6 +65,7 @@ test("RecurringProformaDetailPresenter serializa Value Objects antes de responde
|
||||
globalDiscountPercentage: null,
|
||||
schedule: null,
|
||||
nextOccurrenceDate: null,
|
||||
hasIssue: true,
|
||||
items: [{ id: UniqueID.generateNewID(), position: 1, description: itemDescription }],
|
||||
createdAt: new Date("2027-01-01T10:00:00.000Z"),
|
||||
updatedAt: new Date("2027-01-01T10:00:00.000Z"),
|
||||
@ -65,4 +76,5 @@ test("RecurringProformaDetailPresenter serializa Value Objects antes de responde
|
||||
assert.equal(dto.proforma_series, "PRO");
|
||||
assert.equal(dto.issued_invoice_series, "FAC");
|
||||
assert.equal(dto.items[0]?.description, "Servicio mensual");
|
||||
assert.equal(dto.has_issue, true);
|
||||
});
|
||||
|
||||
@ -107,6 +107,18 @@
|
||||
"created_at": "Created",
|
||||
"updated_at": "Updated",
|
||||
"metadata": "Created on {{createdAt}} · Last updated {{updatedAt}}",
|
||||
"operational_summary": {
|
||||
"customer": "Customer",
|
||||
"status": "Status",
|
||||
"frequency": "Frequency",
|
||||
"issues": "Issues",
|
||||
"next_generation": {
|
||||
"label": "Next generation",
|
||||
"today": "Today",
|
||||
"in_days_one": "In {{count}} day",
|
||||
"in_days_other": "In {{count}} days"
|
||||
}
|
||||
},
|
||||
"inherited": "Inherited",
|
||||
"general": {
|
||||
"title": "General information"
|
||||
|
||||
@ -107,6 +107,18 @@
|
||||
"created_at": "Creada",
|
||||
"updated_at": "Actualizada",
|
||||
"metadata": "Creada el {{createdAt}} · Última actualización {{updatedAt}}",
|
||||
"operational_summary": {
|
||||
"customer": "Cliente",
|
||||
"status": "Estado",
|
||||
"frequency": "Frecuencia",
|
||||
"issues": "Incidencias",
|
||||
"next_generation": {
|
||||
"label": "Próxima generación",
|
||||
"today": "Hoy",
|
||||
"in_days_one": "En {{count}} día",
|
||||
"in_days_other": "En {{count}} días"
|
||||
}
|
||||
},
|
||||
"inherited": "Heredado",
|
||||
"general": {
|
||||
"title": "Informacion general"
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { IssuedInvoiceDetailCommercialViewModel } from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
@ -8,9 +6,7 @@ type IssuedInvoicePaymentSectionProps = {
|
||||
commercial: IssuedInvoiceDetailCommercialViewModel;
|
||||
};
|
||||
|
||||
export const IssuedInvoicePaymentSection = ({
|
||||
commercial,
|
||||
}: IssuedInvoicePaymentSectionProps) => {
|
||||
export const IssuedInvoicePaymentSection = ({ commercial }: IssuedInvoicePaymentSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -21,7 +17,9 @@ export const IssuedInvoicePaymentSection = ({
|
||||
<div className="space-y-3">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.payment_method")}
|
||||
value={commercial.paymentMethodName ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
value={
|
||||
commercial.paymentMethodName ?? t("pages.issued_invoices.detail.empty.not_available")
|
||||
}
|
||||
/>
|
||||
{commercial.paymentMethodDescription ? (
|
||||
<IssuedInvoiceDetailField
|
||||
@ -32,7 +30,9 @@ export const IssuedInvoicePaymentSection = ({
|
||||
) : null}
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.payment_term")}
|
||||
value={commercial.paymentTermName ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
value={
|
||||
commercial.paymentTermName ?? t("pages.issued_invoices.detail.empty.not_available")
|
||||
}
|
||||
/>
|
||||
{commercial.paymentTermDescription ? (
|
||||
<IssuedInvoiceDetailField
|
||||
@ -47,9 +47,9 @@ export const IssuedInvoicePaymentSection = ({
|
||||
commercial.paymentTermDueRules.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2 sm:justify-end">
|
||||
{commercial.paymentTermDueRules.map((rule) => (
|
||||
<Badge className="text-xs" key={rule.key} variant="secondary">
|
||||
<RdxBadge className="text-xs" key={rule.key} variant="secondary">
|
||||
{rule.percentageLabel} · {rule.dueDaysLabel}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
import {
|
||||
DataTable,
|
||||
type DataTableSort,
|
||||
SkeletonDataTable,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { DataTable, type DataTableSort, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type * as React from "react";
|
||||
|
||||
@ -121,8 +117,8 @@ export const IssuedInvoicesGrid = ({
|
||||
manualSorting
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
onSortChange={onSortChange}
|
||||
onRowClick={onRowClick ? (row) => onRowClick(row.id) : undefined}
|
||||
onSortChange={onSortChange}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
sort={sort}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Badge, ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
@ -164,8 +164,13 @@ export const IssuedInvoicesStatusSegmentedFilter = ({
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{option.icon ? <option.icon className="size-3.5" /> : null}
|
||||
<span>{t(`catalog.issued_invoices.status.${option.value.toLowerCase()}.label`, option.label)}</span>
|
||||
<Badge
|
||||
<span>
|
||||
{t(
|
||||
`catalog.issued_invoices.status.${option.value.toLowerCase()}.label`,
|
||||
option.label
|
||||
)}
|
||||
</span>
|
||||
<RdxBadge
|
||||
className={cn(
|
||||
"min-w-5 rounded-full px-1.5 text-[11px] font-semibold tabular-nums",
|
||||
"bg-muted text-muted-foreground",
|
||||
@ -174,7 +179,7 @@ export const IssuedInvoicesStatusSegmentedFilter = ({
|
||||
variant="secondary"
|
||||
>
|
||||
{counts[option.countKey]}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
</span>
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Badge, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -23,7 +23,7 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className={cn(
|
||||
getVerifactuRecordStatusColor(normalizedStatus),
|
||||
"font-semibold",
|
||||
@ -35,7 +35,7 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
|
||||
{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, {
|
||||
defaultValue: status,
|
||||
})}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
ErrorAlert,
|
||||
PageErrorState,
|
||||
PageHeader,
|
||||
SimpleSearchInput,
|
||||
} from "@erp/core/components";
|
||||
import { ErrorAlert, PageErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||
import { LogoVerifactu } from "@repo/rdx-ui/components";
|
||||
import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components";
|
||||
@ -14,10 +9,7 @@ import { createSearchParams, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { useIssuedInvoiceListPageController } from "../../controllers";
|
||||
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
|
||||
import {
|
||||
IssuedInvoicesEmptyGrid,
|
||||
IssuedInvoicesStatusSegmentedFilter,
|
||||
} from "../components";
|
||||
import { IssuedInvoicesEmptyGrid, IssuedInvoicesStatusSegmentedFilter } from "../components";
|
||||
|
||||
export const ListIssuedInvoicesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -143,8 +135,8 @@ export const ListIssuedInvoicesPage = () => {
|
||||
loading={listCtrl.isLoading}
|
||||
onPageChange={listCtrl.setPageIndex}
|
||||
onPageSizeChange={listCtrl.setPageSize}
|
||||
onSortChange={listCtrl.setSort}
|
||||
onRowClick={handleOpenDetail}
|
||||
onSortChange={listCtrl.setSort}
|
||||
pageIndex={listCtrl.pageIndex}
|
||||
pageSize={listCtrl.pageSize}
|
||||
searching={isSearchPending}
|
||||
|
||||
@ -35,7 +35,7 @@ export const ProformaCreatePage = () => {
|
||||
padding="none"
|
||||
width="full"
|
||||
>
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<RdxPageStack spacing="default">
|
||||
{createCtrl.isCreateError && (
|
||||
<ErrorAlert
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
ProformaDetailActionsViewModel,
|
||||
ProformaDetailHeaderViewModel,
|
||||
} from "../../entities";
|
||||
import { ProformaDetailArchivedBadge, ProformaDetailHeaderMeta } from "../components";
|
||||
import { ProformaDetailHeaderMeta } from "../components";
|
||||
|
||||
import { ProformaDetailHeaderActions } from "./proforma-detail-header-actions";
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { CircleAlertIcon, FileClockIcon, FileOutputIcon, Link2OffIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -33,15 +32,18 @@ export const ProformaLinkedInvoiceSection = ({
|
||||
<Icon aria-hidden="true" className="size-4" />
|
||||
</span>
|
||||
<div className="space-y-2">
|
||||
<Badge variant="outline">{linkedInvoice.primaryText}</Badge>
|
||||
<RdxBadge variant="outline">{linkedInvoice.primaryText}</RdxBadge>
|
||||
{linkedInvoice.secondaryText ? (
|
||||
<p className="text-sm leading-6 text-muted-foreground">{linkedInvoice.secondaryText}</p>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{linkedInvoice.secondaryText}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{linkedInvoice.state === "issued-linked-without-detail-route" && linkedInvoice.linkedInvoiceId ? (
|
||||
{linkedInvoice.state === "issued-linked-without-detail-route" &&
|
||||
linkedInvoice.linkedInvoiceId ? (
|
||||
<ProformaDetailField
|
||||
label={t("proformas.detail.fields.linked_invoice_id")}
|
||||
value={linkedInvoice.linkedInvoiceId}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { InitialsAvatar } from "@repo/rdx-ui/components";
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
||||
|
||||
import type { Proforma } from "../../../../shared";
|
||||
@ -30,7 +29,7 @@ export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Badge className="gap-1" variant="outline">
|
||||
<RdxBadge className="gap-1" variant="outline">
|
||||
{proforma.recipient.tin ? (
|
||||
<>
|
||||
<Building2Icon className="size-3" />
|
||||
@ -42,7 +41,7 @@ export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
||||
Particular
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
|
||||
<button
|
||||
className="flex items-center gap-1 text-xs font-mono text-muted-foreground hover:text-foreground"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
|
||||
@ -22,9 +23,9 @@ export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgePr
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span
|
||||
<RdxBadge
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium leading-none whitespace-nowrap",
|
||||
"inline-flex items-center gap-1.5 border-0 leading-none whitespace-nowrap",
|
||||
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:size-3.5",
|
||||
getProformaStatusBadgeClassName(normalizedStatus),
|
||||
className
|
||||
@ -32,7 +33,7 @@ export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgePr
|
||||
>
|
||||
<Icon aria-hidden="true" />
|
||||
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
|
||||
</span>
|
||||
</RdxBadge>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Badge, ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import {
|
||||
CheckCircle2Icon,
|
||||
@ -114,7 +115,7 @@ export const ProformaStatusSegmentedFilter = ({
|
||||
{PROFORMA_STATUS_FILTER_OPTIONS.map((option) => (
|
||||
<ToggleGroupItem
|
||||
className={cn(
|
||||
"group h-auto min-w-fit rounded-full border-transparent px-3 py-2 text-xs sm:text-sm shadow-none transition-colors",
|
||||
"group h-auto min-w-fit rounded-md border-transparent px-3 py-2 text-xs sm:text-sm shadow-none transition-colors",
|
||||
"text-muted-foreground hover:border-border hover:bg-muted/60 hover:text-foreground",
|
||||
"data-[state=on]:shadow-none",
|
||||
option.activeClassName
|
||||
@ -130,7 +131,7 @@ export const ProformaStatusSegmentedFilter = ({
|
||||
<span className="flex items-center gap-2">
|
||||
{option.icon ? <option.icon className="size-3.5" /> : null}
|
||||
<span>{t(option.labelKey)}</span>
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className={cn(
|
||||
"min-w-5 rounded-full px-1.5 text-[11px] font-semibold tabular-nums",
|
||||
"bg-muted text-muted-foreground",
|
||||
@ -139,7 +140,7 @@ export const ProformaStatusSegmentedFilter = ({
|
||||
variant="secondary"
|
||||
>
|
||||
{counts[option.countKey]}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
</span>
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -144,50 +144,53 @@ const orders = [
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
if (status === "Paid") {
|
||||
return (
|
||||
<Badge className="gap-1 border-emerald-200 bg-emerald-50 text-emerald-700" variant="outline">
|
||||
<RdxBadge
|
||||
className="gap-1 border-emerald-200 bg-emerald-50 text-emerald-700"
|
||||
variant="outline"
|
||||
>
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Paid
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
}
|
||||
if (status === "Cancelled") {
|
||||
return (
|
||||
<Badge className="gap-1 border-red-200 bg-red-50 text-red-600" variant="outline">
|
||||
<RdxBadge className="gap-1 border-red-200 bg-red-50 text-red-600" variant="outline">
|
||||
<XCircle className="h-3 w-3" />
|
||||
Cancelled
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge className="gap-1 border-amber-200 bg-amber-50 text-amber-700" variant="outline">
|
||||
<RdxBadge className="gap-1 border-amber-200 bg-amber-50 text-amber-700" variant="outline">
|
||||
<Clock className="h-3 w-3" />
|
||||
{status}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
}
|
||||
|
||||
function FulfillmentBadge({ fulfillment }: { fulfillment: string }) {
|
||||
if (fulfillment === "Fulfilled") {
|
||||
return (
|
||||
<Badge className="gap-1 border-blue-200 bg-blue-50 text-blue-700" variant="outline">
|
||||
<RdxBadge className="gap-1 border-blue-200 bg-blue-50 text-blue-700" variant="outline">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
}
|
||||
if (fulfillment === "Cancelled") {
|
||||
return (
|
||||
<Badge className="gap-1 border-red-200 bg-red-50 text-red-600" variant="outline">
|
||||
<RdxBadge className="gap-1 border-red-200 bg-red-50 text-red-600" variant="outline">
|
||||
<XCircle className="h-3 w-3" />
|
||||
Cancelled
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge className="gap-1 border-border bg-muted text-muted-foreground" variant="outline">
|
||||
<RdxBadge className="gap-1 border-border bg-muted text-muted-foreground" variant="outline">
|
||||
<Clock className="h-3 w-3" />
|
||||
Unfulfilled
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
@ -54,9 +54,9 @@ export const CustomerViewDialog = ({ customerId, open, onOpenChange }: CustomerV
|
||||
</DialogTitle>
|
||||
<DialogDescription className="px-0">
|
||||
{customer?.tin ? (
|
||||
<Badge className="ml-0 font-mono" variant="secondary">
|
||||
<RdxBadge className="ml-0 font-mono" variant="secondary">
|
||||
{customer.tin}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Ficha del cliente</span>
|
||||
)}
|
||||
@ -106,9 +106,9 @@ export const CustomerViewDialog = ({ customerId, open, onOpenChange }: CustomerV
|
||||
<dt className="text-sm text-muted-foreground">Impuestos por defecto</dt>
|
||||
<dd className="mt-1 flex flex-wrap gap-1">
|
||||
{customer.default_taxes.map((tax: string) => (
|
||||
<Badge key={tax} variant="secondary">
|
||||
<RdxBadge key={tax} variant="secondary">
|
||||
{tax}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
))}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
@ -75,7 +75,7 @@ export const SelectedRecipientSummary = ({
|
||||
<p className="min-w-0 wrap-break-word text-base font-semibold">{recipient.name}</p>
|
||||
|
||||
{recipient.status ? (
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className={cn(
|
||||
"shrink-0 border",
|
||||
getCustomerStatusBadgeClassName(recipient.status)
|
||||
@ -83,7 +83,7 @@ export const SelectedRecipientSummary = ({
|
||||
variant="outline"
|
||||
>
|
||||
{recipient.status}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@ -40,7 +40,12 @@ interface HelpReadinessSectionProps {
|
||||
translate: (key: string, params?: Record<string, string | number | null | undefined>) => string;
|
||||
}
|
||||
|
||||
const HelpTrigger = ({ ariaLabel, className, pendingBlockersCount, ...props }: HelpTriggerProps) => {
|
||||
const HelpTrigger = ({
|
||||
ariaLabel,
|
||||
className,
|
||||
pendingBlockersCount,
|
||||
...props
|
||||
}: HelpTriggerProps) => {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
@ -52,12 +57,12 @@ const HelpTrigger = ({ ariaLabel, className, pendingBlockersCount, ...props }: H
|
||||
>
|
||||
<CircleHelpIcon aria-hidden="true" className="size-4" />
|
||||
{pendingBlockersCount > 0 ? (
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className="pointer-events-none absolute -right-1 -top-1 min-w-5 rounded-full border-amber-200 bg-amber-50 px-1 text-[11px] leading-4 text-amber-700 shadow-none"
|
||||
variant="outline"
|
||||
>
|
||||
{pendingBlockersCount}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
) : null}
|
||||
</Button>
|
||||
);
|
||||
@ -199,7 +204,9 @@ export const ProformaUpdateHelpPopover = ({
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={<HelpTrigger ariaLabel={triggerAriaLabel} pendingBlockersCount={pendingBlockersCount} />}
|
||||
render={
|
||||
<HelpTrigger ariaLabel={triggerAriaLabel} pendingBlockersCount={pendingBlockersCount} />
|
||||
}
|
||||
/>
|
||||
|
||||
<PopoverContent
|
||||
|
||||
@ -75,7 +75,7 @@ export const ProformaUpdateEditorForm = ({
|
||||
</div>
|
||||
}
|
||||
className={className}
|
||||
complexity="complex"
|
||||
width="full"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
DatePickerField,
|
||||
RdxBadge,
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
SelectField,
|
||||
@ -7,7 +8,6 @@ import {
|
||||
TextAreaField,
|
||||
TextField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -45,9 +45,9 @@ export const ProformaUpdateHeaderEditor = ({
|
||||
placeholder={t("form_fields.proformas.invoice_number.placeholder")}
|
||||
readOnly={true}
|
||||
rightIcon={
|
||||
<Badge className="text-sm" variant="secondary">
|
||||
<RdxBadge className="text-sm" variant="secondary">
|
||||
Autom.
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@ -4,13 +4,13 @@ import { QuickCreateCustomerDialog, SelectCustomerDialog } from "@erp/customers"
|
||||
import {
|
||||
CancelActionButton,
|
||||
PercentageField,
|
||||
RdxBadge,
|
||||
RdxEditorStickyFooter,
|
||||
RdxPageContainer,
|
||||
RdxPageStack,
|
||||
RhfSubmitActionButton,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { showErrorToast } from "@repo/rdx-ui/helpers";
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -114,12 +114,12 @@ export const ProformaUpdatePage = () => {
|
||||
isSaving={updateCtrl.form.formState.isSubmitting}
|
||||
labels={{
|
||||
title: proforma ? (
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span className="inline-flex max-w-full items-center gap-3">
|
||||
<span>{t("pages.proformas.update.title", "Editar proforma")}</span>
|
||||
<Badge className="border-border/80 bg-muted px-2.5 py-1 font-mono text-base text-muted-foreground shadow-none">
|
||||
<RdxBadge className="border-border/80 bg-muted text-muted-foreground">
|
||||
{proforma.proformaReference}
|
||||
</Badge>
|
||||
<ProformaStatusBadge className="text-base" status={proforma.status} />
|
||||
</RdxBadge>
|
||||
<ProformaStatusBadge status={proforma.status} />
|
||||
</span>
|
||||
) : (
|
||||
t("pages.proformas.update.title", "Editar proforma")
|
||||
@ -142,7 +142,11 @@ export const ProformaUpdatePage = () => {
|
||||
/>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
|
||||
<RdxPageContainer className="min-h-0 flex-1 overflow-y-auto" padding="none" width="full">
|
||||
<RdxPageContainer
|
||||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
padding="default"
|
||||
width="full"
|
||||
>
|
||||
{hasPageAlerts ? (
|
||||
<RdxPageContainer className="pb-0" width="full">
|
||||
<RdxPageStack spacing="compact">
|
||||
|
||||
@ -16,11 +16,12 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { useCreateRecurringProforma } from "../shared/hooks/use-create-recurring-proforma";
|
||||
|
||||
import {
|
||||
buildCreateRecurringProformaInput,
|
||||
getCreatedRecurringProformaPath,
|
||||
type RecurringProformaCreateForm,
|
||||
RecurringProformaCreateFormSchema,
|
||||
buildCreateRecurringProformaInput,
|
||||
getCreatedRecurringProformaPath,
|
||||
} from "./recurring-proforma-create-utils";
|
||||
|
||||
const RECURRING_PROFORMA_CREATE_FORM_ID = "recurring-proforma-create-form";
|
||||
@ -77,7 +78,7 @@ export const RecurringProformaCreatePage = () => {
|
||||
title={t("recurring_proformas.create.title")}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<form
|
||||
className="space-y-6"
|
||||
id={RECURRING_PROFORMA_CREATE_FORM_ID}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { RecurringProformaStatus } from "../shared";
|
||||
import type { RecurringProformaAction } from "../shared/hooks/use-recurring-proforma-action-mutation";
|
||||
import type { RecurringProformaStatus } from "../shared/recurring-proforma-list";
|
||||
|
||||
export type RecurringProformaDetailActionPlacement = "primary" | "secondary" | "menu";
|
||||
|
||||
@ -17,6 +17,7 @@ export const getRecurringProformaDetailActions = (
|
||||
case "draft":
|
||||
return [
|
||||
{ key: "activate", placement: "primary" },
|
||||
{ key: "edit", placement: "secondary" },
|
||||
{ key: "duplicate", placement: "secondary" },
|
||||
{ key: "complete", placement: "menu", terminal: true },
|
||||
{ key: "delete", placement: "menu", terminal: true, destructive: true },
|
||||
@ -30,6 +31,7 @@ export const getRecurringProformaDetailActions = (
|
||||
case "paused":
|
||||
return [
|
||||
{ key: "resume", placement: "primary" },
|
||||
{ key: "edit", placement: "secondary" },
|
||||
{ key: "duplicate", placement: "secondary" },
|
||||
{ key: "complete", placement: "menu", terminal: true },
|
||||
];
|
||||
@ -40,5 +42,7 @@ export const getRecurringProformaDetailActions = (
|
||||
];
|
||||
case "archived":
|
||||
return [{ key: "duplicate", placement: "primary" }];
|
||||
default:
|
||||
throw new Error(`Unsupported recurring proforma status: ${status}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -19,6 +19,7 @@ test("presenta nombre, estado y recurrencia del detalle principal", () => {
|
||||
name: "Mantenimiento mensual",
|
||||
status: "active",
|
||||
customer_id: null,
|
||||
customer: null,
|
||||
description: null,
|
||||
payment_term_id: null,
|
||||
payment_method_id: null,
|
||||
@ -27,8 +28,15 @@ test("presenta nombre, estado y recurrencia del detalle principal", () => {
|
||||
proforma_series: null,
|
||||
issued_invoice_series: null,
|
||||
global_discount_percentage: null,
|
||||
schedule: { start_date: "2026-09-01", end_date: null, unit: "month", interval: 1, monthly_rule: "same_day" },
|
||||
schedule: {
|
||||
start_date: "2026-09-01",
|
||||
end_date: null,
|
||||
unit: "month",
|
||||
interval: 1,
|
||||
monthly_rule: "same_day",
|
||||
},
|
||||
next_occurrence_date: null,
|
||||
has_issue: false,
|
||||
items: [],
|
||||
created_at: "2026-09-01T10:00:00.000Z",
|
||||
updated_at: "2026-09-01T10:00:00.000Z",
|
||||
@ -53,7 +61,16 @@ test("presenta null como herencia y conserva overrides explicitos", () => {
|
||||
});
|
||||
|
||||
test("resuelve loading, error y contenido del detalle", () => {
|
||||
assert.equal(getRecurringProformaDetailFeedbackState({ isLoading: true, isError: false, hasData: false }), "loading");
|
||||
assert.equal(getRecurringProformaDetailFeedbackState({ isLoading: false, isError: true, hasData: false }), "error");
|
||||
assert.equal(getRecurringProformaDetailFeedbackState({ isLoading: false, isError: false, hasData: true }), "ready");
|
||||
assert.equal(
|
||||
getRecurringProformaDetailFeedbackState({ isLoading: true, isError: false, hasData: false }),
|
||||
"loading"
|
||||
);
|
||||
assert.equal(
|
||||
getRecurringProformaDetailFeedbackState({ isLoading: false, isError: true, hasData: false }),
|
||||
"error"
|
||||
);
|
||||
assert.equal(
|
||||
getRecurringProformaDetailFeedbackState({ isLoading: false, isError: false, hasData: true }),
|
||||
"ready"
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,19 +1,25 @@
|
||||
import { DataTable, DataTableColumnHeader, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
DataTable,
|
||||
DataTableColumnHeader,
|
||||
RdxBadge,
|
||||
SkeletonDataTable,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { useRecurringProformaOccurrences } from "../shared/hooks/use-recurring-proforma-occurrences";
|
||||
import type { RecurringProformaOccurrence } from "../shared/recurring-proforma-list";
|
||||
|
||||
import { formatRecurringProformaDateOnly } from "./recurring-proforma-detail-utils";
|
||||
import {
|
||||
buildGeneratedProformaPath,
|
||||
formatRecurringProformaOccurrenceDateTime,
|
||||
getRecurringProformaOccurrencesFeedbackState,
|
||||
getRecurringProformaOccurrenceStatusKey,
|
||||
getRecurringProformaOccurrencesFeedbackState,
|
||||
} from "./recurring-proforma-occurrences-section-utils";
|
||||
import { useRecurringProformaOccurrences } from "../shared/hooks/use-recurring-proforma-occurrences";
|
||||
import type { RecurringProformaOccurrence } from "../shared/recurring-proforma-list";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
@ -38,22 +44,46 @@ export const RecurringProformaOccurrencesSection = ({
|
||||
() => [
|
||||
{
|
||||
accessorKey: "scheduled_date",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title={t("recurring_proformas.occurrences.columns.scheduled_date")} />,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("recurring_proformas.occurrences.columns.scheduled_date")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => formatRecurringProformaDateOnly(row.original.scheduled_date, dateLocale),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title={t("recurring_proformas.occurrences.columns.status")} />,
|
||||
cell: ({ row }) => <Badge variant="secondary">{t(getRecurringProformaOccurrenceStatusKey(row.original.status))}</Badge>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("recurring_proformas.occurrences.columns.status")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<RdxBadge variant="secondary">
|
||||
{t(getRecurringProformaOccurrenceStatusKey(row.original.status))}
|
||||
</RdxBadge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "attempt_count",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title={t("recurring_proformas.occurrences.columns.attempt_count")} />,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("recurring_proformas.occurrences.columns.attempt_count")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <span className="tabular-nums">{row.original.attempt_count}</span>,
|
||||
},
|
||||
{
|
||||
id: "proforma",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title={t("recurring_proformas.occurrences.columns.proforma")} />,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("recurring_proformas.occurrences.columns.proforma")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const proformaPath = buildGeneratedProformaPath(row.original.proforma_id);
|
||||
return proformaPath ? (
|
||||
@ -65,18 +95,34 @@ export const RecurringProformaOccurrencesSection = ({
|
||||
},
|
||||
{
|
||||
accessorKey: "generated_at",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title={t("recurring_proformas.occurrences.columns.generated_at")} />,
|
||||
cell: ({ row }) => formatRecurringProformaOccurrenceDateTime(row.original.generated_at, dateLocale) ?? "-",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("recurring_proformas.occurrences.columns.generated_at")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
formatRecurringProformaOccurrenceDateTime(row.original.generated_at, dateLocale) ?? "-",
|
||||
},
|
||||
{
|
||||
id: "last_error",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title={t("recurring_proformas.occurrences.columns.last_error")} />,
|
||||
cell: ({ row }) => row.original.status === "failed" && row.original.last_error_message ? (
|
||||
<div className="space-y-1">
|
||||
<p className="max-w-sm text-sm text-destructive">{row.original.last_error_message}</p>
|
||||
{row.original.last_error_code ? <span className="text-xs text-muted-foreground">{row.original.last_error_code}</span> : null}
|
||||
</div>
|
||||
) : null,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("recurring_proformas.occurrences.columns.last_error")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.status === "failed" && row.original.last_error_message ? (
|
||||
<div className="space-y-1">
|
||||
<p className="max-w-sm text-sm text-destructive">{row.original.last_error_message}</p>
|
||||
{row.original.last_error_code ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{row.original.last_error_code}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null,
|
||||
},
|
||||
],
|
||||
[dateLocale, t]
|
||||
@ -84,9 +130,13 @@ export const RecurringProformaOccurrencesSection = ({
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{t("recurring_proformas.occurrences.title")}</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("recurring_proformas.occurrences.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{feedbackState === "loading" ? <SkeletonDataTable columns={6} rows={3} showFooter={false} /> : null}
|
||||
{feedbackState === "loading" ? (
|
||||
<SkeletonDataTable columns={6} rows={3} showFooter={false} />
|
||||
) : null}
|
||||
{feedbackState === "error" ? (
|
||||
<div className="space-y-3 rounded-md border border-destructive/30 p-4" role="alert">
|
||||
<p className="text-sm text-destructive">{t("recurring_proformas.occurrences.error")}</p>
|
||||
@ -95,7 +145,11 @@ export const RecurringProformaOccurrencesSection = ({
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{feedbackState === "empty" ? <p className="text-sm text-muted-foreground">{t("recurring_proformas.occurrences.empty")}</p> : null}
|
||||
{feedbackState === "empty" ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("recurring_proformas.occurrences.empty")}
|
||||
</p>
|
||||
) : null}
|
||||
{feedbackState === "ready" ? (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
@ -104,7 +158,10 @@ export const RecurringProformaOccurrencesSection = ({
|
||||
isFetching={query.isFetching}
|
||||
manualPagination
|
||||
onPageChange={(pageIndex) => setPage(pageIndex + 1)}
|
||||
onPageSizeChange={(pageSize) => { setLimit(pageSize); setPage(1); }}
|
||||
onPageSizeChange={(pageSize) => {
|
||||
setLimit(pageSize);
|
||||
setPage(1);
|
||||
}}
|
||||
pageIndex={data.page - 1}
|
||||
pageSize={data.per_page}
|
||||
totalItems={data.total_items}
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildRecurringProformaOperationalSummary } from "./recurring-proforma-operational-summary";
|
||||
|
||||
test("resume frecuencia y próxima generación con fecha civil", () => {
|
||||
const summary = buildRecurringProformaOperationalSummary({
|
||||
recurringProforma: {
|
||||
id: "recurring-id",
|
||||
name: "Mantenimiento",
|
||||
status: "active",
|
||||
customer_id: "customer-id",
|
||||
customer: { id: "customer-id", name: "ACME Ingeniería", tin: "B12345678" },
|
||||
description: null,
|
||||
payment_term_id: null,
|
||||
payment_method_id: null,
|
||||
language: null,
|
||||
currency: null,
|
||||
proforma_series: null,
|
||||
issued_invoice_series: null,
|
||||
global_discount_percentage: null,
|
||||
schedule: {
|
||||
start_date: "2026-09-01",
|
||||
end_date: null,
|
||||
unit: "month",
|
||||
interval: 2,
|
||||
monthly_rule: "last_day_of_month",
|
||||
},
|
||||
next_occurrence_date: "2026-09-15",
|
||||
has_issue: false,
|
||||
items: [],
|
||||
created_at: "2026-09-01T10:00:00.000Z",
|
||||
updated_at: "2026-09-01T10:00:00.000Z",
|
||||
},
|
||||
locale: "es-ES",
|
||||
now: new Date(2026, 8, 1),
|
||||
t: (key, options) => `${key}:${options?.count ?? ""}`,
|
||||
});
|
||||
|
||||
assert.equal(summary.nextGeneration.date, "15/09/2026");
|
||||
assert.equal(
|
||||
summary.nextGeneration.relativeDate,
|
||||
"recurring_proformas.detail.operational_summary.next_generation.in_days:14"
|
||||
);
|
||||
assert.equal(summary.frequency.primary, "recurring_proformas.list.schedule.month:2");
|
||||
assert.equal(summary.frequency.secondary, "recurring_proformas.list.schedule.last_day_of_month:");
|
||||
});
|
||||
|
||||
test("omite la distancia relativa cuando la próxima generación ya ha pasado", () => {
|
||||
const summary = buildRecurringProformaOperationalSummary({
|
||||
recurringProforma: {
|
||||
id: "recurring-id",
|
||||
name: "Mantenimiento",
|
||||
status: "paused",
|
||||
customer_id: null,
|
||||
customer: null,
|
||||
description: null,
|
||||
payment_term_id: null,
|
||||
payment_method_id: null,
|
||||
language: null,
|
||||
currency: null,
|
||||
proforma_series: null,
|
||||
issued_invoice_series: null,
|
||||
global_discount_percentage: null,
|
||||
schedule: null,
|
||||
next_occurrence_date: "2026-09-01",
|
||||
has_issue: true,
|
||||
items: [],
|
||||
created_at: "2026-09-01T10:00:00.000Z",
|
||||
updated_at: "2026-09-01T10:00:00.000Z",
|
||||
},
|
||||
locale: "es-ES",
|
||||
now: new Date(2026, 8, 2),
|
||||
t: (key) => key,
|
||||
});
|
||||
|
||||
assert.equal(summary.frequency.primary, "—");
|
||||
assert.equal(summary.nextGeneration.relativeDate, null);
|
||||
});
|
||||
@ -0,0 +1,70 @@
|
||||
import type { RecurringProformaDetail } from "../shared/recurring-proforma-list";
|
||||
|
||||
type Translate = (key: string, options?: { count?: number }) => string;
|
||||
|
||||
export type RecurringProformaOperationalSummary = {
|
||||
frequency: {
|
||||
primary: string;
|
||||
secondary: string | null;
|
||||
};
|
||||
nextGeneration: {
|
||||
date: string;
|
||||
relativeDate: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
const formatCivilDate = (value: string, locale: string) => {
|
||||
const [year, month, day] = value.split("-").map(Number);
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
}).format(new Date(year, month - 1, day));
|
||||
};
|
||||
|
||||
const getDaysUntil = (value: string, now: Date) => {
|
||||
const [year, month, day] = value.split("-").map(Number);
|
||||
const target = new Date(year, month - 1, day);
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
return Math.round((target.getTime() - today.getTime()) / 86_400_000);
|
||||
};
|
||||
|
||||
export const buildRecurringProformaOperationalSummary = (params: {
|
||||
recurringProforma: RecurringProformaDetail;
|
||||
locale: string;
|
||||
now?: Date;
|
||||
t: Translate;
|
||||
}): RecurringProformaOperationalSummary => {
|
||||
const { recurringProforma, locale, t } = params;
|
||||
const schedule = recurringProforma.schedule;
|
||||
const nextOccurrenceDate = recurringProforma.next_occurrence_date;
|
||||
const daysUntil = nextOccurrenceDate
|
||||
? getDaysUntil(nextOccurrenceDate, params.now ?? new Date())
|
||||
: null;
|
||||
|
||||
return {
|
||||
frequency: schedule
|
||||
? {
|
||||
primary: t(`recurring_proformas.list.schedule.${schedule.unit}`, {
|
||||
count: schedule.interval,
|
||||
}),
|
||||
secondary: schedule.monthly_rule
|
||||
? t(`recurring_proformas.list.schedule.${schedule.monthly_rule}`)
|
||||
: null,
|
||||
}
|
||||
: { primary: "—", secondary: null },
|
||||
nextGeneration: nextOccurrenceDate
|
||||
? {
|
||||
date: formatCivilDate(nextOccurrenceDate, locale),
|
||||
relativeDate:
|
||||
daysUntil === 0
|
||||
? t("recurring_proformas.detail.operational_summary.next_generation.today")
|
||||
: daysUntil && daysUntil > 0
|
||||
? t("recurring_proformas.detail.operational_summary.next_generation.in_days", {
|
||||
count: daysUntil,
|
||||
})
|
||||
: null,
|
||||
}
|
||||
: { date: "—", relativeDate: null },
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,129 @@
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Building2Icon,
|
||||
CalendarClockIcon,
|
||||
CircleAlertIcon,
|
||||
CircleCheckIcon,
|
||||
Repeat2Icon,
|
||||
} from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { getRecurringProformaStatusKey } from "../../../list/recurring-proforma-list-utils";
|
||||
import { getRecurringProformaStatusBadgeClassName } from "../../../shared/helpers/recurring-proforma-status-ui";
|
||||
import type { RecurringProformaDetail } from "../../../shared/recurring-proforma-list";
|
||||
import { buildRecurringProformaOperationalSummary } from "../../recurring-proforma-operational-summary";
|
||||
|
||||
type RecurringProformaOperationalSummaryProps = {
|
||||
recurringProforma: RecurringProformaDetail;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
type SummaryBlockProps = {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const SummaryBlock = ({ icon, label, children }: SummaryBlockProps) => (
|
||||
<section className="min-w-0 space-y-2 p-4 sm:p-5">
|
||||
<p className="flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{icon}
|
||||
{label}
|
||||
</p>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
|
||||
export const RecurringProformaOperationalSummary = ({
|
||||
recurringProforma,
|
||||
locale,
|
||||
}: RecurringProformaOperationalSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const summary = buildRecurringProformaOperationalSummary({
|
||||
recurringProforma,
|
||||
locale,
|
||||
t: (key, options) => String(t(key, options)),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="grid overflow-hidden rounded-lg border bg-card divide-y sm:grid-cols-2 sm:divide-y-0 sm:[&>*:nth-child(odd)]:border-r xl:grid-cols-5 xl:[&>*]:border-r xl:[&>*:last-child]:border-r-0">
|
||||
<SummaryBlock
|
||||
icon={<Building2Icon aria-hidden="true" className="size-4" />}
|
||||
label={t("recurring_proformas.detail.operational_summary.customer")}
|
||||
>
|
||||
{recurringProforma.customer ? (
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<Link
|
||||
className="block truncate text-sm font-semibold text-primary underline-offset-4 hover:underline"
|
||||
to={`/customers/${recurringProforma.customer.id}/edit`}
|
||||
>
|
||||
{recurringProforma.customer.name}
|
||||
</Link>
|
||||
<p className="text-sm text-muted-foreground">{recurringProforma.customer.tin ?? "—"}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{t("recurring_proformas.detail.no_customer")}
|
||||
</p>
|
||||
)}
|
||||
</SummaryBlock>
|
||||
|
||||
<SummaryBlock
|
||||
icon={<CircleCheckIcon aria-hidden="true" className="size-4" />}
|
||||
label={t("recurring_proformas.detail.operational_summary.status")}
|
||||
>
|
||||
<RdxBadge
|
||||
className={getRecurringProformaStatusBadgeClassName(recurringProforma.status)}
|
||||
variant="secondary"
|
||||
>
|
||||
{t(getRecurringProformaStatusKey(recurringProforma.status))}
|
||||
</RdxBadge>
|
||||
</SummaryBlock>
|
||||
|
||||
<SummaryBlock
|
||||
icon={<CalendarClockIcon aria-hidden="true" className="size-4" />}
|
||||
label={t("recurring_proformas.detail.operational_summary.next_generation.label")}
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-semibold tabular-nums">{summary.nextGeneration.date}</p>
|
||||
{summary.nextGeneration.relativeDate ? (
|
||||
<p className="text-sm text-muted-foreground">{summary.nextGeneration.relativeDate}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</SummaryBlock>
|
||||
|
||||
<SummaryBlock
|
||||
icon={<Repeat2Icon aria-hidden="true" className="size-4" />}
|
||||
label={t("recurring_proformas.detail.operational_summary.frequency")}
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-semibold">{summary.frequency.primary}</p>
|
||||
{summary.frequency.secondary ? (
|
||||
<p className="text-sm text-muted-foreground">{summary.frequency.secondary}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</SummaryBlock>
|
||||
|
||||
<SummaryBlock
|
||||
icon={
|
||||
recurringProforma.has_issue ? (
|
||||
<CircleAlertIcon aria-hidden="true" className="size-4 text-destructive" />
|
||||
) : (
|
||||
<CircleCheckIcon aria-hidden="true" className="size-4" />
|
||||
)
|
||||
}
|
||||
label={t("recurring_proformas.detail.operational_summary.issues")}
|
||||
>
|
||||
<p className="text-sm font-semibold">
|
||||
{t(
|
||||
recurringProforma.has_issue
|
||||
? "recurring_proformas.list.issues.issue"
|
||||
: "recurring_proformas.list.issues.none"
|
||||
)}
|
||||
</p>
|
||||
</SummaryBlock>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,18 +1,17 @@
|
||||
import { PageErrorState } from "@erp/core/components";
|
||||
import { RdxPageStack } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
RdxBadge,
|
||||
RdxEditorPageLayout,
|
||||
RdxPageContainer,
|
||||
RdxPageStack,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { ProformaPageHeader } from "../../../../proformas/shared";
|
||||
import { getRecurringProformaStatusBadgeClassName } from "../../../shared/helpers/recurring-proforma-status-ui";
|
||||
import { useRecurringProforma } from "../../../shared/hooks/use-recurring-proforma";
|
||||
import { useRecurringProformaActionMutation } from "../../../shared/hooks/use-recurring-proforma-action-mutation";
|
||||
import { getRecurringProformaDetailActions } from "../../recurring-proforma-detail-actions";
|
||||
@ -26,6 +25,7 @@ import {
|
||||
} from "../../recurring-proforma-detail-utils";
|
||||
import { RecurringProformaOccurrencesSection } from "../../recurring-proforma-occurrences-section";
|
||||
import { RecurringProformaDetailHeaderActions } from "../blocks/recurring-proforma-detail-header-actions";
|
||||
import { RecurringProformaOperationalSummary } from "../blocks/recurring-proforma-operational-summary";
|
||||
|
||||
type DetailFieldProps = {
|
||||
label: string;
|
||||
@ -110,153 +110,165 @@ export const RecurringProformaDetailPage = () => {
|
||||
backLabel={t("recurring_proformas.detail.back")}
|
||||
onBack={() => navigate("/recurring-proformas")}
|
||||
title={
|
||||
<span className="inline-flex max-w-full items-center gap-3">
|
||||
<span className="truncate">{mainView.name}</span>
|
||||
<Badge className="shrink-0 px-4 py-3 rounded-md" variant="secondary">
|
||||
{t(mainView.statusKey)}
|
||||
</Badge>
|
||||
</span>
|
||||
<div className="space-y-3">
|
||||
<span className="inline-flex max-w-full items-center gap-3">
|
||||
<span className="truncate">{mainView.name}</span>
|
||||
<RdxBadge
|
||||
className={`shrink-0 rounded-md px-4 py-3 ${getRecurringProformaStatusBadgeClassName(recurringProforma.status)}`}
|
||||
variant="secondary"
|
||||
>
|
||||
{t(mainView.statusKey)}
|
||||
</RdxBadge>
|
||||
<RdxBadge className="shrink-0 px-4 py-3 rounded-md" variant="secondary">
|
||||
{t(
|
||||
recurringProforma.has_issue
|
||||
? "recurring_proformas.list.issues.issue"
|
||||
: "recurring_proformas.list.issues.none"
|
||||
)}
|
||||
</RdxBadge>
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">{metadata}</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3 border-b bg-background px-4 py-4 sm:px-6">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("recurring_proformas.detail.context")}
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">{metadata}</p>
|
||||
</div>
|
||||
</ProformaPageHeader>
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("recurring_proformas.detail.general.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{recurringProforma.description ? (
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{recurringProforma.description}
|
||||
</p>
|
||||
) : null}
|
||||
<dl className="space-y-3">
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.customer")}
|
||||
value={recurringProforma.customer_id ?? t("recurring_proformas.detail.no_customer")}
|
||||
/>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<RdxPageContainer className="min-h-0 flex-1 overflow-y-auto" padding="none" width="wide">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<RecurringProformaOperationalSummary
|
||||
locale={dateLocale}
|
||||
recurringProforma={recurringProforma}
|
||||
/>
|
||||
</RdxEditorPageLayout>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("recurring_proformas.detail.schedule.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{schedule ? (
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("recurring_proformas.detail.general.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{recurringProforma.description ? (
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{recurringProforma.description}
|
||||
</p>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("recurring_proformas.detail.schedule.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{schedule ? (
|
||||
<dl className="space-y-3">
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.interval")}
|
||||
value={mainView.scheduleLabel}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.start_date")}
|
||||
value={formatRecurringProformaDateOnly(schedule.start_date)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.end_date")}
|
||||
value={
|
||||
schedule.end_date
|
||||
? formatRecurringProformaDateOnly(schedule.end_date)
|
||||
: t("recurring_proformas.detail.schedule.no_end_date")
|
||||
}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.monthly_rule")}
|
||||
value={
|
||||
schedule.monthly_rule
|
||||
? t(`recurring_proformas.list.schedule.${schedule.monthly_rule}`)
|
||||
: t("recurring_proformas.detail.schedule.not_applicable")
|
||||
}
|
||||
/>
|
||||
</dl>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("recurring_proformas.list.schedule.none")}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("recurring_proformas.detail.commercial.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl className="space-y-3">
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.interval")}
|
||||
value={mainView.scheduleLabel}
|
||||
label={t("recurring_proformas.detail.commercial.payment_term")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.payment_term_id, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.start_date")}
|
||||
value={formatRecurringProformaDateOnly(schedule.start_date)}
|
||||
label={t("recurring_proformas.detail.commercial.payment_method")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.payment_method_id, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.end_date")}
|
||||
value={
|
||||
schedule.end_date
|
||||
? formatRecurringProformaDateOnly(schedule.end_date)
|
||||
: t("recurring_proformas.detail.schedule.no_end_date")
|
||||
}
|
||||
label={t("recurring_proformas.detail.commercial.language")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.language, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.monthly_rule")}
|
||||
value={
|
||||
schedule.monthly_rule
|
||||
? t(`recurring_proformas.list.schedule.${schedule.monthly_rule}`)
|
||||
: t("recurring_proformas.detail.schedule.not_applicable")
|
||||
}
|
||||
label={t("recurring_proformas.detail.commercial.currency")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.currency, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.proforma_series")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.proforma_series, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.issued_invoice_series")}
|
||||
value={formatRecurringProformaOverride(
|
||||
recurringProforma.issued_invoice_series,
|
||||
t
|
||||
)}
|
||||
/>
|
||||
{discount ? (
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.global_discount")}
|
||||
value={discount}
|
||||
/>
|
||||
) : null}
|
||||
</dl>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("recurring_proformas.list.schedule.none")}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("recurring_proformas.detail.commercial.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl className="space-y-3">
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.payment_term")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.payment_term_id, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.payment_method")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.payment_method_id, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.language")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.language, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.currency")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.currency, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.proforma_series")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.proforma_series, t)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.issued_invoice_series")}
|
||||
value={formatRecurringProformaOverride(recurringProforma.issued_invoice_series, t)}
|
||||
/>
|
||||
{discount ? (
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.commercial.global_discount")}
|
||||
value={discount}
|
||||
/>
|
||||
) : null}
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("recurring_proformas.detail.items.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{recurringProforma.items.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("recurring_proformas.detail.items.empty")}
|
||||
</p>
|
||||
) : (
|
||||
<ol className="space-y-3">
|
||||
{recurringProforma.items.map((item) => (
|
||||
<li className="flex gap-3 text-sm" key={item.id}>
|
||||
<span className="tabular-nums text-muted-foreground">{item.position}</span>
|
||||
<span>
|
||||
{item.description ?? t("recurring_proformas.detail.items.no_description")}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("recurring_proformas.detail.items.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{recurringProforma.items.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("recurring_proformas.detail.items.empty")}
|
||||
</p>
|
||||
) : (
|
||||
<ol className="space-y-3">
|
||||
{recurringProforma.items.map((item) => (
|
||||
<li className="flex gap-3 text-sm" key={item.id}>
|
||||
<span className="tabular-nums text-muted-foreground">{item.position}</span>
|
||||
<span>
|
||||
{item.description ?? t("recurring_proformas.detail.items.no_description")}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<RecurringProformaOccurrencesSection recurringProformaId={recurringProforma.id} />
|
||||
|
||||
<RecurringProformaOccurrencesSection recurringProformaId={recurringProforma.id} />
|
||||
|
||||
<Button onClick={() => navigate("/recurring-proformas")} type="button" variant="outline">
|
||||
<ArrowLeftIcon aria-hidden className="size-4" />
|
||||
{t("recurring_proformas.detail.back")}
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/recurring-proformas")} type="button" variant="outline">
|
||||
<ArrowLeftIcon aria-hidden className="size-4" />
|
||||
{t("recurring_proformas.detail.back")}
|
||||
</Button>
|
||||
</RdxPageContainer>
|
||||
</RdxPageStack>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export * from "./create";
|
||||
export * from "./detail";
|
||||
export * from "./list/list-recurring-proformas-page";
|
||||
export * from "./list";
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./list-recurring-proformas-page";
|
||||
export * from "./recurring-proforma-list-utils";
|
||||
@ -141,11 +141,14 @@ mock.module("@repo/rdx-ui/components", {
|
||||
</header>
|
||||
),
|
||||
RdxPageActions: ({ primary }: { primary: ReactNode }) => <div>{primary}</div>,
|
||||
RdxBadge: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||
RdxRowActions: () => <span>Acciones de fila</span>,
|
||||
DataTableColumnHeader: ({ title }: { title: string }) => <span>{title}</span>,
|
||||
DataTable: (props: DataTableProps) => {
|
||||
capturedDataTableProps = props;
|
||||
return <section>{props.topContent}</section>;
|
||||
},
|
||||
SkeletonDataTable: () => <section>Cargando tabla</section>,
|
||||
},
|
||||
});
|
||||
|
||||
@ -234,6 +237,17 @@ test("usa PageErrorState para un error inicial bloqueante", async () => {
|
||||
assert.doesNotMatch(html, /Buscador/);
|
||||
});
|
||||
|
||||
test("muestra un skeleton sin montar DataTable durante la carga inicial", async () => {
|
||||
controllerState = createControllerState();
|
||||
controllerState.listCtrl.isLoading = true;
|
||||
capturedDataTableProps = undefined;
|
||||
|
||||
const html = await renderPage();
|
||||
|
||||
assert.match(html, /Cargando tabla/);
|
||||
assert.equal(capturedDataTableProps, undefined);
|
||||
});
|
||||
|
||||
test("conecta CTA, detalle, búsqueda, filtro y paginación con el controller", async () => {
|
||||
events = {
|
||||
create: false,
|
||||
|
||||
@ -2,11 +2,13 @@ import { PageErrorState, SimpleSearchInput } from "@erp/core/components";
|
||||
import {
|
||||
DataTable,
|
||||
DataTableColumnHeader,
|
||||
RdxBadge,
|
||||
RdxPageContainer,
|
||||
type RdxRowAction,
|
||||
RdxRowActions,
|
||||
SkeletonDataTable,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { Badge, Button } from "@repo/shadcn-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
@ -21,6 +23,7 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { getRecurringProformaStatusBadgeClassName } from "../shared/helpers/recurring-proforma-status-ui";
|
||||
import {
|
||||
type RecurringProformaAction,
|
||||
useRecurringProformaActionMutation,
|
||||
@ -161,7 +164,12 @@ export const ListRecurringProformasPage = () => {
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="secondary">{t(getRecurringProformaStatusKey(row.original.status))}</Badge>
|
||||
<RdxBadge
|
||||
className={getRecurringProformaStatusBadgeClassName(row.original.status)}
|
||||
variant="secondary"
|
||||
>
|
||||
{t(getRecurringProformaStatusKey(row.original.status))}
|
||||
</RdxBadge>
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -278,45 +286,57 @@ export const ListRecurringProformasPage = () => {
|
||||
<ScopedListRecurringProformasPageView
|
||||
content={
|
||||
<div className="mx-auto w-full space-y-4">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={listCtrl.data.items}
|
||||
enablePagination
|
||||
isFetching={listCtrl.isFetching}
|
||||
loading={listCtrl.isLoading}
|
||||
manualPagination
|
||||
manualSorting
|
||||
onPageChange={listCtrl.setPageIndex}
|
||||
onPageSizeChange={listCtrl.setPageSize}
|
||||
onRowClick={(row) => openDetailPage(row.id)}
|
||||
onSortChange={listCtrl.setSort}
|
||||
pageIndex={listCtrl.pageIndex}
|
||||
pageSize={listCtrl.pageSize}
|
||||
searching={isSearchPending}
|
||||
sort={listCtrl.sort}
|
||||
topContent={
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="w-full">
|
||||
<SimpleSearchInput
|
||||
loading={listCtrl.isLoading}
|
||||
onPendingChange={setIsSearchPending}
|
||||
onSearchChange={listCtrl.setSearchValue}
|
||||
placeholder={t("recurring_proformas.list.search_placeholder")}
|
||||
value={listCtrl.search}
|
||||
/>
|
||||
{listCtrl.isLoading ? (
|
||||
<SkeletonDataTable
|
||||
columns={columns.length}
|
||||
footerProps={{
|
||||
pageIndex: listCtrl.pageIndex,
|
||||
pageSize: listCtrl.pageSize,
|
||||
totalItems: listCtrl.data.totalItems,
|
||||
}}
|
||||
rows={Math.max(6, listCtrl.pageSize)}
|
||||
showFooter
|
||||
/>
|
||||
) : (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={listCtrl.data.items}
|
||||
enablePagination
|
||||
isFetching={listCtrl.isFetching}
|
||||
isSearching={isSearchPending}
|
||||
manualPagination
|
||||
manualSorting
|
||||
onPageChange={listCtrl.setPageIndex}
|
||||
onPageSizeChange={listCtrl.setPageSize}
|
||||
onRowClick={(row) => openDetailPage(row.id)}
|
||||
onSortChange={listCtrl.setSort}
|
||||
pageIndex={listCtrl.pageIndex}
|
||||
pageSize={listCtrl.pageSize}
|
||||
sort={listCtrl.sort}
|
||||
topContent={
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="w-full">
|
||||
<SimpleSearchInput
|
||||
loading={listCtrl.isLoading}
|
||||
onPendingChange={setIsSearchPending}
|
||||
onSearchChange={listCtrl.setSearchValue}
|
||||
placeholder={t("recurring_proformas.list.search_placeholder")}
|
||||
value={listCtrl.search}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RecurringProformaStatusSegmentedFilter
|
||||
disabled={listCtrl.isLoading}
|
||||
onValueChange={listCtrl.setStatusFilter}
|
||||
value={listCtrl.statusFilter}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
totalItems={listCtrl.data.totalItems}
|
||||
/>
|
||||
<RecurringProformaStatusSegmentedFilter
|
||||
disabled={listCtrl.isLoading}
|
||||
onValueChange={listCtrl.setStatusFilter}
|
||||
value={listCtrl.statusFilter}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
totalItems={listCtrl.data.totalItems}
|
||||
/>
|
||||
)}
|
||||
|
||||
{listCtrl.data.totalItems === 0 ? (
|
||||
<div className="rounded border border-dashed border-border bg-muted/30 px-4 py-6 text-sm text-muted-foreground">
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from './recurring-proforma-status-segmented-filter';
|
||||
@ -1,10 +1,13 @@
|
||||
import { ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { CheckCircle2Icon, CirclePauseIcon, FileTextIcon, PlayCircleIcon } from "lucide-react";
|
||||
import type { ElementType } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { RecurringProformaStatus } from "../../../shared/recurring-proforma-list";
|
||||
import type { RecurringProformaStatus } from "../../../shared";
|
||||
import {
|
||||
getRecurringProformaStatusIcon,
|
||||
getRecurringProformaStatusSegmentedFilterActiveClassName,
|
||||
} from "../../../shared/helpers/recurring-proforma-status-ui";
|
||||
|
||||
export type RecurringProformaStatusFilter = "all" | RecurringProformaStatus;
|
||||
|
||||
@ -22,34 +25,12 @@ const RECURRING_PROFORMA_STATUS_FILTER_OPTIONS = [
|
||||
activeClassName:
|
||||
"data-[state=on]:border-primary data-[state=on]:bg-primary data-[state=on]:text-primary-foreground",
|
||||
},
|
||||
{
|
||||
value: "draft",
|
||||
labelKey: "recurring_proformas.status.draft",
|
||||
icon: FileTextIcon,
|
||||
activeClassName:
|
||||
"data-[state=on]:border-slate-300 data-[state=on]:bg-slate-100 data-[state=on]:text-slate-700",
|
||||
},
|
||||
{
|
||||
value: "active",
|
||||
labelKey: "recurring_proformas.status.active",
|
||||
icon: PlayCircleIcon,
|
||||
activeClassName:
|
||||
"data-[state=on]:border-emerald-300 data-[state=on]:bg-emerald-50 data-[state=on]:text-emerald-700",
|
||||
},
|
||||
{
|
||||
value: "paused",
|
||||
labelKey: "recurring_proformas.status.paused",
|
||||
icon: CirclePauseIcon,
|
||||
activeClassName:
|
||||
"data-[state=on]:border-amber-300 data-[state=on]:bg-amber-50 data-[state=on]:text-amber-700",
|
||||
},
|
||||
{
|
||||
value: "completed",
|
||||
labelKey: "recurring_proformas.status.completed",
|
||||
icon: CheckCircle2Icon,
|
||||
activeClassName:
|
||||
"data-[state=on]:border-sky-300 data-[state=on]:bg-sky-50 data-[state=on]:text-sky-700",
|
||||
},
|
||||
...(["draft", "active", "paused", "completed"] as const).map((value) => ({
|
||||
value,
|
||||
labelKey: `recurring_proformas.status.${value}`,
|
||||
icon: getRecurringProformaStatusIcon(value),
|
||||
activeClassName: getRecurringProformaStatusSegmentedFilterActiveClassName(value),
|
||||
})),
|
||||
] as const satisfies readonly {
|
||||
value: RecurringProformaStatusFilter;
|
||||
labelKey: string;
|
||||
@ -90,7 +71,7 @@ export const RecurringProformaStatusSegmentedFilter = ({
|
||||
{RECURRING_PROFORMA_STATUS_FILTER_OPTIONS.map((option) => (
|
||||
<ToggleGroupItem
|
||||
className={cn(
|
||||
"group h-auto min-w-fit rounded-full border-transparent px-3 py-2 text-xs shadow-none transition-colors sm:text-sm",
|
||||
"group h-auto min-w-fit rounded-md border-transparent px-3 py-2 text-xs shadow-none transition-colors sm:text-sm",
|
||||
"text-muted-foreground hover:border-border hover:bg-muted/60 hover:text-foreground",
|
||||
"data-[state=on]:shadow-none",
|
||||
option.activeClassName
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./components";
|
||||
export * from "./pages";
|
||||
@ -0,0 +1 @@
|
||||
export * from './scoped-list-recurring-proformas-page-view';
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-recurring-proformas.adapter";
|
||||
@ -0,0 +1,5 @@
|
||||
export * from "./create-recurring-proforma.api";
|
||||
export * from "./create-recurring-proforma-from-proforma.api";
|
||||
export * from "./get-recurring-proforma.api";
|
||||
export * from "./list-recurring-proforma-occurrences.api";
|
||||
export * from "./list-recurring-proformas.api";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./recurring-proforma-status.entity";
|
||||
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Enumeración que representa
|
||||
* los posibles estados de una proforma recurrente en el sistema.
|
||||
*/
|
||||
|
||||
export enum RECURRING_PROFORMA_STATUS {
|
||||
DRAFT = "draft",
|
||||
ACTIVE = "active",
|
||||
PAUSED = "paused",
|
||||
COMPLETED = "completed",
|
||||
ARCHIVED = "archived",
|
||||
}
|
||||
|
||||
export type RecurringProformaStatus = `${RECURRING_PROFORMA_STATUS}`;
|
||||
@ -0,0 +1 @@
|
||||
export * from "./recurring-proforma-status-ui";
|
||||
@ -0,0 +1,19 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
getRecurringProformaStatusBadgeClassName,
|
||||
getRecurringProformaStatusIcon,
|
||||
getRecurringProformaStatusSegmentedFilterActiveClassName,
|
||||
} from "./recurring-proforma-status-ui";
|
||||
|
||||
test("centraliza icono y colores para todos los estados de recurrencia", () => {
|
||||
for (const status of ["draft", "active", "paused", "completed", "archived"] as const) {
|
||||
assert.ok(getRecurringProformaStatusIcon(status));
|
||||
assert.match(getRecurringProformaStatusBadgeClassName(status), /border-/);
|
||||
assert.match(
|
||||
getRecurringProformaStatusSegmentedFilterActiveClassName(status),
|
||||
/data-\[state=on\]/
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import {
|
||||
ArchiveIcon,
|
||||
CheckCircle2Icon,
|
||||
CirclePauseIcon,
|
||||
FileTextIcon,
|
||||
PlayCircleIcon,
|
||||
} from "lucide-react";
|
||||
import type { ElementType } from "react";
|
||||
|
||||
import type { RecurringProformaStatus } from "../entities";
|
||||
|
||||
type RecurringProformaStatusUI = {
|
||||
icon: ElementType;
|
||||
badgeClassName: string;
|
||||
segmentedFilterActiveClassName: string;
|
||||
};
|
||||
|
||||
export const RECURRING_PROFORMA_STATUS_UI: Record<
|
||||
RecurringProformaStatus,
|
||||
RecurringProformaStatusUI
|
||||
> = {
|
||||
draft: {
|
||||
icon: FileTextIcon,
|
||||
badgeClassName: "border-slate-300 bg-slate-100 text-slate-700",
|
||||
segmentedFilterActiveClassName:
|
||||
"data-[state=on]:border-slate-300 data-[state=on]:bg-slate-100 data-[state=on]:text-slate-700",
|
||||
},
|
||||
active: {
|
||||
icon: PlayCircleIcon,
|
||||
badgeClassName: "border-emerald-300 bg-emerald-50 text-emerald-700",
|
||||
segmentedFilterActiveClassName:
|
||||
"data-[state=on]:border-emerald-300 data-[state=on]:bg-emerald-50 data-[state=on]:text-emerald-700",
|
||||
},
|
||||
paused: {
|
||||
icon: CirclePauseIcon,
|
||||
badgeClassName: "border-amber-300 bg-amber-50 text-amber-700",
|
||||
segmentedFilterActiveClassName:
|
||||
"data-[state=on]:border-amber-300 data-[state=on]:bg-amber-50 data-[state=on]:text-amber-700",
|
||||
},
|
||||
completed: {
|
||||
icon: CheckCircle2Icon,
|
||||
badgeClassName: "border-sky-300 bg-sky-50 text-sky-700",
|
||||
segmentedFilterActiveClassName:
|
||||
"data-[state=on]:border-sky-300 data-[state=on]:bg-sky-50 data-[state=on]:text-sky-700",
|
||||
},
|
||||
archived: {
|
||||
icon: ArchiveIcon,
|
||||
badgeClassName: "border-slate-300 bg-slate-50 text-slate-700",
|
||||
segmentedFilterActiveClassName:
|
||||
"data-[state=on]:border-slate-300 data-[state=on]:bg-slate-50 data-[state=on]:text-slate-700",
|
||||
},
|
||||
};
|
||||
|
||||
export const getRecurringProformaStatusBadgeClassName = (status: RecurringProformaStatus) =>
|
||||
RECURRING_PROFORMA_STATUS_UI[status].badgeClassName;
|
||||
|
||||
export const getRecurringProformaStatusIcon = (status: RecurringProformaStatus) =>
|
||||
RECURRING_PROFORMA_STATUS_UI[status].icon;
|
||||
|
||||
export const getRecurringProformaStatusSegmentedFilterActiveClassName = (
|
||||
status: RecurringProformaStatus
|
||||
) => RECURRING_PROFORMA_STATUS_UI[status].segmentedFilterActiveClassName;
|
||||
@ -0,0 +1,7 @@
|
||||
export * from './recurring-proforma-query-keys';
|
||||
export * from './use-create-recurring-proforma-from-proforma';
|
||||
export * from './use-create-recurring-proforma';
|
||||
export * from './use-recurring-proforma-action-mutation';
|
||||
export * from './use-recurring-proforma-occurrences';
|
||||
export * from './use-recurring-proforma';
|
||||
export * from './use-recurring-proformas';
|
||||
@ -4,6 +4,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { recurringProformaQueryKeys } from "./recurring-proforma-query-keys";
|
||||
|
||||
export type RecurringProformaAction =
|
||||
| "edit"
|
||||
| "activate"
|
||||
| "pause"
|
||||
| "resume"
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
export * from "./adapters";
|
||||
export * from "./api";
|
||||
export * from "./entities";
|
||||
export * from "./helpers";
|
||||
export * from "./hooks";
|
||||
export * from "./recurring-proforma-list";
|
||||
export * from "./recurring-proforma-navigation";
|
||||
export * from "./recurring-proforma-schedule-summary";
|
||||
export * from "./ui";
|
||||
@ -1,4 +1,4 @@
|
||||
export type RecurringProformaStatus = "draft" | "active" | "paused" | "completed" | "archived";
|
||||
import type { RecurringProformaStatus } from "./entities";
|
||||
|
||||
export type RecurringProformaListFilters = {
|
||||
page: number;
|
||||
@ -86,6 +86,11 @@ export type RecurringProformaDetail = {
|
||||
name: string;
|
||||
status: RecurringProformaStatus;
|
||||
customer_id: string | null;
|
||||
customer: {
|
||||
id: string;
|
||||
name: string;
|
||||
tin: string | null;
|
||||
} | null;
|
||||
description: string | null;
|
||||
payment_term_id: string | null;
|
||||
payment_method_id: string | null;
|
||||
@ -96,6 +101,7 @@ export type RecurringProformaDetail = {
|
||||
global_discount_percentage: RecurringProformaPercentage | null;
|
||||
schedule: RecurringProformaListItemDTO["schedule"];
|
||||
next_occurrence_date: string | null;
|
||||
has_issue: boolean;
|
||||
items: RecurringProformaDetailItem[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@ -44,7 +44,7 @@ export const CustomerCreatePage = () => {
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
{createCtrl.isCreateError ? (
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { InitialsAvatar } from "@repo/rdx-ui/components";
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
||||
|
||||
import type { Customer } from "../../../../shared";
|
||||
@ -21,9 +20,9 @@ export const CustomerHeader = ({ customer }: { customer: Customer }) => {
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="truncate text-lg font-semibold">{customer.name}</h3>
|
||||
<Badge variant={customer.status === "active" ? "default" : "secondary"}>
|
||||
<RdxBadge variant={customer.status === "active" ? "default" : "secondary"}>
|
||||
{customer.status === "active" ? "Activo" : "Inactivo"}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
</div>
|
||||
|
||||
{customer.tradeName && (
|
||||
@ -31,7 +30,7 @@ export const CustomerHeader = ({ customer }: { customer: Customer }) => {
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Badge className="gap-1" variant="outline">
|
||||
<RdxBadge className="gap-1" variant="outline">
|
||||
{customer.isCompany ? (
|
||||
<>
|
||||
<Building2Icon className="size-3" />
|
||||
@ -43,7 +42,7 @@ export const CustomerHeader = ({ customer }: { customer: Customer }) => {
|
||||
Particular
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
|
||||
<button
|
||||
className="flex items-center gap-1 text-xs font-mono text-muted-foreground hover:text-foreground"
|
||||
|
||||
@ -1,26 +1,27 @@
|
||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||
import { DateHelper } from "@repo/rdx-utils";
|
||||
import { Badge, Button } from "@repo/shadcn-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
|
||||
const getInvoiceStatusBadge = (status: "paid" | "pending" | "overdue") => {
|
||||
switch (status) {
|
||||
case "paid":
|
||||
return (
|
||||
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200" variant="outline">
|
||||
<RdxBadge className="bg-emerald-50 text-emerald-700 border-emerald-200" variant="outline">
|
||||
Pagada
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
case "pending":
|
||||
return (
|
||||
<Badge className="bg-amber-50 text-amber-700 border-amber-200" variant="outline">
|
||||
<RdxBadge className="bg-amber-50 text-amber-700 border-amber-200" variant="outline">
|
||||
Pendiente
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
case "overdue":
|
||||
return (
|
||||
<Badge className="bg-red-50 text-red-700 border-red-200" variant="outline">
|
||||
<RdxBadge className="bg-red-50 text-red-700 border-red-200" variant="outline">
|
||||
Vencida
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import {
|
||||
DataTableColumnHeader,
|
||||
InitialsAvatar,
|
||||
RdxRowActions,
|
||||
RdxBadge,
|
||||
type RdxRowAction,
|
||||
RdxRowActions,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { Badge, Checkbox } from "@repo/shadcn-ui/components";
|
||||
import { Checkbox } from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Building2Icon, EyeIcon, PencilIcon, Trash2Icon, UserIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
@ -97,7 +98,7 @@ export function useCustomersGridColumns(
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-mono">{customer.tin}</span>
|
||||
<Badge className="gap-1 px-1.5 py-0 text-xs font-normal" variant="outline">
|
||||
<RdxBadge className="gap-1 px-1.5 py-0 text-xs font-normal" variant="outline">
|
||||
{customer.isCompany ? (
|
||||
<>
|
||||
<Building2Icon className="size-3" />
|
||||
@ -109,7 +110,7 @@ export function useCustomersGridColumns(
|
||||
{t("fields.customer_type.options.individual")}
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { Building2Icon, User2Icon } from "lucide-react";
|
||||
|
||||
export const KindBadge = ({ isCompany }: { isCompany: boolean }) => (
|
||||
<Badge className="gap-1 tracking-wide text-xs text-foreground/70" variant="outline">
|
||||
<RdxBadge className="gap-1 tracking-wide text-xs text-foreground/70" variant="outline">
|
||||
{isCompany ? <Building2Icon className="size-3.5" /> : <User2Icon className="size-3.5" />}
|
||||
{isCompany ? "Company" : "Person"}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
|
||||
@ -93,26 +93,32 @@ export const CustomerUpdatePage = () => {
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
{updateCtrl.isUpdateError ? (
|
||||
<ErrorAlert
|
||||
message={
|
||||
(updateCtrl.updateError as Error)?.message ??
|
||||
t("pages.customers.update.error_msg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
title={t("pages.customers.update.error_title", "No se pudo guardar los cambios")}
|
||||
/>
|
||||
) : null}
|
||||
{updateCtrl.isUpdateError ? (
|
||||
<ErrorAlert
|
||||
message={
|
||||
(updateCtrl.updateError as Error)?.message ??
|
||||
t(
|
||||
"pages.customers.update.error_msg",
|
||||
"Revisa los datos e inténtalo de nuevo."
|
||||
)
|
||||
}
|
||||
title={t(
|
||||
"pages.customers.update.error_title",
|
||||
"No se pudo guardar los cambios"
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<CustomerUpdateEditorForm
|
||||
fiscalCtrl={updateCtrl.fiscalCtrl}
|
||||
formId={updateCtrl.formId}
|
||||
isSubmitting={updateCtrl.isUpdating}
|
||||
onReset={updateCtrl.resetForm}
|
||||
onSubmit={updateCtrl.onSubmit}
|
||||
/>
|
||||
<CustomerUpdateEditorForm
|
||||
fiscalCtrl={updateCtrl.fiscalCtrl}
|
||||
formId={updateCtrl.formId}
|
||||
isSubmitting={updateCtrl.isUpdating}
|
||||
onReset={updateCtrl.resetForm}
|
||||
onSubmit={updateCtrl.onSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RdxEditorPageLayout>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ErrorAlert, PageFormHeader, PageErrorState } from "@erp/core/components";
|
||||
import { ErrorAlert, PageErrorState, PageFormHeader } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||
import {
|
||||
CancelActionButton,
|
||||
@ -29,7 +29,7 @@ import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
|
||||
const AccountLoadingState = ({ label }: { label: string }) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout className="min-h-full" complexity="simple">
|
||||
<RdxEditorPageLayout className="min-h-full" width="simple">
|
||||
<div className="flex min-h-[16rem] items-center justify-center text-sm text-muted-foreground">
|
||||
<div aria-busy="true" role="status">
|
||||
{label}
|
||||
@ -63,7 +63,7 @@ const AccountErrorState = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<div className="mx-auto w-full max-w-3xl">
|
||||
<PageErrorState
|
||||
description={description}
|
||||
@ -227,8 +227,8 @@ export const AccountPage = () => {
|
||||
isRetrying={ctrl.isRetrying}
|
||||
onRetry={ctrl.retry}
|
||||
reference={ctrl.pageError.reference}
|
||||
retryLabel={t("account.pageError.actions.retry")}
|
||||
retryingLabel={t("account.pageError.actions.retrying")}
|
||||
retryLabel={t("account.pageError.actions.retry")}
|
||||
title={ctrl.pageError.title}
|
||||
variant={ctrl.pageError.variant}
|
||||
/>
|
||||
@ -259,7 +259,7 @@ export const AccountPage = () => {
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<FormProvider {...ctrl.profileForm}>
|
||||
@ -311,22 +311,30 @@ export const AccountPage = () => {
|
||||
|
||||
<SelectField<AccountProfileForm>
|
||||
className="md:col-span-12"
|
||||
description={t("account.sections.personal.fields.languageCode.description")}
|
||||
description={t(
|
||||
"account.sections.personal.fields.languageCode.description"
|
||||
)}
|
||||
disabled={ctrl.isSavingProfile}
|
||||
inputClassName="bg-background"
|
||||
items={[
|
||||
{
|
||||
value: "es",
|
||||
label: t("account.sections.personal.fields.languageCode.options.es"),
|
||||
label: t(
|
||||
"account.sections.personal.fields.languageCode.options.es"
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "en",
|
||||
label: t("account.sections.personal.fields.languageCode.options.en"),
|
||||
label: t(
|
||||
"account.sections.personal.fields.languageCode.options.en"
|
||||
),
|
||||
},
|
||||
]}
|
||||
label={t("account.sections.personal.fields.languageCode.label")}
|
||||
name="languageCode"
|
||||
placeholder={t("account.sections.personal.fields.languageCode.placeholder")}
|
||||
placeholder={t(
|
||||
"account.sections.personal.fields.languageCode.placeholder"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</RdxEditorSection>
|
||||
@ -367,7 +375,9 @@ export const AccountPage = () => {
|
||||
>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-base font-medium">{t("account.sections.security.passwordTitle")}</h2>
|
||||
<h2 className="text-base font-medium">
|
||||
{t("account.sections.security.passwordTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("account.sections.security.passwordDescription")}
|
||||
</p>
|
||||
|
||||
@ -35,7 +35,7 @@ export const SupplierCreatePage = () => {
|
||||
title={t("create.title")}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
{controller.mutation.isError ? (
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
RdxRowActions,
|
||||
SkeletonDataTable,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef, OnChangeFn, VisibilityState } from "@tanstack/react-table";
|
||||
import { Building2Icon, EyeIcon, MailIcon, PencilIcon, PhoneIcon, UserIcon } from "lucide-react";
|
||||
import { type ReactNode, useMemo } from "react";
|
||||
@ -48,14 +47,14 @@ export const useSuppliersGridColumns = ({
|
||||
<span className="block truncate font-medium text-foreground">{supplier.name}</span>
|
||||
<span className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
{supplier.tradeName || supplier.tin || "-"}
|
||||
<Badge className="gap-1 px-1.5 py-0 font-normal" variant="outline">
|
||||
<RdxBadge className="gap-1 px-1.5 py-0 font-normal" variant="outline">
|
||||
{supplier.isCompany ? (
|
||||
<Building2Icon aria-hidden className="size-3" />
|
||||
) : (
|
||||
<UserIcon aria-hidden className="size-3" />
|
||||
)}
|
||||
{t(supplier.isCompany ? "fields.company" : "fields.individual")}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
export const SupplierStatusBadge = ({ status }: { status: string }) => {
|
||||
@ -7,8 +5,8 @@ export const SupplierStatusBadge = ({ status }: { status: string }) => {
|
||||
const isActive = status.toLowerCase() === "active";
|
||||
|
||||
return (
|
||||
<Badge variant={isActive ? "default" : "secondary"}>
|
||||
<RdxBadge variant={isActive ? "default" : "secondary"}>
|
||||
{t(isActive ? "status.active" : "status.inactive")}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
};
|
||||
|
||||
@ -57,7 +57,7 @@ export const SupplierUpdatePage = () => {
|
||||
title={t("update.title")}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||
<RdxEditorPageLayout complexity="simple">
|
||||
<RdxEditorPageLayout width="simple">
|
||||
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
{controller.mutation.isError ? (
|
||||
|
||||
@ -46,6 +46,7 @@
|
||||
"vite-tsconfig-paths": "^6.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@ -18,6 +17,8 @@ import type { Column } from "@tanstack/react-table";
|
||||
import { Check, PlusCircleIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
import { RdxBadge } from "../rdx-badge.tsx";
|
||||
|
||||
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||
column?: Column<TData, TValue>;
|
||||
title?: string;
|
||||
@ -45,25 +46,25 @@ export function DataTableFacetedFilter<TData, TValue>({
|
||||
{selectedValues?.size > 0 && (
|
||||
<>
|
||||
<Separator className="mx-2 h-4" orientation="vertical" />
|
||||
<Badge className="rounded-sm px-1 font-normal lg:hidden" variant="secondary">
|
||||
<RdxBadge className="rounded-sm px-1 font-normal lg:hidden" variant="secondary">
|
||||
{selectedValues.size}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
<div className="hidden gap-1 lg:flex">
|
||||
{selectedValues.size > 2 ? (
|
||||
<Badge className="rounded-sm px-1 font-normal" variant="secondary">
|
||||
<RdxBadge className="rounded-sm px-1 font-normal" variant="secondary">
|
||||
{selectedValues.size} selected
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
) : (
|
||||
options
|
||||
.filter((option) => selectedValues.has(option.value))
|
||||
.map((option) => (
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className="rounded-sm px-1 font-normal"
|
||||
key={option.value}
|
||||
variant="secondary"
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -24,7 +24,7 @@ test("RdxEditorPageLayout aplica complejidad simple por defecto y renderiza chil
|
||||
|
||||
test("RdxEditorPageLayout soporta aside opcional y complejidad complex", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxEditorPageLayout aside={<div>Aside content</div>} complexity="complex">
|
||||
<RdxEditorPageLayout aside={<div>Aside content</div>} width="full">
|
||||
<div>Main content</div>
|
||||
</RdxEditorPageLayout>
|
||||
);
|
||||
@ -37,7 +37,7 @@ test("RdxEditorPageLayout soporta aside opcional y complejidad complex", () => {
|
||||
|
||||
test("RdxEditorPageLayout no reserva aside cuando no existe incluso en complex", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxEditorPageLayout complexity="complex">
|
||||
<RdxEditorPageLayout width="full">
|
||||
<div>Main content</div>
|
||||
</RdxEditorPageLayout>
|
||||
);
|
||||
|
||||
@ -1,38 +1,41 @@
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
|
||||
export type RdxEditorComplexity = "simple" | "complex";
|
||||
export type RdxEditorWidth = "wide" | "full";
|
||||
|
||||
export interface RdxEditorPageLayoutProps
|
||||
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||
children: ReactNode;
|
||||
aside?: ReactNode;
|
||||
complexity?: RdxEditorComplexity;
|
||||
width?: RdxEditorWidth;
|
||||
}
|
||||
|
||||
const CONTENT_WIDTH_CLASSNAME: Record<RdxEditorComplexity, string> = {
|
||||
simple: "mx-auto w-full max-w-5xl",
|
||||
complex: "w-full",
|
||||
const CONTENT_WIDTH_CLASSNAME: Record<RdxEditorWidth, string> = {
|
||||
wide: "mx-auto w-full max-w-5xl",
|
||||
full: "w-full",
|
||||
};
|
||||
|
||||
const ASIDE_GRID_CLASSNAME: Record<RdxEditorComplexity, string> = {
|
||||
simple: "xl:grid-cols-[minmax(0,1fr)_280px]",
|
||||
complex: "xl:grid-cols-[minmax(0,1fr)_22rem]",
|
||||
const ASIDE_GRID_CLASSNAME: Record<RdxEditorWidth, string> = {
|
||||
wide: "xl:grid-cols-[minmax(0,1fr)_280px]",
|
||||
full: "xl:grid-cols-[minmax(0,1fr)_22rem]",
|
||||
};
|
||||
|
||||
export const RdxEditorPageLayout = ({
|
||||
children,
|
||||
aside,
|
||||
complexity = "simple",
|
||||
width = "wide",
|
||||
className,
|
||||
...props
|
||||
}: RdxEditorPageLayoutProps) => {
|
||||
const contentClassName = CONTENT_WIDTH_CLASSNAME[complexity];
|
||||
const contentClassName = CONTENT_WIDTH_CLASSNAME[width];
|
||||
|
||||
return (
|
||||
<div className={cn("min-w-0 w-full px-4 py-4 md:px-6 md:py-6", className)} {...props}>
|
||||
<div
|
||||
className={cn("rdx-editor-page-layout min-w-0 w-full px-4 py-4 md:px-6 md:py-6", className)}
|
||||
{...props}
|
||||
>
|
||||
{aside ? (
|
||||
<div className={cn(contentClassName, "grid min-w-0 gap-6", ASIDE_GRID_CLASSNAME[complexity])}>
|
||||
<div className={cn(contentClassName, "grid min-w-0 gap-6", ASIDE_GRID_CLASSNAME[width])}>
|
||||
<div className="min-w-0">{children}</div>
|
||||
<aside className="min-w-0">{aside}</aside>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@ -24,6 +23,7 @@ import * as React from "react";
|
||||
import { type FieldPath, type FieldValues, useController, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../locales/i18n.ts";
|
||||
import { RdxBadge } from "../rdx-badge.tsx";
|
||||
|
||||
import { FormFieldLabel } from "./form-field-label.tsx";
|
||||
|
||||
@ -182,20 +182,22 @@ export const MultiSelectField = <T extends FieldValues>({
|
||||
const Icon = option?.icon;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className={cn(multiSelectFieldVariants({ variant }), badgeClassName)}
|
||||
key={value}
|
||||
>
|
||||
{Icon ? <Icon className="mr-2 h-4 w-4" /> : null}
|
||||
{option?.label}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
})}
|
||||
|
||||
{selectedValues.length > maxCount ? (
|
||||
<Badge className={cn(multiSelectFieldVariants({ variant }), badgeClassName)}>
|
||||
<RdxBadge
|
||||
className={cn(multiSelectFieldVariants({ variant }), badgeClassName)}
|
||||
>
|
||||
+{selectedValues.length - maxCount}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ export * from "./logo-verifactu.tsx";
|
||||
export * from "./lookup-dialog/index.ts";
|
||||
export * from "./multi-select.tsx";
|
||||
export * from "./pdf-viewer.tsx";
|
||||
export * from "./rdx-badge.tsx";
|
||||
export * from "./right-panel/index.ts";
|
||||
export * from "./row-actions/index.ts";
|
||||
export * from "./scroll-to-top.tsx";
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@repo/shadcn-ui/components/sidebar";
|
||||
import { BadgeCheck, Bell, ChevronsUpDownIcon, CreditCard, LogOut, Sparkles } from "lucide-react";
|
||||
import { Bell, ChevronsUpDownIcon, CreditCard, LogOut, Sparkles } from "lucide-react";
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
@ -79,7 +79,7 @@ export function NavUser({
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
<RdxBadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
|
||||
@ -4,8 +4,7 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
export type RdxPageWidth = "full" | "wide" | "content";
|
||||
export type RdxPageContainerPadding = "default" | "none";
|
||||
|
||||
export interface RdxPageContainerProps
|
||||
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||
export interface RdxPageContainerProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||
children: ReactNode;
|
||||
width?: RdxPageWidth;
|
||||
padding?: RdxPageContainerPadding;
|
||||
@ -31,7 +30,12 @@ export const RdxPageContainer = ({
|
||||
}: RdxPageContainerProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("min-w-0 w-full", WIDTH_CLASSNAME[width], PADDING_CLASSNAME[padding], className)}
|
||||
className={cn(
|
||||
"rdx-page-container min-w-0 w-full",
|
||||
WIDTH_CLASSNAME[width],
|
||||
PADDING_CLASSNAME[padding],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { Badge, Button } from "@repo/shadcn-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
|
||||
import { RdxBadge } from "../rdx-badge.tsx";
|
||||
|
||||
export interface RdxPageHeaderProps
|
||||
extends Omit<ComponentPropsWithoutRef<"header">, "children" | "title"> {
|
||||
title: ReactNode;
|
||||
@ -72,14 +74,14 @@ export const RdxPageHeader = ({
|
||||
</h1>
|
||||
|
||||
{statusLabel ? (
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className={cn(
|
||||
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
|
||||
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
|
||||
)}
|
||||
>
|
||||
{statusLabel}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@ -20,6 +19,8 @@ import * as React from "react";
|
||||
|
||||
import { useTranslation } from "../locales/i18n.ts";
|
||||
|
||||
import { RdxBadge } from "./rdx-badge.tsx";
|
||||
|
||||
/**
|
||||
* Variants for the multi-select component to handle different styles.
|
||||
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
|
||||
@ -255,7 +256,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
const option = options.find((o) => o.value === value);
|
||||
const IconComponent = option?.icon;
|
||||
return (
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className={cn(
|
||||
isAnimating ? "animate-bounce" : "",
|
||||
multiSelectVariants({ variant })
|
||||
@ -272,11 +273,11 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
toggleOption(value);
|
||||
}}
|
||||
/>*/}
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
);
|
||||
})}
|
||||
{selectedValues.length > maxCount && (
|
||||
<Badge
|
||||
<RdxBadge
|
||||
className={cn(
|
||||
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
||||
isAnimating ? "animate-bounce" : "",
|
||||
@ -292,7 +293,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
clearExtraOptions();
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
</RdxBadge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
12
packages/rdx-ui/src/components/rdx-badge.test.tsx
Normal file
12
packages/rdx-ui/src/components/rdx-badge.test.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { RdxBadge } from "./rdx-badge.tsx";
|
||||
|
||||
test("renderiza el contenido sin recurrir al propio componente", () => {
|
||||
const html = renderToStaticMarkup(<RdxBadge variant="secondary">Activa</RdxBadge>);
|
||||
|
||||
assert.match(html, /Activa/);
|
||||
});
|
||||
11
packages/rdx-ui/src/components/rdx-badge.tsx
Normal file
11
packages/rdx-ui/src/components/rdx-badge.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import type { useRender } from "@base-ui/react/use-render";
|
||||
import { Badge, type badgeVariants } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
export const RdxBadge = ({
|
||||
className,
|
||||
...props
|
||||
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) => (
|
||||
<Badge className={cn("tracking-wide px-4 h-6 rounded-md", className)} {...props} />
|
||||
);
|
||||
@ -1,8 +1,7 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils"
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
@ -10,14 +9,11 @@ const badgeVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
@ -25,7 +21,7 @@ const badgeVariants = cva(
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
@ -46,7 +42,7 @@ function Badge({
|
||||
slot: "badge",
|
||||
variant,
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@ -1468,6 +1468,9 @@ importers:
|
||||
|
||||
packages/rdx-ui:
|
||||
dependencies:
|
||||
'@base-ui/react':
|
||||
specifier: ^1.3.0
|
||||
version: 1.5.0(@date-fns/tz@1.5.0)(@types/react@19.2.15)(date-fns@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user