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 {
|
import {
|
||||||
|
RdxBadge,
|
||||||
|
RdxContextPanel,
|
||||||
RdxEditorPageLayout,
|
RdxEditorPageLayout,
|
||||||
RdxEditorSection,
|
RdxEditorSection,
|
||||||
RdxEditorStickyFooter,
|
RdxEditorStickyFooter,
|
||||||
RdxFormGrid,
|
RdxFormGrid,
|
||||||
RdxPageActions,
|
RdxPageActions,
|
||||||
RdxContextPanel,
|
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
RdxPageHeader,
|
RdxPageHeader,
|
||||||
RdxPageStack,
|
RdxPageStack,
|
||||||
@ -13,7 +13,6 @@ import {
|
|||||||
RightPanel,
|
RightPanel,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -34,6 +33,8 @@ import {
|
|||||||
import { ArrowLeftIcon, MoreHorizontalIcon, PlusIcon, XIcon } from "lucide-react";
|
import { ArrowLeftIcon, MoreHorizontalIcon, PlusIcon, XIcon } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { useTheme } from "@/lib/hooks";
|
||||||
|
|
||||||
import { UiKitSection } from "./sections/ui-kit-section";
|
import { UiKitSection } from "./sections/ui-kit-section";
|
||||||
|
|
||||||
type ThemeValue = "dark" | "light" | "system";
|
type ThemeValue = "dark" | "light" | "system";
|
||||||
@ -60,11 +61,7 @@ const ExampleCard = ({ title, notes, children }: ExampleCardProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SurfaceExample = ({ children }: { children: ReactNode }) => {
|
const SurfaceExample = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return <div className="rounded-lg border border-border bg-background p-4">{children}</div>;
|
||||||
<div className="rounded-lg border border-border bg-background p-4">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const OverflowMenuExample = () => {
|
const OverflowMenuExample = () => {
|
||||||
@ -94,27 +91,34 @@ export default function UiKitPage() {
|
|||||||
<RdxPageHeader
|
<RdxPageHeader
|
||||||
actions={
|
actions={
|
||||||
<RdxPageActions
|
<RdxPageActions
|
||||||
|
overflow={<OverflowMenuExample />}
|
||||||
primary={
|
primary={
|
||||||
<Button type="button">
|
<Button type="button">
|
||||||
<PlusIcon className="size-4" />
|
<PlusIcon className="size-4" />
|
||||||
Primary action
|
Primary action
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
secondary={<Button type="button" variant="outline">Secondary</Button>}
|
secondary={
|
||||||
overflow={<OverflowMenuExample />}
|
<Button type="button" variant="outline">
|
||||||
|
Secondary
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
description="Infraestructura mínima del sistema visual Rdx para validar foundations estructurales antes de migrar pantallas funcionales."
|
description="Infraestructura mínima del sistema visual Rdx para validar foundations estructurales antes de migrar pantallas funcionales."
|
||||||
metadata={
|
metadata={
|
||||||
<>
|
<>
|
||||||
<Badge variant="outline">Development only</Badge>
|
<RdxBadge variant="outline">Development only</RdxBadge>
|
||||||
<Badge variant="outline">Static data</Badge>
|
<RdxBadge variant="outline">Static data</RdxBadge>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
title="Rdx UI Kit"
|
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">
|
<div className="flex flex-wrap gap-2">
|
||||||
<a
|
<a
|
||||||
className="inline-flex h-8 items-center rounded-md px-2.5 text-sm font-medium hover:bg-muted"
|
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">
|
<div className="grid gap-4 xl:grid-cols-3">
|
||||||
<ExampleCard notes="`width="full"`" title="Full">
|
<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>
|
<div className="text-sm text-foreground">Available width</div>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
|
|
||||||
<ExampleCard notes="`width="wide"`" title="Wide">
|
<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>
|
<div className="text-sm text-foreground">Max width screen 2xl</div>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
|
|
||||||
<ExampleCard notes="`width="content"`" title="Content">
|
<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>
|
<div className="text-sm text-foreground">Centered content editor</div>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
@ -270,7 +283,10 @@ export default function UiKitPage() {
|
|||||||
>
|
>
|
||||||
<div className="grid gap-4 xl:grid-cols-3">
|
<div className="grid gap-4 xl:grid-cols-3">
|
||||||
<ExampleCard notes="`spacing="compact"`" title="Compact">
|
<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 1</SurfaceExample>
|
||||||
<SurfaceExample>Block 2</SurfaceExample>
|
<SurfaceExample>Block 2</SurfaceExample>
|
||||||
<SurfaceExample>Block 3</SurfaceExample>
|
<SurfaceExample>Block 3</SurfaceExample>
|
||||||
@ -278,7 +294,10 @@ export default function UiKitPage() {
|
|||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
|
|
||||||
<ExampleCard notes="`spacing="default"`" title="Default">
|
<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 1</SurfaceExample>
|
||||||
<SurfaceExample>Block 2</SurfaceExample>
|
<SurfaceExample>Block 2</SurfaceExample>
|
||||||
<SurfaceExample>Block 3</SurfaceExample>
|
<SurfaceExample>Block 3</SurfaceExample>
|
||||||
@ -286,7 +305,10 @@ export default function UiKitPage() {
|
|||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
|
|
||||||
<ExampleCard notes="`spacing="relaxed"`" title="Relaxed">
|
<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 1</SurfaceExample>
|
||||||
<SurfaceExample>Block 2</SurfaceExample>
|
<SurfaceExample>Block 2</SurfaceExample>
|
||||||
<SurfaceExample>Block 3</SurfaceExample>
|
<SurfaceExample>Block 3</SurfaceExample>
|
||||||
@ -323,9 +345,13 @@ export default function UiKitPage() {
|
|||||||
<RdxPageHeader
|
<RdxPageHeader
|
||||||
actions={
|
actions={
|
||||||
<RdxPageActions
|
<RdxPageActions
|
||||||
primary={<Button type="button">Save</Button>}
|
|
||||||
secondary={<Button type="button" variant="outline">Cancel</Button>}
|
|
||||||
overflow={<OverflowMenuExample />}
|
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."
|
description="Header with leading control, metadata and grouped actions."
|
||||||
@ -336,7 +362,7 @@ export default function UiKitPage() {
|
|||||||
}
|
}
|
||||||
metadata={
|
metadata={
|
||||||
<>
|
<>
|
||||||
<Badge variant="outline">Draft</Badge>
|
<RdxBadge variant="outline">Draft</RdxBadge>
|
||||||
<span>Last updated 5 minutes ago</span>
|
<span>Last updated 5 minutes ago</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@ -376,7 +402,11 @@ export default function UiKitPage() {
|
|||||||
<SurfaceExample>
|
<SurfaceExample>
|
||||||
<RdxPageActions
|
<RdxPageActions
|
||||||
primary={<Button type="button">Save</Button>}
|
primary={<Button type="button">Save</Button>}
|
||||||
secondary={<Button type="button" variant="outline">Cancel</Button>}
|
secondary={
|
||||||
|
<Button type="button" variant="outline">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</SurfaceExample>
|
</SurfaceExample>
|
||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
@ -386,7 +416,11 @@ export default function UiKitPage() {
|
|||||||
<RdxPageActions
|
<RdxPageActions
|
||||||
overflow={<OverflowMenuExample />}
|
overflow={<OverflowMenuExample />}
|
||||||
primary={<Button type="button">Approve</Button>}
|
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>
|
</SurfaceExample>
|
||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
@ -398,8 +432,12 @@ export default function UiKitPage() {
|
|||||||
primary={<Button type="button">Primary action with long label</Button>}
|
primary={<Button type="button">Primary action with long label</Button>}
|
||||||
secondary={
|
secondary={
|
||||||
<>
|
<>
|
||||||
<Button type="button" variant="outline">Secondary one</Button>
|
<Button type="button" variant="outline">
|
||||||
<Button type="button" variant="outline">Secondary two</Button>
|
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="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="flex items-start justify-between gap-3 border-b border-border pb-3">
|
||||||
<div className="space-y-1">
|
<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">
|
<div className="text-xs text-muted-foreground">
|
||||||
Panel contextual opcional
|
Panel contextual opcional
|
||||||
</div>
|
</div>
|
||||||
</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" />
|
<XIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -819,7 +864,7 @@ export default function UiKitPage() {
|
|||||||
Sticky header remains outside `rdx-ui`.
|
Sticky header remains outside `rdx-ui`.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline">Modified</Badge>
|
<RdxBadge variant="outline">Modified</RdxBadge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -897,7 +942,11 @@ export default function UiKitPage() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
primary={<Button type="button">Guardar</Button>}
|
primary={<Button type="button">Guardar</Button>}
|
||||||
secondary={<Button type="button" variant="outline">Cancelar</Button>}
|
secondary={
|
||||||
|
<Button type="button" variant="outline">
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
status="Modificada"
|
status="Modificada"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -935,10 +984,7 @@ export default function UiKitPage() {
|
|||||||
<div className="rounded-lg border border-border bg-muted/30">
|
<div className="rounded-lg border border-border bg-muted/30">
|
||||||
<RdxPageContainer width="content">
|
<RdxPageContainer width="content">
|
||||||
<RdxPageStack>
|
<RdxPageStack>
|
||||||
<RdxPageHeader
|
<RdxPageHeader description="Simple centered editor" title="Editor page" />
|
||||||
description="Simple centered editor"
|
|
||||||
title="Editor page"
|
|
||||||
/>
|
|
||||||
<SurfaceExample>Form content</SurfaceExample>
|
<SurfaceExample>Form content</SurfaceExample>
|
||||||
</RdxPageStack>
|
</RdxPageStack>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
@ -947,10 +993,7 @@ export default function UiKitPage() {
|
|||||||
<div className="rounded-lg border border-border bg-muted/30">
|
<div className="rounded-lg border border-border bg-muted/30">
|
||||||
<RdxPageContainer width="wide">
|
<RdxPageContainer width="wide">
|
||||||
<RdxPageStack>
|
<RdxPageStack>
|
||||||
<RdxPageHeader
|
<RdxPageHeader description="Wide detail composition" title="Detail page" />
|
||||||
description="Wide detail composition"
|
|
||||||
title="Detail page"
|
|
||||||
/>
|
|
||||||
<SurfaceExample>Detail content</SurfaceExample>
|
<SurfaceExample>Detail content</SurfaceExample>
|
||||||
</RdxPageStack>
|
</RdxPageStack>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -8,7 +9,6 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Card,
|
Card,
|
||||||
@ -218,9 +218,9 @@ export default function ShadcnShowcasePage() {
|
|||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Badge>Badge</Badge>
|
<RdxBadge>Badge</RdxBadge>
|
||||||
<Badge variant="secondary">Secondary</Badge>
|
<RdxBadge variant="secondary">Secondary</RdxBadge>
|
||||||
<Badge variant="outline">Outline</Badge>
|
<RdxBadge variant="outline">Outline</RdxBadge>
|
||||||
</div>
|
</div>
|
||||||
<RadioGroup className="ml-auto flex w-fit gap-3" defaultValue="apple">
|
<RadioGroup className="ml-auto flex w-fit gap-3" defaultValue="apple">
|
||||||
<RadioGroupItem value="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 { PageErrorState } from "@erp/core/components";
|
||||||
import { RdxPageContainer } from "@repo/rdx-ui/components";
|
import { RdxBadge, RdxPageContainer } from "@repo/rdx-ui/components";
|
||||||
import { Badge, Button, Card, CardContent, RadioGroup, RadioGroupItem } from "@repo/shadcn-ui/components";
|
import { Button, Card, CardContent, RadioGroup, RadioGroupItem } from "@repo/shadcn-ui/components";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -129,15 +129,15 @@ export const PaymentMethodsSettingsPage = () => {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
{paymentMethod.isSystem ? (
|
{paymentMethod.isSystem ? (
|
||||||
<Badge variant="secondary">
|
<RdxBadge variant="secondary">
|
||||||
{t("settings.paymentMethods.status.system")}
|
{t("settings.paymentMethods.status.system")}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{paymentMethod.isDefault ? (
|
{paymentMethod.isDefault ? (
|
||||||
<Badge variant="outline">
|
<RdxBadge variant="outline">
|
||||||
{t("settings.paymentMethods.status.default")}
|
{t("settings.paymentMethods.status.default")}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -204,7 +204,9 @@ export const PaymentMethodsSettingsPage = () => {
|
|||||||
isEditMode
|
isEditMode
|
||||||
isSubmitting={featureCtrl.isSaving || featureCtrl.isEditingPaymentMethodLoading}
|
isSubmitting={featureCtrl.isSaving || featureCtrl.isEditingPaymentMethodLoading}
|
||||||
onSubmit={featureCtrl.onSubmit}
|
onSubmit={featureCtrl.onSubmit}
|
||||||
open={Boolean(featureCtrl.editingPaymentMethod) || featureCtrl.isEditingPaymentMethodLoading}
|
open={
|
||||||
|
Boolean(featureCtrl.editingPaymentMethod) || featureCtrl.isEditingPaymentMethodLoading
|
||||||
|
}
|
||||||
openChange={(open) => {
|
openChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
featureCtrl.closeEditDialog();
|
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 {
|
import {
|
||||||
|
RdxBadge,
|
||||||
RdxFormGrid,
|
RdxFormGrid,
|
||||||
SwitchField,
|
SwitchField,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -21,6 +16,10 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { PlusIcon, Trash2Icon } from "lucide-react";
|
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 { useTranslation } from "../../../../i18n";
|
||||||
import type { PaymentTermForm } from "../../entities";
|
import type { PaymentTermForm } from "../../entities";
|
||||||
@ -74,10 +73,12 @@ export const PaymentTermFormDialog = ({
|
|||||||
});
|
});
|
||||||
const dueRules = useMemo(() => {
|
const dueRules = useMemo(() => {
|
||||||
return fields.map((field, index) => {
|
return fields.map((field, index) => {
|
||||||
return watchedDueRules?.[index] ?? {
|
return (
|
||||||
|
watchedDueRules?.[index] ?? {
|
||||||
dueDays: field.dueDays ?? "",
|
dueDays: field.dueDays ?? "",
|
||||||
percentage: field.percentage ?? "",
|
percentage: field.percentage ?? "",
|
||||||
};
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, [fields, watchedDueRules]);
|
}, [fields, watchedDueRules]);
|
||||||
|
|
||||||
@ -140,13 +141,15 @@ export const PaymentTermFormDialog = ({
|
|||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||||
<div className="space-y-1">
|
<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">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("settings.paymentTerms.form.rules.description")}
|
{t("settings.paymentTerms.form.rules.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Badge
|
<RdxBadge
|
||||||
className={`border px-3 py-1 text-xs ${totalToneClassNames[totalStatus.tone]}`}
|
className={`border px-3 py-1 text-xs ${totalToneClassNames[totalStatus.tone]}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
@ -156,7 +159,7 @@ export const PaymentTermFormDialog = ({
|
|||||||
i18n.language
|
i18n.language
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -173,10 +176,7 @@ export const PaymentTermFormDialog = ({
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<div
|
<div className="rounded-xl border border-border bg-muted/20 p-3" key={field.id}>
|
||||||
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="grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] md:items-start">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground md:hidden">
|
<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 { CalendarClockIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -61,14 +61,16 @@ export const PaymentTermListItem = ({
|
|||||||
{paymentTerm.name}
|
{paymentTerm.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Badge variant={paymentTerm.isActive ? "outline" : "secondary"}>
|
<RdxBadge variant={paymentTerm.isActive ? "outline" : "secondary"}>
|
||||||
{paymentTerm.isActive
|
{paymentTerm.isActive
|
||||||
? t("settings.paymentTerms.status.active")
|
? t("settings.paymentTerms.status.active")
|
||||||
: t("settings.paymentTerms.status.inactive")}
|
: t("settings.paymentTerms.status.inactive")}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
|
|
||||||
{paymentTerm.isSystem ? (
|
{paymentTerm.isSystem ? (
|
||||||
<Badge variant="secondary">{t("settings.paymentTerms.status.system")}</Badge>
|
<RdxBadge variant="secondary">
|
||||||
|
{t("settings.paymentTerms.status.system")}
|
||||||
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { PageErrorState } from "@erp/core/components";
|
import { PageErrorState } from "@erp/core/components";
|
||||||
import { RdxPageContainer } from "@repo/rdx-ui/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 { PlusIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -95,14 +95,16 @@ export const TaxRegimesSettingsPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{taxRegime.isSystem ? (
|
{taxRegime.isSystem ? (
|
||||||
<Badge variant="secondary">{t("settings.taxRegimes.status.system")}</Badge>
|
<RdxBadge variant="secondary">
|
||||||
|
{t("settings.taxRegimes.status.system")}
|
||||||
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Badge variant={taxRegime.isActive ? "outline" : "secondary"}>
|
<RdxBadge variant={taxRegime.isActive ? "outline" : "secondary"}>
|
||||||
{taxRegime.isActive
|
{taxRegime.isActive
|
||||||
? t("settings.taxRegimes.status.active")
|
? t("settings.taxRegimes.status.active")
|
||||||
: t("settings.taxRegimes.status.inactive")}
|
: t("settings.taxRegimes.status.inactive")}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// packages/rdx-ui/src/components/form-actions/erp-form-header.tsx
|
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||||
import { Badge, Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { ArrowLeftIcon } from "lucide-react";
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
@ -62,14 +62,14 @@ export const PageFormHeader = ({
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{statusLabel ? (
|
{statusLabel ? (
|
||||||
<Badge
|
<RdxBadge
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
|
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
|
||||||
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
|
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{statusLabel}
|
{statusLabel}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export type RecurringProformaDetailReadModel = {
|
|||||||
name: string;
|
name: string;
|
||||||
status: RecurringProformaStatus;
|
status: RecurringProformaStatus;
|
||||||
customerId: UniqueID | null;
|
customerId: UniqueID | null;
|
||||||
|
customer: CustomerPublicSummary | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
paymentTermId: UniqueID | null;
|
paymentTermId: UniqueID | null;
|
||||||
paymentMethodId: UniqueID | null;
|
paymentMethodId: UniqueID | null;
|
||||||
@ -60,6 +61,7 @@ export type RecurringProformaDetailReadModel = {
|
|||||||
globalDiscountPercentage: DiscountPercentage | null;
|
globalDiscountPercentage: DiscountPercentage | null;
|
||||||
schedule: RecurringProformaScheduleReadModel | null;
|
schedule: RecurringProformaScheduleReadModel | null;
|
||||||
nextOccurrenceDate: Date | null;
|
nextOccurrenceDate: Date | null;
|
||||||
|
hasIssue: boolean;
|
||||||
items: readonly RecurringProformaItemReadModel[];
|
items: readonly RecurringProformaItemReadModel[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
RecurringProformaOccurrence,
|
RecurringProformaOccurrence,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
import { toRecurringProformaAuditTarget } from "../audit";
|
import { toRecurringProformaAuditTarget } from "../audit";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RecurringProformaCommands,
|
RecurringProformaCommands,
|
||||||
RecurringProformaReader,
|
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 () => {
|
test("RecurringProformaReader conserva overrides nulos, schedule e items ordenados en detail", async () => {
|
||||||
let receivedCompany: UniqueID | undefined;
|
let receivedCompany: UniqueID | undefined;
|
||||||
|
let receivedOccurrenceTransaction: unknown;
|
||||||
const recurring = buildRecurring();
|
const recurring = buildRecurring();
|
||||||
const reader = new RecurringProformaReader(
|
const reader = new RecurringProformaReader(
|
||||||
{
|
{
|
||||||
@ -156,13 +158,25 @@ test("RecurringProformaReader conserva overrides nulos, schedule e items ordenad
|
|||||||
return Result.ok(recurring);
|
return Result.ok(recurring);
|
||||||
},
|
},
|
||||||
} as never,
|
} 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,
|
{} as never,
|
||||||
transactionManager()
|
transactionManager()
|
||||||
);
|
);
|
||||||
const result = await reader.detail({ companyId: companyA, id: recurringId });
|
const result = await reader.detail({ companyId: companyA, id: recurringId });
|
||||||
assert.equal(result.isSuccess, true);
|
assert.equal(result.isSuccess, true);
|
||||||
assert.equal(receivedCompany?.equals(companyA), true);
|
assert.equal(receivedCompany?.equals(companyA), true);
|
||||||
|
assert.equal(receivedOccurrenceTransaction, transaction);
|
||||||
assert.equal(result.data.customerId, null);
|
assert.equal(result.data.customerId, null);
|
||||||
assert.equal(result.data.paymentTermId, null);
|
assert.equal(result.data.paymentTermId, null);
|
||||||
assert.equal(result.data.language, 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]?.ivaCode, "general");
|
||||||
assert.equal(result.data.items[0]?.recCode, "rec");
|
assert.equal(result.data.items[0]?.recCode, "rec");
|
||||||
assert.equal(result.data.items[0]?.retentionCode, "ret");
|
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 () => {
|
test("RecurringProformaReader propaga parent, empresa y paginación de occurrences", async () => {
|
||||||
|
|||||||
@ -81,7 +81,28 @@ export class RecurringProformaReader {
|
|||||||
params.id,
|
params.id,
|
||||||
transaction
|
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) => ({
|
const schedule = source.schedule.map<RecurringProformaScheduleReadModel>((value) => ({
|
||||||
startDate: value.startDate,
|
startDate: value.startDate,
|
||||||
endDate: value.endDate,
|
endDate: value.endDate,
|
||||||
@ -110,6 +135,7 @@ export class RecurringProformaReader {
|
|||||||
(value) => value,
|
(value) => value,
|
||||||
() => null
|
() => null
|
||||||
),
|
),
|
||||||
|
customer,
|
||||||
description: source.description.match(
|
description: source.description.match(
|
||||||
(value) => value,
|
(value) => value,
|
||||||
() => null
|
() => null
|
||||||
@ -150,6 +176,7 @@ export class RecurringProformaReader {
|
|||||||
(value) => value,
|
(value) => value,
|
||||||
() => null
|
() => null
|
||||||
),
|
),
|
||||||
|
hasIssue,
|
||||||
items: source.items.getAll().map((item, index) => ({
|
items: source.items.getAll().map((item, index) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
position: index + 1,
|
position: index + 1,
|
||||||
|
|||||||
@ -44,6 +44,13 @@ export class RecurringProformaDetailPresenter {
|
|||||||
name: item.name,
|
name: item.name,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
customer_id: id(item.customerId),
|
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,
|
description: item.description,
|
||||||
payment_term_id: id(item.paymentTermId),
|
payment_term_id: id(item.paymentTermId),
|
||||||
payment_method_id: id(item.paymentMethodId),
|
payment_method_id: id(item.paymentMethodId),
|
||||||
@ -62,6 +69,7 @@ export class RecurringProformaDetailPresenter {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
next_occurrence_date: dateOnly(item.nextOccurrenceDate),
|
next_occurrence_date: dateOnly(item.nextOccurrenceDate),
|
||||||
|
has_issue: item.hasIssue,
|
||||||
items: item.items.map((recurringItem: any) => ({
|
items: item.items.map((recurringItem: any) => ({
|
||||||
...recurringItem,
|
...recurringItem,
|
||||||
id: id(recurringItem.id),
|
id: id(recurringItem.id),
|
||||||
|
|||||||
@ -10,11 +10,13 @@ import { RecurringProformaDetailPresenter } from "./index";
|
|||||||
|
|
||||||
test("RecurringProformaDetailPresenter conserva PercentageSchema como strings value y scale", () => {
|
test("RecurringProformaDetailPresenter conserva PercentageSchema como strings value y scale", () => {
|
||||||
const discount = DiscountPercentage.create({ value: 500 }).data;
|
const discount = DiscountPercentage.create({ value: 500 }).data;
|
||||||
|
const customerId = UniqueID.generateNewID();
|
||||||
const dto = new RecurringProformaDetailPresenter().present({
|
const dto = new RecurringProformaDetailPresenter().present({
|
||||||
id: UniqueID.generateNewID(),
|
id: UniqueID.generateNewID(),
|
||||||
name: "Cuota",
|
name: "Cuota",
|
||||||
status: "draft",
|
status: "draft",
|
||||||
customerId: null,
|
customerId,
|
||||||
|
customer: { id: customerId, name: "ACME Ingeniería", tin: "B12345678" },
|
||||||
description: null,
|
description: null,
|
||||||
paymentTermId: null,
|
paymentTermId: null,
|
||||||
paymentMethodId: null,
|
paymentMethodId: null,
|
||||||
@ -25,6 +27,7 @@ test("RecurringProformaDetailPresenter conserva PercentageSchema como strings va
|
|||||||
globalDiscountPercentage: discount,
|
globalDiscountPercentage: discount,
|
||||||
schedule: null,
|
schedule: null,
|
||||||
nextOccurrenceDate: null,
|
nextOccurrenceDate: null,
|
||||||
|
hasIssue: false,
|
||||||
items: [],
|
items: [],
|
||||||
createdAt: new Date("2027-01-01T10:00:00.000Z"),
|
createdAt: new Date("2027-01-01T10:00:00.000Z"),
|
||||||
updatedAt: 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.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.value, "string");
|
||||||
assert.equal(typeof dto.global_discount_percentage.scale, "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", () => {
|
test("RecurringProformaDetailPresenter serializa Value Objects antes de responder HTTP", () => {
|
||||||
@ -45,6 +54,7 @@ test("RecurringProformaDetailPresenter serializa Value Objects antes de responde
|
|||||||
name: "Cuota",
|
name: "Cuota",
|
||||||
status: "draft",
|
status: "draft",
|
||||||
customerId: null,
|
customerId: null,
|
||||||
|
customer: null,
|
||||||
description: null,
|
description: null,
|
||||||
paymentTermId: null,
|
paymentTermId: null,
|
||||||
paymentMethodId: null,
|
paymentMethodId: null,
|
||||||
@ -55,6 +65,7 @@ test("RecurringProformaDetailPresenter serializa Value Objects antes de responde
|
|||||||
globalDiscountPercentage: null,
|
globalDiscountPercentage: null,
|
||||||
schedule: null,
|
schedule: null,
|
||||||
nextOccurrenceDate: null,
|
nextOccurrenceDate: null,
|
||||||
|
hasIssue: true,
|
||||||
items: [{ id: UniqueID.generateNewID(), position: 1, description: itemDescription }],
|
items: [{ id: UniqueID.generateNewID(), position: 1, description: itemDescription }],
|
||||||
createdAt: new Date("2027-01-01T10:00:00.000Z"),
|
createdAt: new Date("2027-01-01T10:00:00.000Z"),
|
||||||
updatedAt: 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.proforma_series, "PRO");
|
||||||
assert.equal(dto.issued_invoice_series, "FAC");
|
assert.equal(dto.issued_invoice_series, "FAC");
|
||||||
assert.equal(dto.items[0]?.description, "Servicio mensual");
|
assert.equal(dto.items[0]?.description, "Servicio mensual");
|
||||||
|
assert.equal(dto.has_issue, true);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -107,6 +107,18 @@
|
|||||||
"created_at": "Created",
|
"created_at": "Created",
|
||||||
"updated_at": "Updated",
|
"updated_at": "Updated",
|
||||||
"metadata": "Created on {{createdAt}} · Last updated {{updatedAt}}",
|
"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",
|
"inherited": "Inherited",
|
||||||
"general": {
|
"general": {
|
||||||
"title": "General information"
|
"title": "General information"
|
||||||
|
|||||||
@ -107,6 +107,18 @@
|
|||||||
"created_at": "Creada",
|
"created_at": "Creada",
|
||||||
"updated_at": "Actualizada",
|
"updated_at": "Actualizada",
|
||||||
"metadata": "Creada el {{createdAt}} · Última actualización {{updatedAt}}",
|
"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",
|
"inherited": "Heredado",
|
||||||
"general": {
|
"general": {
|
||||||
"title": "Informacion general"
|
"title": "Informacion general"
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { IssuedInvoiceDetailCommercialViewModel } from "../../entities";
|
import type { IssuedInvoiceDetailCommercialViewModel } from "../../entities";
|
||||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||||
@ -8,9 +6,7 @@ type IssuedInvoicePaymentSectionProps = {
|
|||||||
commercial: IssuedInvoiceDetailCommercialViewModel;
|
commercial: IssuedInvoiceDetailCommercialViewModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuedInvoicePaymentSection = ({
|
export const IssuedInvoicePaymentSection = ({ commercial }: IssuedInvoicePaymentSectionProps) => {
|
||||||
commercial,
|
|
||||||
}: IssuedInvoicePaymentSectionProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -21,7 +17,9 @@ export const IssuedInvoicePaymentSection = ({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<IssuedInvoiceDetailField
|
<IssuedInvoiceDetailField
|
||||||
label={t("pages.issued_invoices.detail.fields.payment_method")}
|
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 ? (
|
{commercial.paymentMethodDescription ? (
|
||||||
<IssuedInvoiceDetailField
|
<IssuedInvoiceDetailField
|
||||||
@ -32,7 +30,9 @@ export const IssuedInvoicePaymentSection = ({
|
|||||||
) : null}
|
) : null}
|
||||||
<IssuedInvoiceDetailField
|
<IssuedInvoiceDetailField
|
||||||
label={t("pages.issued_invoices.detail.fields.payment_term")}
|
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 ? (
|
{commercial.paymentTermDescription ? (
|
||||||
<IssuedInvoiceDetailField
|
<IssuedInvoiceDetailField
|
||||||
@ -47,9 +47,9 @@ export const IssuedInvoicePaymentSection = ({
|
|||||||
commercial.paymentTermDueRules.length > 0 ? (
|
commercial.paymentTermDueRules.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-2 sm:justify-end">
|
<div className="flex flex-wrap gap-2 sm:justify-end">
|
||||||
{commercial.paymentTermDueRules.map((rule) => (
|
{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}
|
{rule.percentageLabel} · {rule.dueDaysLabel}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { DataTable, type DataTableSort, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||||
DataTable,
|
|
||||||
type DataTableSort,
|
|
||||||
SkeletonDataTable,
|
|
||||||
} from "@repo/rdx-ui/components";
|
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
|
|
||||||
@ -121,8 +117,8 @@ export const IssuedInvoicesGrid = ({
|
|||||||
manualSorting
|
manualSorting
|
||||||
onPageChange={onPageChange}
|
onPageChange={onPageChange}
|
||||||
onPageSizeChange={onPageSizeChange}
|
onPageSizeChange={onPageSizeChange}
|
||||||
onSortChange={onSortChange}
|
|
||||||
onRowClick={onRowClick ? (row) => onRowClick(row.id) : undefined}
|
onRowClick={onRowClick ? (row) => onRowClick(row.id) : undefined}
|
||||||
|
onSortChange={onSortChange}
|
||||||
pageIndex={pageIndex}
|
pageIndex={pageIndex}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
sort={sort}
|
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 { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import {
|
import {
|
||||||
AlertTriangleIcon,
|
AlertTriangleIcon,
|
||||||
@ -164,8 +164,13 @@ export const IssuedInvoicesStatusSegmentedFilter = ({
|
|||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{option.icon ? <option.icon className="size-3.5" /> : null}
|
{option.icon ? <option.icon className="size-3.5" /> : null}
|
||||||
<span>{t(`catalog.issued_invoices.status.${option.value.toLowerCase()}.label`, option.label)}</span>
|
<span>
|
||||||
<Badge
|
{t(
|
||||||
|
`catalog.issued_invoices.status.${option.value.toLowerCase()}.label`,
|
||||||
|
option.label
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<RdxBadge
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-5 rounded-full px-1.5 text-[11px] font-semibold tabular-nums",
|
"min-w-5 rounded-full px-1.5 text-[11px] font-semibold tabular-nums",
|
||||||
"bg-muted text-muted-foreground",
|
"bg-muted text-muted-foreground",
|
||||||
@ -174,7 +179,7 @@ export const IssuedInvoicesStatusSegmentedFilter = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{counts[option.countKey]}
|
{counts[option.countKey]}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
</span>
|
</span>
|
||||||
</ToggleGroupItem>
|
</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 { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -23,7 +23,7 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={
|
||||||
<Badge
|
<RdxBadge
|
||||||
className={cn(
|
className={cn(
|
||||||
getVerifactuRecordStatusColor(normalizedStatus),
|
getVerifactuRecordStatusColor(normalizedStatus),
|
||||||
"font-semibold",
|
"font-semibold",
|
||||||
@ -35,7 +35,7 @@ export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadge
|
|||||||
{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, {
|
{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, {
|
||||||
defaultValue: status,
|
defaultValue: status,
|
||||||
})}
|
})}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { ErrorAlert, PageErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||||
ErrorAlert,
|
|
||||||
PageErrorState,
|
|
||||||
PageHeader,
|
|
||||||
SimpleSearchInput,
|
|
||||||
} from "@erp/core/components";
|
|
||||||
import { useReturnToNavigation } from "@erp/core/hooks";
|
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { LogoVerifactu } from "@repo/rdx-ui/components";
|
import { LogoVerifactu } from "@repo/rdx-ui/components";
|
||||||
import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components";
|
import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components";
|
||||||
@ -14,10 +9,7 @@ import { createSearchParams, useNavigate } from "react-router-dom";
|
|||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import { useIssuedInvoiceListPageController } from "../../controllers";
|
import { useIssuedInvoiceListPageController } from "../../controllers";
|
||||||
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
|
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
|
||||||
import {
|
import { IssuedInvoicesEmptyGrid, IssuedInvoicesStatusSegmentedFilter } from "../components";
|
||||||
IssuedInvoicesEmptyGrid,
|
|
||||||
IssuedInvoicesStatusSegmentedFilter,
|
|
||||||
} from "../components";
|
|
||||||
|
|
||||||
export const ListIssuedInvoicesPage = () => {
|
export const ListIssuedInvoicesPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -143,8 +135,8 @@ export const ListIssuedInvoicesPage = () => {
|
|||||||
loading={listCtrl.isLoading}
|
loading={listCtrl.isLoading}
|
||||||
onPageChange={listCtrl.setPageIndex}
|
onPageChange={listCtrl.setPageIndex}
|
||||||
onPageSizeChange={listCtrl.setPageSize}
|
onPageSizeChange={listCtrl.setPageSize}
|
||||||
onSortChange={listCtrl.setSort}
|
|
||||||
onRowClick={handleOpenDetail}
|
onRowClick={handleOpenDetail}
|
||||||
|
onSortChange={listCtrl.setSort}
|
||||||
pageIndex={listCtrl.pageIndex}
|
pageIndex={listCtrl.pageIndex}
|
||||||
pageSize={listCtrl.pageSize}
|
pageSize={listCtrl.pageSize}
|
||||||
searching={isSearchPending}
|
searching={isSearchPending}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export const ProformaCreatePage = () => {
|
|||||||
padding="none"
|
padding="none"
|
||||||
width="full"
|
width="full"
|
||||||
>
|
>
|
||||||
<RdxEditorPageLayout complexity="simple">
|
<RdxEditorPageLayout width="simple">
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageStack spacing="default">
|
||||||
{createCtrl.isCreateError && (
|
{createCtrl.isCreateError && (
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import type {
|
|||||||
ProformaDetailActionsViewModel,
|
ProformaDetailActionsViewModel,
|
||||||
ProformaDetailHeaderViewModel,
|
ProformaDetailHeaderViewModel,
|
||||||
} from "../../entities";
|
} from "../../entities";
|
||||||
import { ProformaDetailArchivedBadge, ProformaDetailHeaderMeta } from "../components";
|
import { ProformaDetailHeaderMeta } from "../components";
|
||||||
|
|
||||||
import { ProformaDetailHeaderActions } from "./proforma-detail-header-actions";
|
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 { CircleAlertIcon, FileClockIcon, FileOutputIcon, Link2OffIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -33,15 +32,18 @@ export const ProformaLinkedInvoiceSection = ({
|
|||||||
<Icon aria-hidden="true" className="size-4" />
|
<Icon aria-hidden="true" className="size-4" />
|
||||||
</span>
|
</span>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Badge variant="outline">{linkedInvoice.primaryText}</Badge>
|
<RdxBadge variant="outline">{linkedInvoice.primaryText}</RdxBadge>
|
||||||
{linkedInvoice.secondaryText ? (
|
{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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{linkedInvoice.state === "issued-linked-without-detail-route" && linkedInvoice.linkedInvoiceId ? (
|
{linkedInvoice.state === "issued-linked-without-detail-route" &&
|
||||||
|
linkedInvoice.linkedInvoiceId ? (
|
||||||
<ProformaDetailField
|
<ProformaDetailField
|
||||||
label={t("proformas.detail.fields.linked_invoice_id")}
|
label={t("proformas.detail.fields.linked_invoice_id")}
|
||||||
value={linkedInvoice.linkedInvoiceId}
|
value={linkedInvoice.linkedInvoiceId}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { InitialsAvatar } from "@repo/rdx-ui/components";
|
import { InitialsAvatar } from "@repo/rdx-ui/components";
|
||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
||||||
|
|
||||||
import type { Proforma } from "../../../../shared";
|
import type { Proforma } from "../../../../shared";
|
||||||
@ -30,7 +29,7 @@ export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<Badge className="gap-1" variant="outline">
|
<RdxBadge className="gap-1" variant="outline">
|
||||||
{proforma.recipient.tin ? (
|
{proforma.recipient.tin ? (
|
||||||
<>
|
<>
|
||||||
<Building2Icon className="size-3" />
|
<Building2Icon className="size-3" />
|
||||||
@ -42,7 +41,7 @@ export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
|||||||
Particular
|
Particular
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 text-xs font-mono text-muted-foreground hover:text-foreground"
|
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 { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
@ -22,9 +23,9 @@ export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgePr
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={
|
||||||
<span
|
<RdxBadge
|
||||||
className={cn(
|
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",
|
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:size-3.5",
|
||||||
getProformaStatusBadgeClassName(normalizedStatus),
|
getProformaStatusBadgeClassName(normalizedStatus),
|
||||||
className
|
className
|
||||||
@ -32,7 +33,7 @@ export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgePr
|
|||||||
>
|
>
|
||||||
<Icon aria-hidden="true" />
|
<Icon aria-hidden="true" />
|
||||||
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
|
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
|
||||||
</span>
|
</RdxBadge>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TooltipContent>
|
<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 { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import {
|
import {
|
||||||
CheckCircle2Icon,
|
CheckCircle2Icon,
|
||||||
@ -114,7 +115,7 @@ export const ProformaStatusSegmentedFilter = ({
|
|||||||
{PROFORMA_STATUS_FILTER_OPTIONS.map((option) => (
|
{PROFORMA_STATUS_FILTER_OPTIONS.map((option) => (
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className={cn(
|
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",
|
"text-muted-foreground hover:border-border hover:bg-muted/60 hover:text-foreground",
|
||||||
"data-[state=on]:shadow-none",
|
"data-[state=on]:shadow-none",
|
||||||
option.activeClassName
|
option.activeClassName
|
||||||
@ -130,7 +131,7 @@ export const ProformaStatusSegmentedFilter = ({
|
|||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{option.icon ? <option.icon className="size-3.5" /> : null}
|
{option.icon ? <option.icon className="size-3.5" /> : null}
|
||||||
<span>{t(option.labelKey)}</span>
|
<span>{t(option.labelKey)}</span>
|
||||||
<Badge
|
<RdxBadge
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-5 rounded-full px-1.5 text-[11px] font-semibold tabular-nums",
|
"min-w-5 rounded-full px-1.5 text-[11px] font-semibold tabular-nums",
|
||||||
"bg-muted text-muted-foreground",
|
"bg-muted text-muted-foreground",
|
||||||
@ -139,7 +140,7 @@ export const ProformaStatusSegmentedFilter = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{counts[option.countKey]}
|
{counts[option.countKey]}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
</span>
|
</span>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -144,50 +144,53 @@ const orders = [
|
|||||||
function StatusBadge({ status }: { status: string }) {
|
function StatusBadge({ status }: { status: string }) {
|
||||||
if (status === "Paid") {
|
if (status === "Paid") {
|
||||||
return (
|
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" />
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
Paid
|
Paid
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (status === "Cancelled") {
|
if (status === "Cancelled") {
|
||||||
return (
|
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" />
|
<XCircle className="h-3 w-3" />
|
||||||
Cancelled
|
Cancelled
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
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" />
|
<Clock className="h-3 w-3" />
|
||||||
{status}
|
{status}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FulfillmentBadge({ fulfillment }: { fulfillment: string }) {
|
function FulfillmentBadge({ fulfillment }: { fulfillment: string }) {
|
||||||
if (fulfillment === "Fulfilled") {
|
if (fulfillment === "Fulfilled") {
|
||||||
return (
|
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" />
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
Fulfilled
|
Fulfilled
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (fulfillment === "Cancelled") {
|
if (fulfillment === "Cancelled") {
|
||||||
return (
|
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" />
|
<XCircle className="h-3 w-3" />
|
||||||
Cancelled
|
Cancelled
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
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" />
|
<Clock className="h-3 w-3" />
|
||||||
Unfulfilled
|
Unfulfilled
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -54,9 +54,9 @@ export const CustomerViewDialog = ({ customerId, open, onOpenChange }: CustomerV
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="px-0">
|
<DialogDescription className="px-0">
|
||||||
{customer?.tin ? (
|
{customer?.tin ? (
|
||||||
<Badge className="ml-0 font-mono" variant="secondary">
|
<RdxBadge className="ml-0 font-mono" variant="secondary">
|
||||||
{customer.tin}
|
{customer.tin}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">Ficha del cliente</span>
|
<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>
|
<dt className="text-sm text-muted-foreground">Impuestos por defecto</dt>
|
||||||
<dd className="mt-1 flex flex-wrap gap-1">
|
<dd className="mt-1 flex flex-wrap gap-1">
|
||||||
{customer.default_taxes.map((tax: string) => (
|
{customer.default_taxes.map((tax: string) => (
|
||||||
<Badge key={tax} variant="secondary">
|
<RdxBadge key={tax} variant="secondary">
|
||||||
{tax}
|
{tax}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
))}
|
))}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { CustomerSelectionOption } from "@erp/customers";
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
|
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
@ -75,7 +75,7 @@ export const SelectedRecipientSummary = ({
|
|||||||
<p className="min-w-0 wrap-break-word text-base font-semibold">{recipient.name}</p>
|
<p className="min-w-0 wrap-break-word text-base font-semibold">{recipient.name}</p>
|
||||||
|
|
||||||
{recipient.status ? (
|
{recipient.status ? (
|
||||||
<Badge
|
<RdxBadge
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 border",
|
"shrink-0 border",
|
||||||
getCustomerStatusBadgeClassName(recipient.status)
|
getCustomerStatusBadgeClassName(recipient.status)
|
||||||
@ -83,7 +83,7 @@ export const SelectedRecipientSummary = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{recipient.status}
|
{recipient.status}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@ -40,7 +40,12 @@ interface HelpReadinessSectionProps {
|
|||||||
translate: (key: string, params?: Record<string, string | number | null | undefined>) => string;
|
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 (
|
return (
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
@ -52,12 +57,12 @@ const HelpTrigger = ({ ariaLabel, className, pendingBlockersCount, ...props }: H
|
|||||||
>
|
>
|
||||||
<CircleHelpIcon aria-hidden="true" className="size-4" />
|
<CircleHelpIcon aria-hidden="true" className="size-4" />
|
||||||
{pendingBlockersCount > 0 ? (
|
{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"
|
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"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{pendingBlockersCount}
|
{pendingBlockersCount}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@ -199,7 +204,9 @@ export const ProformaUpdateHelpPopover = ({
|
|||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
render={<HelpTrigger ariaLabel={triggerAriaLabel} pendingBlockersCount={pendingBlockersCount} />}
|
render={
|
||||||
|
<HelpTrigger ariaLabel={triggerAriaLabel} pendingBlockersCount={pendingBlockersCount} />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className={className}
|
className={className}
|
||||||
complexity="complex"
|
width="full"
|
||||||
>
|
>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
|
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
DatePickerField,
|
DatePickerField,
|
||||||
|
RdxBadge,
|
||||||
RdxEditorSection,
|
RdxEditorSection,
|
||||||
RdxFormGrid,
|
RdxFormGrid,
|
||||||
SelectField,
|
SelectField,
|
||||||
@ -7,7 +8,6 @@ import {
|
|||||||
TextAreaField,
|
TextAreaField,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
import { FileTextIcon } from "lucide-react";
|
import { FileTextIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -45,9 +45,9 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
placeholder={t("form_fields.proformas.invoice_number.placeholder")}
|
placeholder={t("form_fields.proformas.invoice_number.placeholder")}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
rightIcon={
|
rightIcon={
|
||||||
<Badge className="text-sm" variant="secondary">
|
<RdxBadge className="text-sm" variant="secondary">
|
||||||
Autom.
|
Autom.
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import { QuickCreateCustomerDialog, SelectCustomerDialog } from "@erp/customers"
|
|||||||
import {
|
import {
|
||||||
CancelActionButton,
|
CancelActionButton,
|
||||||
PercentageField,
|
PercentageField,
|
||||||
|
RdxBadge,
|
||||||
RdxEditorStickyFooter,
|
RdxEditorStickyFooter,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
RdxPageStack,
|
RdxPageStack,
|
||||||
RhfSubmitActionButton,
|
RhfSubmitActionButton,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import { showErrorToast } from "@repo/rdx-ui/helpers";
|
import { showErrorToast } from "@repo/rdx-ui/helpers";
|
||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -114,12 +114,12 @@ export const ProformaUpdatePage = () => {
|
|||||||
isSaving={updateCtrl.form.formState.isSubmitting}
|
isSaving={updateCtrl.form.formState.isSubmitting}
|
||||||
labels={{
|
labels={{
|
||||||
title: proforma ? (
|
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>
|
<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}
|
{proforma.proformaReference}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
<ProformaStatusBadge className="text-base" status={proforma.status} />
|
<ProformaStatusBadge status={proforma.status} />
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
t("pages.proformas.update.title", "Editar proforma")
|
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">
|
<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 ? (
|
{hasPageAlerts ? (
|
||||||
<RdxPageContainer className="pb-0" width="full">
|
<RdxPageContainer className="pb-0" width="full">
|
||||||
<RdxPageStack spacing="compact">
|
<RdxPageStack spacing="compact">
|
||||||
|
|||||||
@ -16,11 +16,12 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { useCreateRecurringProforma } from "../shared/hooks/use-create-recurring-proforma";
|
import { useCreateRecurringProforma } from "../shared/hooks/use-create-recurring-proforma";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildCreateRecurringProformaInput,
|
|
||||||
getCreatedRecurringProformaPath,
|
|
||||||
type RecurringProformaCreateForm,
|
type RecurringProformaCreateForm,
|
||||||
RecurringProformaCreateFormSchema,
|
RecurringProformaCreateFormSchema,
|
||||||
|
buildCreateRecurringProformaInput,
|
||||||
|
getCreatedRecurringProformaPath,
|
||||||
} from "./recurring-proforma-create-utils";
|
} from "./recurring-proforma-create-utils";
|
||||||
|
|
||||||
const RECURRING_PROFORMA_CREATE_FORM_ID = "recurring-proforma-create-form";
|
const RECURRING_PROFORMA_CREATE_FORM_ID = "recurring-proforma-create-form";
|
||||||
@ -77,7 +78,7 @@ export const RecurringProformaCreatePage = () => {
|
|||||||
title={t("recurring_proformas.create.title")}
|
title={t("recurring_proformas.create.title")}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
<div className="flex-1 overflow-y-auto bg-muted/20">
|
||||||
<RdxEditorPageLayout complexity="simple">
|
<RdxEditorPageLayout width="simple">
|
||||||
<form
|
<form
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
id={RECURRING_PROFORMA_CREATE_FORM_ID}
|
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 { RecurringProformaAction } from "../shared/hooks/use-recurring-proforma-action-mutation";
|
||||||
import type { RecurringProformaStatus } from "../shared/recurring-proforma-list";
|
|
||||||
|
|
||||||
export type RecurringProformaDetailActionPlacement = "primary" | "secondary" | "menu";
|
export type RecurringProformaDetailActionPlacement = "primary" | "secondary" | "menu";
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ export const getRecurringProformaDetailActions = (
|
|||||||
case "draft":
|
case "draft":
|
||||||
return [
|
return [
|
||||||
{ key: "activate", placement: "primary" },
|
{ key: "activate", placement: "primary" },
|
||||||
|
{ key: "edit", placement: "secondary" },
|
||||||
{ key: "duplicate", placement: "secondary" },
|
{ key: "duplicate", placement: "secondary" },
|
||||||
{ key: "complete", placement: "menu", terminal: true },
|
{ key: "complete", placement: "menu", terminal: true },
|
||||||
{ key: "delete", placement: "menu", terminal: true, destructive: true },
|
{ key: "delete", placement: "menu", terminal: true, destructive: true },
|
||||||
@ -30,6 +31,7 @@ export const getRecurringProformaDetailActions = (
|
|||||||
case "paused":
|
case "paused":
|
||||||
return [
|
return [
|
||||||
{ key: "resume", placement: "primary" },
|
{ key: "resume", placement: "primary" },
|
||||||
|
{ key: "edit", placement: "secondary" },
|
||||||
{ key: "duplicate", placement: "secondary" },
|
{ key: "duplicate", placement: "secondary" },
|
||||||
{ key: "complete", placement: "menu", terminal: true },
|
{ key: "complete", placement: "menu", terminal: true },
|
||||||
];
|
];
|
||||||
@ -40,5 +42,7 @@ export const getRecurringProformaDetailActions = (
|
|||||||
];
|
];
|
||||||
case "archived":
|
case "archived":
|
||||||
return [{ key: "duplicate", placement: "primary" }];
|
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",
|
name: "Mantenimiento mensual",
|
||||||
status: "active",
|
status: "active",
|
||||||
customer_id: null,
|
customer_id: null,
|
||||||
|
customer: null,
|
||||||
description: null,
|
description: null,
|
||||||
payment_term_id: null,
|
payment_term_id: null,
|
||||||
payment_method_id: null,
|
payment_method_id: null,
|
||||||
@ -27,8 +28,15 @@ test("presenta nombre, estado y recurrencia del detalle principal", () => {
|
|||||||
proforma_series: null,
|
proforma_series: null,
|
||||||
issued_invoice_series: null,
|
issued_invoice_series: null,
|
||||||
global_discount_percentage: 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,
|
next_occurrence_date: null,
|
||||||
|
has_issue: false,
|
||||||
items: [],
|
items: [],
|
||||||
created_at: "2026-09-01T10:00:00.000Z",
|
created_at: "2026-09-01T10:00:00.000Z",
|
||||||
updated_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", () => {
|
test("resuelve loading, error y contenido del detalle", () => {
|
||||||
assert.equal(getRecurringProformaDetailFeedbackState({ isLoading: true, isError: false, hasData: false }), "loading");
|
assert.equal(
|
||||||
assert.equal(getRecurringProformaDetailFeedbackState({ isLoading: false, isError: true, hasData: false }), "error");
|
getRecurringProformaDetailFeedbackState({ isLoading: true, isError: false, hasData: false }),
|
||||||
assert.equal(getRecurringProformaDetailFeedbackState({ isLoading: false, isError: false, hasData: true }), "ready");
|
"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 {
|
||||||
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
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 type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
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 { formatRecurringProformaDateOnly } from "./recurring-proforma-detail-utils";
|
||||||
import {
|
import {
|
||||||
buildGeneratedProformaPath,
|
buildGeneratedProformaPath,
|
||||||
formatRecurringProformaOccurrenceDateTime,
|
formatRecurringProformaOccurrenceDateTime,
|
||||||
getRecurringProformaOccurrencesFeedbackState,
|
|
||||||
getRecurringProformaOccurrenceStatusKey,
|
getRecurringProformaOccurrenceStatusKey,
|
||||||
|
getRecurringProformaOccurrencesFeedbackState,
|
||||||
} from "./recurring-proforma-occurrences-section-utils";
|
} 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;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
@ -38,22 +44,46 @@ export const RecurringProformaOccurrencesSection = ({
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
accessorKey: "scheduled_date",
|
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),
|
cell: ({ row }) => formatRecurringProformaDateOnly(row.original.scheduled_date, dateLocale),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
header: ({ column }) => <DataTableColumnHeader column={column} title={t("recurring_proformas.occurrences.columns.status")} />,
|
header: ({ column }) => (
|
||||||
cell: ({ row }) => <Badge variant="secondary">{t(getRecurringProformaOccurrenceStatusKey(row.original.status))}</Badge>,
|
<DataTableColumnHeader
|
||||||
|
column={column}
|
||||||
|
title={t("recurring_proformas.occurrences.columns.status")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<RdxBadge variant="secondary">
|
||||||
|
{t(getRecurringProformaOccurrenceStatusKey(row.original.status))}
|
||||||
|
</RdxBadge>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "attempt_count",
|
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>,
|
cell: ({ row }) => <span className="tabular-nums">{row.original.attempt_count}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "proforma",
|
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 }) => {
|
cell: ({ row }) => {
|
||||||
const proformaPath = buildGeneratedProformaPath(row.original.proforma_id);
|
const proformaPath = buildGeneratedProformaPath(row.original.proforma_id);
|
||||||
return proformaPath ? (
|
return proformaPath ? (
|
||||||
@ -65,16 +95,32 @@ export const RecurringProformaOccurrencesSection = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "generated_at",
|
accessorKey: "generated_at",
|
||||||
header: ({ column }) => <DataTableColumnHeader column={column} title={t("recurring_proformas.occurrences.columns.generated_at")} />,
|
header: ({ column }) => (
|
||||||
cell: ({ row }) => formatRecurringProformaOccurrenceDateTime(row.original.generated_at, dateLocale) ?? "-",
|
<DataTableColumnHeader
|
||||||
|
column={column}
|
||||||
|
title={t("recurring_proformas.occurrences.columns.generated_at")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) =>
|
||||||
|
formatRecurringProformaOccurrenceDateTime(row.original.generated_at, dateLocale) ?? "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "last_error",
|
id: "last_error",
|
||||||
header: ({ column }) => <DataTableColumnHeader column={column} title={t("recurring_proformas.occurrences.columns.last_error")} />,
|
header: ({ column }) => (
|
||||||
cell: ({ row }) => row.original.status === "failed" && row.original.last_error_message ? (
|
<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">
|
<div className="space-y-1">
|
||||||
<p className="max-w-sm text-sm text-destructive">{row.original.last_error_message}</p>
|
<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}
|
{row.original.last_error_code ? (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{row.original.last_error_code}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null,
|
) : null,
|
||||||
},
|
},
|
||||||
@ -84,9 +130,13 @@ export const RecurringProformaOccurrencesSection = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>{t("recurring_proformas.occurrences.title")}</CardTitle></CardHeader>
|
<CardHeader>
|
||||||
|
<CardTitle>{t("recurring_proformas.occurrences.title")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{feedbackState === "loading" ? <SkeletonDataTable columns={6} rows={3} showFooter={false} /> : null}
|
{feedbackState === "loading" ? (
|
||||||
|
<SkeletonDataTable columns={6} rows={3} showFooter={false} />
|
||||||
|
) : null}
|
||||||
{feedbackState === "error" ? (
|
{feedbackState === "error" ? (
|
||||||
<div className="space-y-3 rounded-md border border-destructive/30 p-4" role="alert">
|
<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>
|
<p className="text-sm text-destructive">{t("recurring_proformas.occurrences.error")}</p>
|
||||||
@ -95,7 +145,11 @@ export const RecurringProformaOccurrencesSection = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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" ? (
|
{feedbackState === "ready" ? (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -104,7 +158,10 @@ export const RecurringProformaOccurrencesSection = ({
|
|||||||
isFetching={query.isFetching}
|
isFetching={query.isFetching}
|
||||||
manualPagination
|
manualPagination
|
||||||
onPageChange={(pageIndex) => setPage(pageIndex + 1)}
|
onPageChange={(pageIndex) => setPage(pageIndex + 1)}
|
||||||
onPageSizeChange={(pageSize) => { setLimit(pageSize); setPage(1); }}
|
onPageSizeChange={(pageSize) => {
|
||||||
|
setLimit(pageSize);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
pageIndex={data.page - 1}
|
pageIndex={data.page - 1}
|
||||||
pageSize={data.per_page}
|
pageSize={data.per_page}
|
||||||
totalItems={data.total_items}
|
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 { PageErrorState } from "@erp/core/components";
|
||||||
import { RdxPageStack } from "@repo/rdx-ui/components";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
RdxBadge,
|
||||||
Button,
|
RdxEditorPageLayout,
|
||||||
Card,
|
RdxPageContainer,
|
||||||
CardContent,
|
RdxPageStack,
|
||||||
CardHeader,
|
} from "@repo/rdx-ui/components";
|
||||||
CardTitle,
|
import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import { ArrowLeftIcon } from "lucide-react";
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import { ProformaPageHeader } from "../../../../proformas/shared";
|
import { ProformaPageHeader } from "../../../../proformas/shared";
|
||||||
|
import { getRecurringProformaStatusBadgeClassName } from "../../../shared/helpers/recurring-proforma-status-ui";
|
||||||
import { useRecurringProforma } from "../../../shared/hooks/use-recurring-proforma";
|
import { useRecurringProforma } from "../../../shared/hooks/use-recurring-proforma";
|
||||||
import { useRecurringProformaActionMutation } from "../../../shared/hooks/use-recurring-proforma-action-mutation";
|
import { useRecurringProformaActionMutation } from "../../../shared/hooks/use-recurring-proforma-action-mutation";
|
||||||
import { getRecurringProformaDetailActions } from "../../recurring-proforma-detail-actions";
|
import { getRecurringProformaDetailActions } from "../../recurring-proforma-detail-actions";
|
||||||
@ -26,6 +25,7 @@ import {
|
|||||||
} from "../../recurring-proforma-detail-utils";
|
} from "../../recurring-proforma-detail-utils";
|
||||||
import { RecurringProformaOccurrencesSection } from "../../recurring-proforma-occurrences-section";
|
import { RecurringProformaOccurrencesSection } from "../../recurring-proforma-occurrences-section";
|
||||||
import { RecurringProformaDetailHeaderActions } from "../blocks/recurring-proforma-detail-header-actions";
|
import { RecurringProformaDetailHeaderActions } from "../blocks/recurring-proforma-detail-header-actions";
|
||||||
|
import { RecurringProformaOperationalSummary } from "../blocks/recurring-proforma-operational-summary";
|
||||||
|
|
||||||
type DetailFieldProps = {
|
type DetailFieldProps = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -110,21 +110,35 @@ export const RecurringProformaDetailPage = () => {
|
|||||||
backLabel={t("recurring_proformas.detail.back")}
|
backLabel={t("recurring_proformas.detail.back")}
|
||||||
onBack={() => navigate("/recurring-proformas")}
|
onBack={() => navigate("/recurring-proformas")}
|
||||||
title={
|
title={
|
||||||
|
<div className="space-y-3">
|
||||||
<span className="inline-flex max-w-full items-center gap-3">
|
<span className="inline-flex max-w-full items-center gap-3">
|
||||||
<span className="truncate">{mainView.name}</span>
|
<span className="truncate">{mainView.name}</span>
|
||||||
<Badge className="shrink-0 px-4 py-3 rounded-md" variant="secondary">
|
<RdxBadge
|
||||||
{t(mainView.statusKey)}
|
className={`shrink-0 rounded-md px-4 py-3 ${getRecurringProformaStatusBadgeClassName(recurringProforma.status)}`}
|
||||||
</Badge>
|
variant="secondary"
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="space-y-3 border-b bg-background px-4 py-4 sm:px-6">
|
{t(mainView.statusKey)}
|
||||||
<span className="text-sm text-muted-foreground">
|
</RdxBadge>
|
||||||
{t("recurring_proformas.detail.context")}
|
<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>
|
</span>
|
||||||
<p className="text-sm text-muted-foreground">{metadata}</p>
|
<p className="text-sm text-muted-foreground">{metadata}</p>
|
||||||
</div>
|
</div>
|
||||||
</ProformaPageHeader>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RdxPageContainer className="min-h-0 flex-1 overflow-y-auto" padding="none" width="wide">
|
||||||
|
<RdxEditorPageLayout width="simple">
|
||||||
|
<RecurringProformaOperationalSummary
|
||||||
|
locale={dateLocale}
|
||||||
|
recurringProforma={recurringProforma}
|
||||||
|
/>
|
||||||
|
</RdxEditorPageLayout>
|
||||||
|
|
||||||
<div className="grid gap-6 xl:grid-cols-2">
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
@ -137,12 +151,6 @@ export const RecurringProformaDetailPage = () => {
|
|||||||
{recurringProforma.description}
|
{recurringProforma.description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : 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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -214,7 +222,10 @@ export const RecurringProformaDetailPage = () => {
|
|||||||
/>
|
/>
|
||||||
<DetailField
|
<DetailField
|
||||||
label={t("recurring_proformas.detail.commercial.issued_invoice_series")}
|
label={t("recurring_proformas.detail.commercial.issued_invoice_series")}
|
||||||
value={formatRecurringProformaOverride(recurringProforma.issued_invoice_series, t)}
|
value={formatRecurringProformaOverride(
|
||||||
|
recurringProforma.issued_invoice_series,
|
||||||
|
t
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{discount ? (
|
{discount ? (
|
||||||
<DetailField
|
<DetailField
|
||||||
@ -257,6 +268,7 @@ export const RecurringProformaDetailPage = () => {
|
|||||||
<ArrowLeftIcon aria-hidden className="size-4" />
|
<ArrowLeftIcon aria-hidden className="size-4" />
|
||||||
{t("recurring_proformas.detail.back")}
|
{t("recurring_proformas.detail.back")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</RdxPageContainer>
|
||||||
</RdxPageStack>
|
</RdxPageStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export * from "./create";
|
export * from "./create";
|
||||||
export * from "./detail";
|
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>
|
</header>
|
||||||
),
|
),
|
||||||
RdxPageActions: ({ primary }: { primary: ReactNode }) => <div>{primary}</div>,
|
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>,
|
DataTableColumnHeader: ({ title }: { title: string }) => <span>{title}</span>,
|
||||||
DataTable: (props: DataTableProps) => {
|
DataTable: (props: DataTableProps) => {
|
||||||
capturedDataTableProps = props;
|
capturedDataTableProps = props;
|
||||||
return <section>{props.topContent}</section>;
|
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/);
|
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 () => {
|
test("conecta CTA, detalle, búsqueda, filtro y paginación con el controller", async () => {
|
||||||
events = {
|
events = {
|
||||||
create: false,
|
create: false,
|
||||||
|
|||||||
@ -2,11 +2,13 @@ import { PageErrorState, SimpleSearchInput } from "@erp/core/components";
|
|||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
DataTableColumnHeader,
|
DataTableColumnHeader,
|
||||||
|
RdxBadge,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
type RdxRowAction,
|
type RdxRowAction,
|
||||||
RdxRowActions,
|
RdxRowActions,
|
||||||
|
SkeletonDataTable,
|
||||||
} from "@repo/rdx-ui/components";
|
} 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 type { ColumnDef } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
@ -21,6 +23,7 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
import { getRecurringProformaStatusBadgeClassName } from "../shared/helpers/recurring-proforma-status-ui";
|
||||||
import {
|
import {
|
||||||
type RecurringProformaAction,
|
type RecurringProformaAction,
|
||||||
useRecurringProformaActionMutation,
|
useRecurringProformaActionMutation,
|
||||||
@ -161,7 +164,12 @@ export const ListRecurringProformasPage = () => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
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,12 +286,24 @@ export const ListRecurringProformasPage = () => {
|
|||||||
<ScopedListRecurringProformasPageView
|
<ScopedListRecurringProformasPageView
|
||||||
content={
|
content={
|
||||||
<div className="mx-auto w-full space-y-4">
|
<div className="mx-auto w-full space-y-4">
|
||||||
|
{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
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={listCtrl.data.items}
|
data={listCtrl.data.items}
|
||||||
enablePagination
|
enablePagination
|
||||||
isFetching={listCtrl.isFetching}
|
isFetching={listCtrl.isFetching}
|
||||||
loading={listCtrl.isLoading}
|
isSearching={isSearchPending}
|
||||||
manualPagination
|
manualPagination
|
||||||
manualSorting
|
manualSorting
|
||||||
onPageChange={listCtrl.setPageIndex}
|
onPageChange={listCtrl.setPageIndex}
|
||||||
@ -292,7 +312,6 @@ export const ListRecurringProformasPage = () => {
|
|||||||
onSortChange={listCtrl.setSort}
|
onSortChange={listCtrl.setSort}
|
||||||
pageIndex={listCtrl.pageIndex}
|
pageIndex={listCtrl.pageIndex}
|
||||||
pageSize={listCtrl.pageSize}
|
pageSize={listCtrl.pageSize}
|
||||||
searching={isSearchPending}
|
|
||||||
sort={listCtrl.sort}
|
sort={listCtrl.sort}
|
||||||
topContent={
|
topContent={
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
@ -317,6 +336,7 @@ export const ListRecurringProformasPage = () => {
|
|||||||
}
|
}
|
||||||
totalItems={listCtrl.data.totalItems}
|
totalItems={listCtrl.data.totalItems}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{listCtrl.data.totalItems === 0 ? (
|
{listCtrl.data.totalItems === 0 ? (
|
||||||
<div className="rounded border border-dashed border-border bg-muted/30 px-4 py-6 text-sm text-muted-foreground">
|
<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 { ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { CheckCircle2Icon, CirclePauseIcon, FileTextIcon, PlayCircleIcon } from "lucide-react";
|
|
||||||
import type { ElementType } from "react";
|
import type { ElementType } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
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;
|
export type RecurringProformaStatusFilter = "all" | RecurringProformaStatus;
|
||||||
|
|
||||||
@ -22,34 +25,12 @@ const RECURRING_PROFORMA_STATUS_FILTER_OPTIONS = [
|
|||||||
activeClassName:
|
activeClassName:
|
||||||
"data-[state=on]:border-primary data-[state=on]:bg-primary data-[state=on]:text-primary-foreground",
|
"data-[state=on]:border-primary data-[state=on]:bg-primary data-[state=on]:text-primary-foreground",
|
||||||
},
|
},
|
||||||
{
|
...(["draft", "active", "paused", "completed"] as const).map((value) => ({
|
||||||
value: "draft",
|
value,
|
||||||
labelKey: "recurring_proformas.status.draft",
|
labelKey: `recurring_proformas.status.${value}`,
|
||||||
icon: FileTextIcon,
|
icon: getRecurringProformaStatusIcon(value),
|
||||||
activeClassName:
|
activeClassName: getRecurringProformaStatusSegmentedFilterActiveClassName(value),
|
||||||
"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",
|
|
||||||
},
|
|
||||||
] as const satisfies readonly {
|
] as const satisfies readonly {
|
||||||
value: RecurringProformaStatusFilter;
|
value: RecurringProformaStatusFilter;
|
||||||
labelKey: string;
|
labelKey: string;
|
||||||
@ -90,7 +71,7 @@ export const RecurringProformaStatusSegmentedFilter = ({
|
|||||||
{RECURRING_PROFORMA_STATUS_FILTER_OPTIONS.map((option) => (
|
{RECURRING_PROFORMA_STATUS_FILTER_OPTIONS.map((option) => (
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className={cn(
|
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",
|
"text-muted-foreground hover:border-border hover:bg-muted/60 hover:text-foreground",
|
||||||
"data-[state=on]:shadow-none",
|
"data-[state=on]:shadow-none",
|
||||||
option.activeClassName
|
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";
|
import { recurringProformaQueryKeys } from "./recurring-proforma-query-keys";
|
||||||
|
|
||||||
export type RecurringProformaAction =
|
export type RecurringProformaAction =
|
||||||
|
| "edit"
|
||||||
| "activate"
|
| "activate"
|
||||||
| "pause"
|
| "pause"
|
||||||
| "resume"
|
| "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 = {
|
export type RecurringProformaListFilters = {
|
||||||
page: number;
|
page: number;
|
||||||
@ -86,6 +86,11 @@ export type RecurringProformaDetail = {
|
|||||||
name: string;
|
name: string;
|
||||||
status: RecurringProformaStatus;
|
status: RecurringProformaStatus;
|
||||||
customer_id: string | null;
|
customer_id: string | null;
|
||||||
|
customer: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tin: string | null;
|
||||||
|
} | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
payment_term_id: string | null;
|
payment_term_id: string | null;
|
||||||
payment_method_id: string | null;
|
payment_method_id: string | null;
|
||||||
@ -96,6 +101,7 @@ export type RecurringProformaDetail = {
|
|||||||
global_discount_percentage: RecurringProformaPercentage | null;
|
global_discount_percentage: RecurringProformaPercentage | null;
|
||||||
schedule: RecurringProformaListItemDTO["schedule"];
|
schedule: RecurringProformaListItemDTO["schedule"];
|
||||||
next_occurrence_date: string | null;
|
next_occurrence_date: string | null;
|
||||||
|
has_issue: boolean;
|
||||||
items: RecurringProformaDetailItem[];
|
items: RecurringProformaDetailItem[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export const CustomerCreatePage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
<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="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">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
{createCtrl.isCreateError ? (
|
{createCtrl.isCreateError ? (
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { InitialsAvatar } from "@repo/rdx-ui/components";
|
import { InitialsAvatar } from "@repo/rdx-ui/components";
|
||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
||||||
|
|
||||||
import type { Customer } from "../../../../shared";
|
import type { Customer } from "../../../../shared";
|
||||||
@ -21,9 +20,9 @@ export const CustomerHeader = ({ customer }: { customer: Customer }) => {
|
|||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="truncate text-lg font-semibold">{customer.name}</h3>
|
<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"}
|
{customer.status === "active" ? "Activo" : "Inactivo"}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{customer.tradeName && (
|
{customer.tradeName && (
|
||||||
@ -31,7 +30,7 @@ export const CustomerHeader = ({ customer }: { customer: Customer }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<Badge className="gap-1" variant="outline">
|
<RdxBadge className="gap-1" variant="outline">
|
||||||
{customer.isCompany ? (
|
{customer.isCompany ? (
|
||||||
<>
|
<>
|
||||||
<Building2Icon className="size-3" />
|
<Building2Icon className="size-3" />
|
||||||
@ -43,7 +42,7 @@ export const CustomerHeader = ({ customer }: { customer: Customer }) => {
|
|||||||
Particular
|
Particular
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 text-xs font-mono text-muted-foreground hover:text-foreground"
|
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 { 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";
|
import { FileTextIcon } from "lucide-react";
|
||||||
|
|
||||||
const getInvoiceStatusBadge = (status: "paid" | "pending" | "overdue") => {
|
const getInvoiceStatusBadge = (status: "paid" | "pending" | "overdue") => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "paid":
|
case "paid":
|
||||||
return (
|
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
|
Pagada
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
case "pending":
|
case "pending":
|
||||||
return (
|
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
|
Pendiente
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
case "overdue":
|
case "overdue":
|
||||||
return (
|
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
|
Vencida
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
DataTableColumnHeader,
|
DataTableColumnHeader,
|
||||||
InitialsAvatar,
|
InitialsAvatar,
|
||||||
RdxRowActions,
|
RdxBadge,
|
||||||
type RdxRowAction,
|
type RdxRowAction,
|
||||||
|
RdxRowActions,
|
||||||
} from "@repo/rdx-ui/components";
|
} 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 type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Building2Icon, EyeIcon, PencilIcon, Trash2Icon, UserIcon } from "lucide-react";
|
import { Building2Icon, EyeIcon, PencilIcon, Trash2Icon, UserIcon } from "lucide-react";
|
||||||
import * as React from "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">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<span className="font-mono">{customer.tin}</span>
|
<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 ? (
|
{customer.isCompany ? (
|
||||||
<>
|
<>
|
||||||
<Building2Icon className="size-3" />
|
<Building2Icon className="size-3" />
|
||||||
@ -109,7 +110,7 @@ export function useCustomersGridColumns(
|
|||||||
{t("fields.customer_type.options.individual")}
|
{t("fields.customer_type.options.individual")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
import { Building2Icon, User2Icon } from "lucide-react";
|
import { Building2Icon, User2Icon } from "lucide-react";
|
||||||
|
|
||||||
export const KindBadge = ({ isCompany }: { isCompany: boolean }) => (
|
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 ? <Building2Icon className="size-3.5" /> : <User2Icon className="size-3.5" />}
|
||||||
{isCompany ? "Company" : "Person"}
|
{isCompany ? "Company" : "Person"}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -93,16 +93,22 @@ export const CustomerUpdatePage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
<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="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">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
{updateCtrl.isUpdateError ? (
|
{updateCtrl.isUpdateError ? (
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
message={
|
message={
|
||||||
(updateCtrl.updateError as Error)?.message ??
|
(updateCtrl.updateError as Error)?.message ??
|
||||||
t("pages.customers.update.error_msg", "Revisa los datos e inténtalo de nuevo.")
|
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")}
|
title={t(
|
||||||
|
"pages.customers.update.error_title",
|
||||||
|
"No se pudo guardar los cambios"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@ -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 { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import {
|
import {
|
||||||
CancelActionButton,
|
CancelActionButton,
|
||||||
@ -29,7 +29,7 @@ import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
|
|||||||
const AccountLoadingState = ({ label }: { label: string }) => {
|
const AccountLoadingState = ({ label }: { label: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
<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 className="flex min-h-[16rem] items-center justify-center text-sm text-muted-foreground">
|
||||||
<div aria-busy="true" role="status">
|
<div aria-busy="true" role="status">
|
||||||
{label}
|
{label}
|
||||||
@ -63,7 +63,7 @@ const AccountErrorState = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
<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">
|
<div className="mx-auto w-full max-w-3xl">
|
||||||
<PageErrorState
|
<PageErrorState
|
||||||
description={description}
|
description={description}
|
||||||
@ -227,8 +227,8 @@ export const AccountPage = () => {
|
|||||||
isRetrying={ctrl.isRetrying}
|
isRetrying={ctrl.isRetrying}
|
||||||
onRetry={ctrl.retry}
|
onRetry={ctrl.retry}
|
||||||
reference={ctrl.pageError.reference}
|
reference={ctrl.pageError.reference}
|
||||||
retryLabel={t("account.pageError.actions.retry")}
|
|
||||||
retryingLabel={t("account.pageError.actions.retrying")}
|
retryingLabel={t("account.pageError.actions.retrying")}
|
||||||
|
retryLabel={t("account.pageError.actions.retry")}
|
||||||
title={ctrl.pageError.title}
|
title={ctrl.pageError.title}
|
||||||
variant={ctrl.pageError.variant}
|
variant={ctrl.pageError.variant}
|
||||||
/>
|
/>
|
||||||
@ -259,7 +259,7 @@ export const AccountPage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
<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="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">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
<FormProvider {...ctrl.profileForm}>
|
<FormProvider {...ctrl.profileForm}>
|
||||||
@ -311,22 +311,30 @@ export const AccountPage = () => {
|
|||||||
|
|
||||||
<SelectField<AccountProfileForm>
|
<SelectField<AccountProfileForm>
|
||||||
className="md:col-span-12"
|
className="md:col-span-12"
|
||||||
description={t("account.sections.personal.fields.languageCode.description")}
|
description={t(
|
||||||
|
"account.sections.personal.fields.languageCode.description"
|
||||||
|
)}
|
||||||
disabled={ctrl.isSavingProfile}
|
disabled={ctrl.isSavingProfile}
|
||||||
inputClassName="bg-background"
|
inputClassName="bg-background"
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
value: "es",
|
value: "es",
|
||||||
label: t("account.sections.personal.fields.languageCode.options.es"),
|
label: t(
|
||||||
|
"account.sections.personal.fields.languageCode.options.es"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "en",
|
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")}
|
label={t("account.sections.personal.fields.languageCode.label")}
|
||||||
name="languageCode"
|
name="languageCode"
|
||||||
placeholder={t("account.sections.personal.fields.languageCode.placeholder")}
|
placeholder={t(
|
||||||
|
"account.sections.personal.fields.languageCode.placeholder"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorSection>
|
</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="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="space-y-1">
|
<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">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("account.sections.security.passwordDescription")}
|
{t("account.sections.security.passwordDescription")}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export const SupplierCreatePage = () => {
|
|||||||
title={t("create.title")}
|
title={t("create.title")}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
<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="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">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
{controller.mutation.isError ? (
|
{controller.mutation.isError ? (
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
RdxRowActions,
|
RdxRowActions,
|
||||||
SkeletonDataTable,
|
SkeletonDataTable,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
import type { ColumnDef, OnChangeFn, VisibilityState } from "@tanstack/react-table";
|
import type { ColumnDef, OnChangeFn, VisibilityState } from "@tanstack/react-table";
|
||||||
import { Building2Icon, EyeIcon, MailIcon, PencilIcon, PhoneIcon, UserIcon } from "lucide-react";
|
import { Building2Icon, EyeIcon, MailIcon, PencilIcon, PhoneIcon, UserIcon } from "lucide-react";
|
||||||
import { type ReactNode, useMemo } from "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="block truncate font-medium text-foreground">{supplier.name}</span>
|
||||||
<span className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
<span className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
{supplier.tradeName || supplier.tin || "-"}
|
{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 ? (
|
{supplier.isCompany ? (
|
||||||
<Building2Icon aria-hidden className="size-3" />
|
<Building2Icon aria-hidden className="size-3" />
|
||||||
) : (
|
) : (
|
||||||
<UserIcon aria-hidden className="size-3" />
|
<UserIcon aria-hidden className="size-3" />
|
||||||
)}
|
)}
|
||||||
{t(supplier.isCompany ? "fields.company" : "fields.individual")}
|
{t(supplier.isCompany ? "fields.company" : "fields.individual")}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { Badge } from "@repo/shadcn-ui/components";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
export const SupplierStatusBadge = ({ status }: { status: string }) => {
|
export const SupplierStatusBadge = ({ status }: { status: string }) => {
|
||||||
@ -7,8 +5,8 @@ export const SupplierStatusBadge = ({ status }: { status: string }) => {
|
|||||||
const isActive = status.toLowerCase() === "active";
|
const isActive = status.toLowerCase() === "active";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge variant={isActive ? "default" : "secondary"}>
|
<RdxBadge variant={isActive ? "default" : "secondary"}>
|
||||||
{t(isActive ? "status.active" : "status.inactive")}
|
{t(isActive ? "status.active" : "status.inactive")}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export const SupplierUpdatePage = () => {
|
|||||||
title={t("update.title")}
|
title={t("update.title")}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
<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="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">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
{controller.mutation.isError ? (
|
{controller.mutation.isError ? (
|
||||||
|
|||||||
@ -46,6 +46,7 @@
|
|||||||
"vite-tsconfig-paths": "^6.1.1"
|
"vite-tsconfig-paths": "^6.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.3.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@ -18,6 +17,8 @@ import type { Column } from "@tanstack/react-table";
|
|||||||
import { Check, PlusCircleIcon } from "lucide-react";
|
import { Check, PlusCircleIcon } from "lucide-react";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
|
|
||||||
|
import { RdxBadge } from "../rdx-badge.tsx";
|
||||||
|
|
||||||
interface DataTableFacetedFilterProps<TData, TValue> {
|
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||||
column?: Column<TData, TValue>;
|
column?: Column<TData, TValue>;
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -45,25 +46,25 @@ export function DataTableFacetedFilter<TData, TValue>({
|
|||||||
{selectedValues?.size > 0 && (
|
{selectedValues?.size > 0 && (
|
||||||
<>
|
<>
|
||||||
<Separator className="mx-2 h-4" orientation="vertical" />
|
<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}
|
{selectedValues.size}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
<div className="hidden gap-1 lg:flex">
|
<div className="hidden gap-1 lg:flex">
|
||||||
{selectedValues.size > 2 ? (
|
{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
|
{selectedValues.size} selected
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
) : (
|
) : (
|
||||||
options
|
options
|
||||||
.filter((option) => selectedValues.has(option.value))
|
.filter((option) => selectedValues.has(option.value))
|
||||||
.map((option) => (
|
.map((option) => (
|
||||||
<Badge
|
<RdxBadge
|
||||||
className="rounded-sm px-1 font-normal"
|
className="rounded-sm px-1 font-normal"
|
||||||
key={option.value}
|
key={option.value}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -24,7 +24,7 @@ test("RdxEditorPageLayout aplica complejidad simple por defecto y renderiza chil
|
|||||||
|
|
||||||
test("RdxEditorPageLayout soporta aside opcional y complejidad complex", () => {
|
test("RdxEditorPageLayout soporta aside opcional y complejidad complex", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<RdxEditorPageLayout aside={<div>Aside content</div>} complexity="complex">
|
<RdxEditorPageLayout aside={<div>Aside content</div>} width="full">
|
||||||
<div>Main content</div>
|
<div>Main content</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorPageLayout>
|
||||||
);
|
);
|
||||||
@ -37,7 +37,7 @@ test("RdxEditorPageLayout soporta aside opcional y complejidad complex", () => {
|
|||||||
|
|
||||||
test("RdxEditorPageLayout no reserva aside cuando no existe incluso en complex", () => {
|
test("RdxEditorPageLayout no reserva aside cuando no existe incluso en complex", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<RdxEditorPageLayout complexity="complex">
|
<RdxEditorPageLayout width="full">
|
||||||
<div>Main content</div>
|
<div>Main content</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorPageLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,38 +1,41 @@
|
|||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||||
|
|
||||||
export type RdxEditorComplexity = "simple" | "complex";
|
export type RdxEditorWidth = "wide" | "full";
|
||||||
|
|
||||||
export interface RdxEditorPageLayoutProps
|
export interface RdxEditorPageLayoutProps
|
||||||
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
aside?: ReactNode;
|
aside?: ReactNode;
|
||||||
complexity?: RdxEditorComplexity;
|
width?: RdxEditorWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTENT_WIDTH_CLASSNAME: Record<RdxEditorComplexity, string> = {
|
const CONTENT_WIDTH_CLASSNAME: Record<RdxEditorWidth, string> = {
|
||||||
simple: "mx-auto w-full max-w-5xl",
|
wide: "mx-auto w-full max-w-5xl",
|
||||||
complex: "w-full",
|
full: "w-full",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ASIDE_GRID_CLASSNAME: Record<RdxEditorComplexity, string> = {
|
const ASIDE_GRID_CLASSNAME: Record<RdxEditorWidth, string> = {
|
||||||
simple: "xl:grid-cols-[minmax(0,1fr)_280px]",
|
wide: "xl:grid-cols-[minmax(0,1fr)_280px]",
|
||||||
complex: "xl:grid-cols-[minmax(0,1fr)_22rem]",
|
full: "xl:grid-cols-[minmax(0,1fr)_22rem]",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RdxEditorPageLayout = ({
|
export const RdxEditorPageLayout = ({
|
||||||
children,
|
children,
|
||||||
aside,
|
aside,
|
||||||
complexity = "simple",
|
width = "wide",
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: RdxEditorPageLayoutProps) => {
|
}: RdxEditorPageLayoutProps) => {
|
||||||
const contentClassName = CONTENT_WIDTH_CLASSNAME[complexity];
|
const contentClassName = CONTENT_WIDTH_CLASSNAME[width];
|
||||||
|
|
||||||
return (
|
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 ? (
|
{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>
|
<div className="min-w-0">{children}</div>
|
||||||
<aside className="min-w-0">{aside}</aside>
|
<aside className="min-w-0">{aside}</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@ -24,6 +23,7 @@ import * as React from "react";
|
|||||||
import { type FieldPath, type FieldValues, useController, useFormContext } from "react-hook-form";
|
import { type FieldPath, type FieldValues, useController, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../locales/i18n.ts";
|
import { useTranslation } from "../../locales/i18n.ts";
|
||||||
|
import { RdxBadge } from "../rdx-badge.tsx";
|
||||||
|
|
||||||
import { FormFieldLabel } from "./form-field-label.tsx";
|
import { FormFieldLabel } from "./form-field-label.tsx";
|
||||||
|
|
||||||
@ -182,20 +182,22 @@ export const MultiSelectField = <T extends FieldValues>({
|
|||||||
const Icon = option?.icon;
|
const Icon = option?.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<RdxBadge
|
||||||
className={cn(multiSelectFieldVariants({ variant }), badgeClassName)}
|
className={cn(multiSelectFieldVariants({ variant }), badgeClassName)}
|
||||||
key={value}
|
key={value}
|
||||||
>
|
>
|
||||||
{Icon ? <Icon className="mr-2 h-4 w-4" /> : null}
|
{Icon ? <Icon className="mr-2 h-4 w-4" /> : null}
|
||||||
{option?.label}
|
{option?.label}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{selectedValues.length > maxCount ? (
|
{selectedValues.length > maxCount ? (
|
||||||
<Badge className={cn(multiSelectFieldVariants({ variant }), badgeClassName)}>
|
<RdxBadge
|
||||||
|
className={cn(multiSelectFieldVariants({ variant }), badgeClassName)}
|
||||||
|
>
|
||||||
+{selectedValues.length - maxCount}
|
+{selectedValues.length - maxCount}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export * from "./logo-verifactu.tsx";
|
|||||||
export * from "./lookup-dialog/index.ts";
|
export * from "./lookup-dialog/index.ts";
|
||||||
export * from "./multi-select.tsx";
|
export * from "./multi-select.tsx";
|
||||||
export * from "./pdf-viewer.tsx";
|
export * from "./pdf-viewer.tsx";
|
||||||
|
export * from "./rdx-badge.tsx";
|
||||||
export * from "./right-panel/index.ts";
|
export * from "./right-panel/index.ts";
|
||||||
export * from "./row-actions/index.ts";
|
export * from "./row-actions/index.ts";
|
||||||
export * from "./scroll-to-top.tsx";
|
export * from "./scroll-to-top.tsx";
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@repo/shadcn-ui/components/sidebar";
|
} 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({
|
export function NavUser({
|
||||||
user,
|
user,
|
||||||
@ -79,7 +79,7 @@ export function NavUser({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<BadgeCheck />
|
<RdxBadgeCheck />
|
||||||
Account
|
Account
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
|
|||||||
@ -4,8 +4,7 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|||||||
export type RdxPageWidth = "full" | "wide" | "content";
|
export type RdxPageWidth = "full" | "wide" | "content";
|
||||||
export type RdxPageContainerPadding = "default" | "none";
|
export type RdxPageContainerPadding = "default" | "none";
|
||||||
|
|
||||||
export interface RdxPageContainerProps
|
export interface RdxPageContainerProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||||
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
width?: RdxPageWidth;
|
width?: RdxPageWidth;
|
||||||
padding?: RdxPageContainerPadding;
|
padding?: RdxPageContainerPadding;
|
||||||
@ -31,7 +30,12 @@ export const RdxPageContainer = ({
|
|||||||
}: RdxPageContainerProps) => {
|
}: RdxPageContainerProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{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 { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { ArrowLeftIcon } from "lucide-react";
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { RdxBadge } from "../rdx-badge.tsx";
|
||||||
|
|
||||||
export interface RdxPageHeaderProps
|
export interface RdxPageHeaderProps
|
||||||
extends Omit<ComponentPropsWithoutRef<"header">, "children" | "title"> {
|
extends Omit<ComponentPropsWithoutRef<"header">, "children" | "title"> {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
@ -72,14 +74,14 @@ export const RdxPageHeader = ({
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{statusLabel ? (
|
{statusLabel ? (
|
||||||
<Badge
|
<RdxBadge
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
|
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
|
||||||
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
|
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{statusLabel}
|
{statusLabel}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@ -20,6 +19,8 @@ import * as React from "react";
|
|||||||
|
|
||||||
import { useTranslation } from "../locales/i18n.ts";
|
import { useTranslation } from "../locales/i18n.ts";
|
||||||
|
|
||||||
|
import { RdxBadge } from "./rdx-badge.tsx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variants for the multi-select component to handle different styles.
|
* Variants for the multi-select component to handle different styles.
|
||||||
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
|
* 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 option = options.find((o) => o.value === value);
|
||||||
const IconComponent = option?.icon;
|
const IconComponent = option?.icon;
|
||||||
return (
|
return (
|
||||||
<Badge
|
<RdxBadge
|
||||||
className={cn(
|
className={cn(
|
||||||
isAnimating ? "animate-bounce" : "",
|
isAnimating ? "animate-bounce" : "",
|
||||||
multiSelectVariants({ variant })
|
multiSelectVariants({ variant })
|
||||||
@ -272,11 +273,11 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|||||||
toggleOption(value);
|
toggleOption(value);
|
||||||
}}
|
}}
|
||||||
/>*/}
|
/>*/}
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{selectedValues.length > maxCount && (
|
{selectedValues.length > maxCount && (
|
||||||
<Badge
|
<RdxBadge
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
||||||
isAnimating ? "animate-bounce" : "",
|
isAnimating ? "animate-bounce" : "",
|
||||||
@ -292,7 +293,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|||||||
clearExtraOptions();
|
clearExtraOptions();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Badge>
|
</RdxBadge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<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 { mergeProps } from "@base-ui/react/merge-props";
|
||||||
import { useRender } from "@base-ui/react/use-render"
|
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 { type VariantProps, cva } from "class-variance-authority";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils"
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
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!",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
secondary:
|
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
||||||
destructive:
|
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",
|
"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:
|
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||||
"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",
|
||||||
ghost:
|
|
||||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -25,7 +21,7 @@ const badgeVariants = cva(
|
|||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
function Badge({
|
function Badge({
|
||||||
className,
|
className,
|
||||||
@ -46,7 +42,7 @@ function Badge({
|
|||||||
slot: "badge",
|
slot: "badge",
|
||||||
variant,
|
variant,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
export { Badge, badgeVariants };
|
||||||
|
|||||||
@ -1468,6 +1468,9 @@ importers:
|
|||||||
|
|
||||||
packages/rdx-ui:
|
packages/rdx-ui:
|
||||||
dependencies:
|
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':
|
'@dnd-kit/core':
|
||||||
specifier: ^6.3.1
|
specifier: ^6.3.1
|
||||||
version: 6.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
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