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