Editor de proformas
This commit is contained in:
parent
834d90f60c
commit
6e83958e30
@ -1,9 +1,11 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
import { AppMain } from "./app-main";
|
||||||
|
|
||||||
export const AppFullscreenLayout = () => {
|
export const AppFullscreenLayout = () => {
|
||||||
return (
|
return (
|
||||||
<main className="h-dvh w-dvw overflow-hidden bg-background">
|
<AppMain className="h-dvh w-dvw">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</AppMain>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,7 +32,12 @@ export const PageFormHeader = ({
|
|||||||
contentClassName,
|
contentClassName,
|
||||||
}: PageFormHeaderProps) => {
|
}: PageFormHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<header className={cn("sticky top-0 z-50 border-b bg-background", className)}>
|
<header
|
||||||
|
className={cn(
|
||||||
|
"sticky top-0 z-50 ring-1 ring-foreground/10 bg-background shadow-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-14 items-center justify-between gap-6 border-b px-4 py-2 sm:px-6 md:h-18",
|
"flex h-14 items-center justify-between gap-6 border-b px-4 py-2 sm:px-6 md:h-18",
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export const PageHeader = ({
|
|||||||
|
|
||||||
<div className="min-w-0 space-y-2">
|
<div className="min-w-0 space-y-2">
|
||||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<h1 className="min-w-0 truncate text-xl font-bold tracking-tight text-foreground lg:text-2xl">
|
<h1 className="min-w-0 truncate text-2xl font-semibold tracking-tight text-foreground xl:text-3xl font-heading">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { isNullishOrEmpty } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { ProformaItemUpdateForm, ProformaLineInput } from "../entities";
|
import type { ProformaItemUpdateForm, ProformaLineInput } from "../entities";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,16 +18,19 @@ export const mapProformaItemFormToProformaLineInputs = (
|
|||||||
items: ProformaItemUpdateForm[],
|
items: ProformaItemUpdateForm[],
|
||||||
params: MapProformaItemFormToProformaLineInputsParams
|
params: MapProformaItemFormToProformaLineInputsParams
|
||||||
): ProformaLineInput[] => {
|
): ProformaLineInput[] => {
|
||||||
return items
|
return (
|
||||||
.filter((item) => item.isValued)
|
items
|
||||||
.map((item) => ({
|
// quitar tuplas no valoradas
|
||||||
quantity: item.quantity,
|
.filter((item) => !(isNullishOrEmpty(item.quantity) || isNullishOrEmpty(item.unitAmount)))
|
||||||
unitAmount: item.unitAmount,
|
.map((item) => ({
|
||||||
|
quantity: item.quantity,
|
||||||
|
unitAmount: item.unitAmount,
|
||||||
|
|
||||||
itemDiscountPercentage: item.itemDiscountPercentage,
|
itemDiscountPercentage: item.itemDiscountPercentage,
|
||||||
globalDiscountPercentage: params.globalDiscountPercentage,
|
globalDiscountPercentage: params.globalDiscountPercentage,
|
||||||
|
|
||||||
taxPercentage: item.taxPercentage,
|
taxPercentage: item.taxPercentage,
|
||||||
recPercentage: item.recPercentage,
|
recPercentage: item.recPercentage,
|
||||||
}));
|
}))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export const mapProformaItemsToProformaItemsUpdateForm = (
|
|||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
position: item.position,
|
position: item.position,
|
||||||
isValued: item.isValued,
|
//isValued: item.isValued, <- campo calculado en frontend, no se mapea desde la API
|
||||||
|
|
||||||
description: item.description ?? null,
|
description: item.description ?? null,
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,8 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
|
|||||||
taxMode: fiscalDefaults.taxMode,
|
taxMode: fiscalDefaults.taxMode,
|
||||||
taxRegimeCode: "01", //taxRegimeCode: proforma.taxRegimeCode ?? proformaDefaults.taxRegimeCode, // TODO: implementar en API
|
taxRegimeCode: "01", //taxRegimeCode: proforma.taxRegimeCode ?? proformaDefaults.taxRegimeCode, // TODO: implementar en API
|
||||||
|
|
||||||
hasTaxPercentage: fiscalDefaults.defaultTaxPercentage !== null,
|
hasTaxPercentage:
|
||||||
|
fiscalDefaults.taxMode === "single" && fiscalDefaults.defaultTaxPercentage !== null,
|
||||||
taxPercentage: fiscalDefaults.defaultTaxPercentage,
|
taxPercentage: fiscalDefaults.defaultTaxPercentage,
|
||||||
|
|
||||||
hasRecPercentage: fiscalDefaults.defaultRecPercentage !== null,
|
hasRecPercentage: fiscalDefaults.defaultRecPercentage !== null,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { isNullishOrEmpty } from "@repo/rdx-utils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
type FieldArrayWithId,
|
type FieldArrayWithId,
|
||||||
@ -16,6 +17,8 @@ import { calculateProformaLinesTotals } from "../utils/calculations/calculate-pr
|
|||||||
export interface ProformaItemAmounts {
|
export interface ProformaItemAmounts {
|
||||||
subtotal: number;
|
subtotal: number;
|
||||||
itemDiscountAmount: number;
|
itemDiscountAmount: number;
|
||||||
|
subtotalTaxable: number;
|
||||||
|
taxAmount: number;
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +38,8 @@ export interface UseUpdateProformaItemsControllerResult {
|
|||||||
hasItems: boolean;
|
hasItems: boolean;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
|
|
||||||
|
getItemIsValued: (index: number) => boolean;
|
||||||
|
|
||||||
itemErrors: ProformaItemError[];
|
itemErrors: ProformaItemError[];
|
||||||
getItemError: (index: number) => ProformaItemError | undefined;
|
getItemError: (index: number) => ProformaItemError | undefined;
|
||||||
getItemErrorMessage: (index: number) => string | undefined;
|
getItemErrorMessage: (index: number) => string | undefined;
|
||||||
@ -96,9 +101,14 @@ export const useUpdateProformaItemsController = ({
|
|||||||
const appendItem = React.useCallback(() => {
|
const appendItem = React.useCallback(() => {
|
||||||
const nextPosition = getValues("items")?.length ?? 0;
|
const nextPosition = getValues("items")?.length ?? 0;
|
||||||
|
|
||||||
append(buildProformaItemUpdateDefault(nextPosition), {
|
append(
|
||||||
shouldFocus: false,
|
buildProformaItemUpdateDefault(nextPosition, {
|
||||||
});
|
defaultTaxPercentage: getValues("taxPercentage") ?? undefined,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
shouldFocus: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
}, [append, getValues]);
|
}, [append, getValues]);
|
||||||
|
|
||||||
const removeItem = React.useCallback(
|
const removeItem = React.useCallback(
|
||||||
@ -161,23 +171,6 @@ export const useUpdateProformaItemsController = ({
|
|||||||
[getValues, replaceItems]
|
[getValues, replaceItems]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getItemAmounts = React.useCallback(
|
|
||||||
(index: number): ProformaItemAmounts => {
|
|
||||||
const line = mapProformaItemFormToProformaLineInputs([items[index]], {
|
|
||||||
globalDiscountPercentage: null,
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
const amounts = calculateProformaLineTotal(line);
|
|
||||||
|
|
||||||
return {
|
|
||||||
subtotal: amounts.subtotalBeforeDiscounts,
|
|
||||||
itemDiscountAmount: amounts.itemDiscountAmount,
|
|
||||||
total: amounts.subtotalBeforeGlobalDiscount,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[items]
|
|
||||||
);
|
|
||||||
|
|
||||||
const insertItemAt = React.useCallback(
|
const insertItemAt = React.useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const currentItems = getValues("items") ?? [];
|
const currentItems = getValues("items") ?? [];
|
||||||
@ -223,6 +216,19 @@ export const useUpdateProformaItemsController = ({
|
|||||||
};
|
};
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
|
const itemsIsValued = React.useMemo(() => {
|
||||||
|
return items.map((item, _) => {
|
||||||
|
return !(isNullishOrEmpty(item.quantity) || isNullishOrEmpty(item.unitAmount));
|
||||||
|
});
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const getItemIsValued = React.useCallback(
|
||||||
|
(index: number): boolean => {
|
||||||
|
return itemsIsValued[index] ?? false;
|
||||||
|
},
|
||||||
|
[itemsIsValued]
|
||||||
|
);
|
||||||
|
|
||||||
const itemErrors = React.useMemo<ProformaItemError[]>(() => {
|
const itemErrors = React.useMemo<ProformaItemError[]>(() => {
|
||||||
if (!Array.isArray(errors.items)) return [];
|
if (!Array.isArray(errors.items)) return [];
|
||||||
|
|
||||||
@ -241,7 +247,6 @@ export const useUpdateProformaItemsController = ({
|
|||||||
const error = itemErrors[index];
|
const error = itemErrors[index];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
error?.isValued?.message ??
|
|
||||||
error?.quantity?.message ??
|
error?.quantity?.message ??
|
||||||
error?.unitAmount?.message ??
|
error?.unitAmount?.message ??
|
||||||
error?.description?.message ??
|
error?.description?.message ??
|
||||||
@ -251,6 +256,25 @@ export const useUpdateProformaItemsController = ({
|
|||||||
[itemErrors]
|
[itemErrors]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getItemAmounts = React.useCallback(
|
||||||
|
(index: number): ProformaItemAmounts => {
|
||||||
|
const line = mapProformaItemFormToProformaLineInputs([items[index]], {
|
||||||
|
globalDiscountPercentage: null,
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
const amounts = calculateProformaLineTotal(line);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subtotal: amounts.subtotalBeforeDiscounts,
|
||||||
|
itemDiscountAmount: amounts.itemDiscountAmount,
|
||||||
|
subtotalTaxable: amounts.subtotalTaxable,
|
||||||
|
taxAmount: amounts.taxAmount,
|
||||||
|
total: amounts.totalAmount,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fields,
|
fields,
|
||||||
items,
|
items,
|
||||||
@ -258,6 +282,8 @@ export const useUpdateProformaItemsController = ({
|
|||||||
hasItems: items.length > 0,
|
hasItems: items.length > 0,
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
|
|
||||||
|
getItemIsValued,
|
||||||
|
|
||||||
itemErrors,
|
itemErrors,
|
||||||
getItemError,
|
getItemError,
|
||||||
getItemErrorMessage,
|
getItemErrorMessage,
|
||||||
|
|||||||
@ -98,6 +98,8 @@ export const useUpdateProformaTaxController = ({
|
|||||||
}, [taxPercentage, getValues, hasRecPercentage, setValue, taxMode]);
|
}, [taxPercentage, getValues, hasRecPercentage, setValue, taxMode]);
|
||||||
|
|
||||||
const enablePerLineTaxes = useCallback(() => {
|
const enablePerLineTaxes = useCallback(() => {
|
||||||
|
// Activar impuestos por línea
|
||||||
|
|
||||||
setValue("taxMode", "perLine", {
|
setValue("taxMode", "perLine", {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
shouldTouch: true,
|
shouldTouch: true,
|
||||||
@ -106,6 +108,8 @@ export const useUpdateProformaTaxController = ({
|
|||||||
}, [setValue]);
|
}, [setValue]);
|
||||||
|
|
||||||
const disablePerLineTaxes = useCallback(() => {
|
const disablePerLineTaxes = useCallback(() => {
|
||||||
|
// Desactivar impuestos por línea
|
||||||
|
|
||||||
setValue("taxMode", "single", {
|
setValue("taxMode", "single", {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
shouldTouch: true,
|
shouldTouch: true,
|
||||||
|
|||||||
@ -15,6 +15,9 @@ export interface ProformaLineTotal {
|
|||||||
subtotalBeforeGlobalDiscount: number;
|
subtotalBeforeGlobalDiscount: number;
|
||||||
globalDiscountAmount: number;
|
globalDiscountAmount: number;
|
||||||
totalDiscountAmount: number;
|
totalDiscountAmount: number;
|
||||||
|
subtotalTaxable: number;
|
||||||
|
taxAmount: number;
|
||||||
|
totalAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProformaTaxBreakdown {
|
export interface ProformaTaxBreakdown {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export interface ProformaItemUpdateForm {
|
export interface ProformaItemUpdateForm {
|
||||||
id: string;
|
id: string;
|
||||||
position: number;
|
position: number;
|
||||||
isValued: boolean;
|
//isValued: boolean; <- campo calculado en frontend, no se mapea desde la API
|
||||||
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { z } from "zod/v4";
|
|||||||
export const ProformaItemUpdateFormSchema = z.object({
|
export const ProformaItemUpdateFormSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
position: z.number(),
|
position: z.number(),
|
||||||
isValued: z.boolean(),
|
//isValued: z.boolean(), <- campo calculado en frontend, no se mapea desde la API
|
||||||
|
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
|
|
||||||
|
|||||||
@ -63,6 +63,7 @@ export interface LineEditorProps<TLine> {
|
|||||||
moveDownLabel: string;
|
moveDownLabel: string;
|
||||||
removeLabel: string;
|
removeLabel: string;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +97,8 @@ export const LineEditor = <TLine,>({
|
|||||||
moveDownLabel,
|
moveDownLabel,
|
||||||
removeLabel,
|
removeLabel,
|
||||||
|
|
||||||
|
disabled = false,
|
||||||
|
|
||||||
className,
|
className,
|
||||||
}: LineEditorProps<TLine>) => {
|
}: LineEditorProps<TLine>) => {
|
||||||
return (
|
return (
|
||||||
@ -104,12 +107,24 @@ export const LineEditor = <TLine,>({
|
|||||||
<h3 className="text-lg font-semibold">{title}</h3>
|
<h3 className="text-lg font-semibold">{title}</h3>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={onAddAtStart} size="sm" type="button" variant="outline">
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onAddAtStart}
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
<Plus className="mr-1 h-4 w-4" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
{addAtStartLabel}
|
{addAtStartLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={onAddAtEnd} size="sm" type="button" variant="default">
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onAddAtEnd}
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
<Plus className="mr-1 h-4 w-4" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
{addAtEndLabel}
|
{addAtEndLabel}
|
||||||
</Button>
|
</Button>
|
||||||
@ -132,7 +147,7 @@ export const LineEditor = <TLine,>({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<TableHead className="w-[50px] font-semibold text-muted-foreground text-center">
|
<TableHead className="w-[50px] font-semibold text-muted-foreground text-center">
|
||||||
<MoreHorizontalIcon className="size-4 text-muted-foreground mx-auto" />
|
Acciones
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@ -162,13 +177,13 @@ export const LineEditor = <TLine,>({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<TableCell className="text-muted-foreground text-sm font-medium align-top">
|
<TableCell className="text-muted-foreground text-sm font-medium align-top text-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu disabled={disabled}>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
aria-label={actionsLabel}
|
aria-label={actionsLabel}
|
||||||
className="size-8 opacity-0 group-hover:opacity-100 focus:opacity-100"
|
className="size-8"
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@ -24,6 +24,7 @@ interface ProformaLineEditorProps {
|
|||||||
|
|
||||||
showLineTaxes: boolean;
|
showLineTaxes: boolean;
|
||||||
|
|
||||||
|
getItemIsValued: (index: number) => boolean;
|
||||||
getItemAmounts: (index: number) => ProformaItemAmounts;
|
getItemAmounts: (index: number) => ProformaItemAmounts;
|
||||||
getItemErrorMessage: (index: number) => string | undefined;
|
getItemErrorMessage: (index: number) => string | undefined;
|
||||||
|
|
||||||
@ -40,15 +41,16 @@ interface ProformaLineEditorProps {
|
|||||||
removeItem: (index: number) => void;
|
removeItem: (index: number) => void;
|
||||||
|
|
||||||
currency?: string;
|
currency?: string;
|
||||||
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
2;
|
|
||||||
|
|
||||||
export const ProformaLineEditor = ({
|
export const ProformaLineEditor = ({
|
||||||
fields,
|
fields,
|
||||||
|
|
||||||
showLineTaxes,
|
showLineTaxes,
|
||||||
|
|
||||||
|
getItemIsValued,
|
||||||
getItemAmounts,
|
getItemAmounts,
|
||||||
getItemErrorMessage,
|
getItemErrorMessage,
|
||||||
|
|
||||||
@ -65,6 +67,7 @@ export const ProformaLineEditor = ({
|
|||||||
removeItem,
|
removeItem,
|
||||||
|
|
||||||
currency = "EUR",
|
currency = "EUR",
|
||||||
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
}: ProformaLineEditorProps) => {
|
}: ProformaLineEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -109,6 +112,22 @@ export const ProformaLineEditor = ({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "subtotal",
|
||||||
|
header: t("form_fields.items.subtotal.label", "Subtotal"),
|
||||||
|
headClassName: "text-right",
|
||||||
|
className: "text-right font-semibold tabular-nums pt-4",
|
||||||
|
//cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency),
|
||||||
|
cell: ({ index }) => {
|
||||||
|
const isValued = getItemIsValued(index);
|
||||||
|
if (!isValued) return "";
|
||||||
|
|
||||||
|
const { subtotal } = getItemAmounts(index);
|
||||||
|
return MoneyHelper.formatCurrency(subtotal, 2, currency);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "itemDiscountPercentage",
|
id: "itemDiscountPercentage",
|
||||||
header: t("form_fields.items.discount_percentage.label", "Dto (%)"),
|
header: t("form_fields.items.discount_percentage.label", "Dto (%)"),
|
||||||
@ -123,6 +142,21 @@ export const ProformaLineEditor = ({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "itemDiscountAmount",
|
||||||
|
header: t("form_fields.items.itemDiscountAmount.label", "Imp. dto."),
|
||||||
|
headClassName: "text-right",
|
||||||
|
className: "text-right font-semibold tabular-nums pt-4",
|
||||||
|
//cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency),
|
||||||
|
cell: ({ index }) => {
|
||||||
|
const isValued = getItemIsValued(index);
|
||||||
|
if (!isValued) return "";
|
||||||
|
|
||||||
|
const { itemDiscountAmount } = getItemAmounts(index);
|
||||||
|
return MoneyHelper.formatCurrency(itemDiscountAmount, 2, currency);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
...(showLineTaxes
|
...(showLineTaxes
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@ -142,13 +176,74 @@ export const ProformaLineEditor = ({
|
|||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|
||||||
{
|
...(showLineTaxes
|
||||||
id: "total",
|
? [
|
||||||
header: t("form_fields.items.total.label", "Total"),
|
{
|
||||||
headClassName: "w-[200px] text-right",
|
id: "taxAmount",
|
||||||
className: "text-right font-semibold tabular-nums pt-4",
|
header: t("form_fields.items.total.label", "Imp. IVA"),
|
||||||
cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency),
|
headClassName: "w-[200px] text-right",
|
||||||
},
|
className: "text-right font-semibold tabular-nums pt-4",
|
||||||
|
cell: ({ index }: { index: number }) => {
|
||||||
|
const isValued = getItemIsValued(index);
|
||||||
|
if (!isValued) return "";
|
||||||
|
|
||||||
|
const { taxAmount } = getItemAmounts(index);
|
||||||
|
return MoneyHelper.formatCurrency(taxAmount);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
|
/*{
|
||||||
|
id: "isValued",
|
||||||
|
header: t("form_fields.items.is_valued.label", "Val."),
|
||||||
|
headClassName: "text-center",
|
||||||
|
className: "w-[60px] text-center",
|
||||||
|
cell: ({ index }) => {
|
||||||
|
const isValued = getItemIsValued(index);
|
||||||
|
return isValued ? (
|
||||||
|
<span className="size-5 rounded-full bg-green-500" />
|
||||||
|
) : (
|
||||||
|
<span className="size-5 rounded-full bg-muted-foreground/30" />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},*/
|
||||||
|
|
||||||
|
...(showLineTaxes
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "total",
|
||||||
|
header: t("form_fields.items.total.label", "Total"),
|
||||||
|
headClassName: "w-[200px] text-right",
|
||||||
|
className: "text-right font-semibold tabular-nums pt-4",
|
||||||
|
cell: ({ index }: { index: number }) => {
|
||||||
|
const isValued = getItemIsValued(index);
|
||||||
|
if (!isValued) return "";
|
||||||
|
|
||||||
|
const { total } = getItemAmounts(index);
|
||||||
|
return MoneyHelper.formatCurrency(total, 2, currency);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
|
...(showLineTaxes
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: "subtotalTaxable",
|
||||||
|
header: t("form_fields.items.total.label", "Total"),
|
||||||
|
headClassName: "w-[200px] text-right",
|
||||||
|
className: "text-right font-semibold tabular-nums pt-4",
|
||||||
|
cell: ({ index }: { index: number }) => {
|
||||||
|
const isValued = getItemIsValued(index);
|
||||||
|
if (!isValued) return "";
|
||||||
|
|
||||||
|
const { subtotalTaxable } = getItemAmounts(index);
|
||||||
|
return MoneyHelper.formatCurrency(subtotalTaxable, 2, currency);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -158,6 +253,7 @@ export const ProformaLineEditor = ({
|
|||||||
addAtStartLabel={t("common.add_at_start", "Al inicio")}
|
addAtStartLabel={t("common.add_at_start", "Al inicio")}
|
||||||
className={className}
|
className={className}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
disabled={disabled}
|
||||||
duplicateLabel={t("common.duplicate", "Duplicar")}
|
duplicateLabel={t("common.duplicate", "Duplicar")}
|
||||||
getLineErrorMessage={(_, index) => {
|
getLineErrorMessage={(_, index) => {
|
||||||
const message = getItemErrorMessage(index);
|
const message = getItemErrorMessage(index);
|
||||||
@ -179,7 +275,12 @@ export const ProformaLineEditor = ({
|
|||||||
onRemove={removeItem}
|
onRemove={removeItem}
|
||||||
removeLabel={t("common.remove", "Eliminar")}
|
removeLabel={t("common.remove", "Eliminar")}
|
||||||
renderFooter={() => (
|
renderFooter={() => (
|
||||||
<ProformaLineFooterEditor currency={currency} itemsTotals={itemsTotals} totals={totals} />
|
<ProformaLineFooterEditor
|
||||||
|
currency={currency}
|
||||||
|
disabled={disabled}
|
||||||
|
itemsTotals={itemsTotals}
|
||||||
|
totals={totals}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
title={t("form_fields.items.title", "Líneas de detalle")}
|
title={t("form_fields.items.title", "Líneas de detalle")}
|
||||||
/>
|
/>
|
||||||
@ -188,13 +289,14 @@ export const ProformaLineEditor = ({
|
|||||||
|
|
||||||
type ProformaLineFooterEditorProps = Pick<
|
type ProformaLineFooterEditorProps = Pick<
|
||||||
ProformaLineEditorProps,
|
ProformaLineEditorProps,
|
||||||
"totals" | "itemsTotals" | "currency"
|
"totals" | "itemsTotals" | "currency" | "disabled"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const ProformaLineFooterEditor = ({
|
export const ProformaLineFooterEditor = ({
|
||||||
totals,
|
totals,
|
||||||
itemsTotals,
|
itemsTotals,
|
||||||
currency,
|
currency,
|
||||||
|
disabled,
|
||||||
}: ProformaLineFooterEditorProps) => {
|
}: ProformaLineFooterEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -205,6 +307,7 @@ export const ProformaLineFooterEditor = ({
|
|||||||
<FormFieldLabel>Descuento global (%):</FormFieldLabel>
|
<FormFieldLabel>Descuento global (%):</FormFieldLabel>
|
||||||
<div className="flex items-center gap-2 w-24">
|
<div className="flex items-center gap-2 w-24">
|
||||||
<PercentageField
|
<PercentageField
|
||||||
|
disabled={disabled}
|
||||||
maxFractionDigits={2}
|
maxFractionDigits={2}
|
||||||
minFractionDigits={0}
|
minFractionDigits={0}
|
||||||
name={"globalDiscountPercentage"}
|
name={"globalDiscountPercentage"}
|
||||||
|
|||||||
@ -5,11 +5,7 @@ import { PercentageField } from "@repo/rdx-ui/components";
|
|||||||
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
import {
|
import { ProformaUpdatePaymentEditor, ProformaUpdateRecipientEditor } from ".";
|
||||||
ProformaUpdatePaymentEditor,
|
|
||||||
ProformaUpdateRecipientEditor,
|
|
||||||
ProformaUpdateSettingsEditor,
|
|
||||||
} from ".";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type {
|
import type {
|
||||||
@ -113,7 +109,6 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
totals={totalsCtrl.totals}
|
totals={totalsCtrl.totals}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProformaUpdateSettingsEditor className="w-full" />
|
|
||||||
<ProformaUpdatePaymentEditor
|
<ProformaUpdatePaymentEditor
|
||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={isSubmitting || paymentCtrl.isLoading}
|
disabled={isSubmitting || paymentCtrl.isLoading}
|
||||||
|
|||||||
@ -35,10 +35,12 @@ export const ProformaUpdateItemsEditor = ({
|
|||||||
<ProformaLineEditor
|
<ProformaLineEditor
|
||||||
addItemAtStart={itemsCtrl.addItemAtStart}
|
addItemAtStart={itemsCtrl.addItemAtStart}
|
||||||
appendItem={itemsCtrl.appendItem}
|
appendItem={itemsCtrl.appendItem}
|
||||||
|
disabled={disabled}
|
||||||
duplicateItem={itemsCtrl.duplicateItem}
|
duplicateItem={itemsCtrl.duplicateItem}
|
||||||
fields={itemsCtrl.fields}
|
fields={itemsCtrl.fields}
|
||||||
getItemAmounts={itemsCtrl.getItemAmounts}
|
getItemAmounts={itemsCtrl.getItemAmounts}
|
||||||
getItemErrorMessage={itemsCtrl.getItemErrorMessage}
|
getItemErrorMessage={itemsCtrl.getItemErrorMessage}
|
||||||
|
getItemIsValued={itemsCtrl.getItemIsValued}
|
||||||
insertItemAfter={itemsCtrl.insertItemAfter}
|
insertItemAfter={itemsCtrl.insertItemAfter}
|
||||||
insertItemBefore={itemsCtrl.insertItemBefore}
|
insertItemBefore={itemsCtrl.insertItemBefore}
|
||||||
itemsTotals={itemsCtrl.totals}
|
itemsTotals={itemsCtrl.totals}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export const ProformaUpdateRecipientEditor = ({
|
|||||||
}: ProformaUpdateRecipientEditorProps) => {
|
}: ProformaUpdateRecipientEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const showActions = !(readOnly || disabled);
|
const showActions = !readOnly;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
@ -66,9 +66,10 @@ export const ProformaUpdateRecipientEditor = ({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<CardFooter className="flex flex-col space-y-4">
|
<CardFooter className="flex gap-2 2xl:justify-between">
|
||||||
<Button
|
<Button
|
||||||
className="w-full justify-center"
|
className="justify-center"
|
||||||
|
disabled={disabled}
|
||||||
onClick={onCreateCustomerClick}
|
onClick={onCreateCustomerClick}
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -76,12 +77,12 @@ export const ProformaUpdateRecipientEditor = ({
|
|||||||
<PlusIcon className="mr-2 size-4" />
|
<PlusIcon className="mr-2 size-4" />
|
||||||
{t("customers.selected_customer.new_customer", "Nuevo cliente")}
|
{t("customers.selected_customer.new_customer", "Nuevo cliente")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full justify-center"
|
className="justify-center"
|
||||||
|
disabled={disabled}
|
||||||
onClick={onChangeCustomerClick}
|
onClick={onChangeCustomerClick}
|
||||||
type="button"
|
type="button"
|
||||||
variant={selectedCustomer ? "secondary" : "default"}
|
variant={selectedCustomer ? "outline" : "default"}
|
||||||
>
|
>
|
||||||
<RefreshCwIcon className="mr-2 size-4" />
|
<RefreshCwIcon className="mr-2 size-4" />
|
||||||
{selectedCustomer
|
{selectedCustomer
|
||||||
|
|||||||
@ -110,11 +110,32 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
"form_fields.proformas.tax_regime_code.placeholder",
|
"form_fields.proformas.tax_regime_code.placeholder",
|
||||||
"Selecciona el régimen fiscal para esta proforma"
|
"Selecciona el régimen fiscal para esta proforma"
|
||||||
)}
|
)}
|
||||||
readOnly={readOnly || taxCtrl.usesPerLineTax}
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator className="w-full col-start-1 col-span-full" />
|
||||||
|
|
||||||
|
<SwitchField
|
||||||
|
className="md:col-span-12 md:col-start-1 not-disabled:cursor-pointer"
|
||||||
|
disabled={disabled}
|
||||||
|
label="Mismo IVA en todas las líneas de la proforma"
|
||||||
|
name="hasTaxPercentage"
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
checked ? taxCtrl.disablePerLineTaxes() : taxCtrl.enablePerLineTaxes();
|
||||||
|
}}
|
||||||
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
className="md:col-span-4 md:col-start-1"
|
className="md:col-span-4 md:col-start-1 lg:col-span-2 2xl:col-span-6"
|
||||||
|
description={
|
||||||
|
taxCtrl.usesSingleTax
|
||||||
|
? t("form_fields.proformas.default_tax_percentage.description", "")
|
||||||
|
: t(
|
||||||
|
"form_fields.proformas.default_tax_percentage.description",
|
||||||
|
"Para las lineas de detalle nuevas"
|
||||||
|
)
|
||||||
|
}
|
||||||
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputClassName="bg-background"
|
inputClassName="bg-background"
|
||||||
@ -131,23 +152,12 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
"form_fields.proformas.default_tax_percentage.placeholder",
|
"form_fields.proformas.default_tax_percentage.placeholder",
|
||||||
"Selecciona IVA"
|
"Selecciona IVA"
|
||||||
)}
|
)}
|
||||||
readOnly={readOnly || taxCtrl.usesPerLineTax}
|
readOnly={readOnly}
|
||||||
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator className="w-full col-start-1 col-span-full" />
|
<Separator className="w-full col-start-1 col-span-full" />
|
||||||
|
|
||||||
<SwitchField
|
|
||||||
checked={taxCtrl.usesSingleTax}
|
|
||||||
className="md:col-span-12 md:col-start-1 not-disabled:cursor-pointer"
|
|
||||||
disabled={disabled || readOnly}
|
|
||||||
label="Mismo IVA en todas las líneas de la proforma"
|
|
||||||
name=""
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
checked ? taxCtrl.disablePerLineTaxes() : taxCtrl.enablePerLineTaxes()
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SwitchField
|
<SwitchField
|
||||||
className="md:col-span-12 md:col-start-1"
|
className="md:col-span-12 md:col-start-1"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -158,7 +168,7 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
"Recargo de equivalencia"
|
"Recargo de equivalencia"
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{taxCtrl.hasRecPercentage ? (
|
{taxCtrl.hasRecPercentage && !taxCtrl.usesPerLineTax ? (
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{PercentageHelper.formatPercent(taxCtrl.recPercentage ?? 0)}
|
{PercentageHelper.formatPercent(taxCtrl.recPercentage ?? 0)}
|
||||||
</span>
|
</span>
|
||||||
@ -179,7 +189,7 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
className="md:col-span-4 md:col-start-1"
|
className="md:col-span-4 md:col-start-1 lg:col-span-2 2xl:col-span-6"
|
||||||
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
||||||
disabled={disabled || !taxCtrl.hasRetentionPercentage}
|
disabled={disabled || !taxCtrl.hasRetentionPercentage}
|
||||||
inputClassName="bg-background"
|
inputClassName="bg-background"
|
||||||
@ -196,7 +206,7 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
"form_fields.proformas.default_tax_percentage.placeholder",
|
"form_fields.proformas.default_tax_percentage.placeholder",
|
||||||
"Selecciona IVA"
|
"Selecciona IVA"
|
||||||
)}
|
)}
|
||||||
readOnly={readOnly || taxCtrl.usesPerLineTax}
|
readOnly={readOnly}
|
||||||
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||||
/>
|
/>
|
||||||
</FormSectionGrid>
|
</FormSectionGrid>
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export const ProformaUpdatePage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex flex-col overflow-y-scroll bg-muted">
|
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
||||||
<FormProvider {...updateCtrl.form}>
|
<FormProvider {...updateCtrl.form}>
|
||||||
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||||
<ProformaUpdateHeader
|
<ProformaUpdateHeader
|
||||||
|
|||||||
@ -2,11 +2,18 @@ import { UniqueID } from "@repo/rdx-ddd";
|
|||||||
|
|
||||||
import type { ProformaItemUpdateForm } from "../entities";
|
import type { ProformaItemUpdateForm } from "../entities";
|
||||||
|
|
||||||
export const buildProformaItemUpdateDefault = (position: number): ProformaItemUpdateForm => {
|
export type buildProformaItemUpdateDefaultContext = {
|
||||||
|
defaultTaxPercentage?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildProformaItemUpdateDefault = (
|
||||||
|
position: number,
|
||||||
|
context?: buildProformaItemUpdateDefaultContext
|
||||||
|
): ProformaItemUpdateForm => {
|
||||||
return {
|
return {
|
||||||
id: UniqueID.generateNewID().toString(),
|
id: UniqueID.generateNewID().toString(),
|
||||||
position,
|
position,
|
||||||
isValued: false,
|
//isValued: false, <- campo calculado en frontend, no se mapea desde la API
|
||||||
|
|
||||||
description: null,
|
description: null,
|
||||||
|
|
||||||
@ -14,7 +21,7 @@ export const buildProformaItemUpdateDefault = (position: number): ProformaItemUp
|
|||||||
unitAmount: null,
|
unitAmount: null,
|
||||||
itemDiscountPercentage: null,
|
itemDiscountPercentage: null,
|
||||||
|
|
||||||
ivaPercentage: null,
|
taxPercentage: context?.defaultTaxPercentage ?? null,
|
||||||
recPercentage: null,
|
recPercentage: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export const buildProformaUpdateDefault = (): ProformaUpdateForm => {
|
|||||||
retentionPercentage: 15,
|
retentionPercentage: 15,
|
||||||
|
|
||||||
paymentMethodId: null,
|
paymentMethodId: null,
|
||||||
|
paymentTermId: null,
|
||||||
|
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -166,7 +166,6 @@ const resolveSingleRecCode = (formData: ProformaUpdateForm): string | null => {
|
|||||||
if (!formData.hasRecPercentage) {
|
if (!formData.hasRecPercentage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getProformaRecCode(formData.recPercentage);
|
return getProformaRecCode(formData.recPercentage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,9 @@ export const calculateProformaLineTotal = (line?: ProformaLineInput): ProformaLi
|
|||||||
subtotalBeforeGlobalDiscount: 0,
|
subtotalBeforeGlobalDiscount: 0,
|
||||||
globalDiscountAmount: 0,
|
globalDiscountAmount: 0,
|
||||||
totalDiscountAmount: 0,
|
totalDiscountAmount: 0,
|
||||||
|
subtotalTaxable: 0,
|
||||||
|
taxAmount: 0,
|
||||||
|
totalAmount: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +36,14 @@ export const calculateProformaLineTotal = (line?: ProformaLineInput): ProformaLi
|
|||||||
|
|
||||||
const totalDiscountAmount = itemDiscountAmount + globalDiscountAmount;
|
const totalDiscountAmount = itemDiscountAmount + globalDiscountAmount;
|
||||||
|
|
||||||
|
const subtotalTaxable = subtotalBeforeDiscounts - itemDiscountAmount;
|
||||||
|
|
||||||
|
const taxPercentage = toCalculationNumber(line.taxPercentage);
|
||||||
|
|
||||||
|
const taxAmount = percentageAmount(subtotalTaxable, taxPercentage);
|
||||||
|
|
||||||
|
const totalAmount = toCalculationNumber(subtotalTaxable + taxAmount);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subtotalBeforeDiscounts: roundMoney(subtotalBeforeDiscounts),
|
subtotalBeforeDiscounts: roundMoney(subtotalBeforeDiscounts),
|
||||||
|
|
||||||
@ -43,5 +54,11 @@ export const calculateProformaLineTotal = (line?: ProformaLineInput): ProformaLi
|
|||||||
globalDiscountAmount: roundMoney(globalDiscountAmount),
|
globalDiscountAmount: roundMoney(globalDiscountAmount),
|
||||||
|
|
||||||
totalDiscountAmount: roundMoney(totalDiscountAmount),
|
totalDiscountAmount: roundMoney(totalDiscountAmount),
|
||||||
|
|
||||||
|
subtotalTaxable: roundMoney(subtotalTaxable),
|
||||||
|
|
||||||
|
taxAmount: roundMoney(taxAmount),
|
||||||
|
|
||||||
|
totalAmount: roundMoney(totalAmount),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user